JVM笔记

jvisualvm:jdk的监控工具,在cmd中输入打开

类加载

在java代码中,类型的加载、连接、初始化过程都是在陈鼓型云性期间完成的,为程序开发人员提供创造的可能性,Java是静态语言,但它拥有很多动态语言的特性

类加载器深入剖析:

Java虚拟集程序的生命周期

在如下几种情况,java虚拟集将结束生命周期:

  • 执行System.exit()方法;

  • 程序正常执行结束

  • 程序在执行过程中域到了异常或错误而终止、

  • 由于操作系统出现错误导致java虚拟机进程终止

类的加载、连接、初始化

  • 加载:查找并加载类的二进制数据

  • 连接:

    1. 验证:确保被加载的类的正确性

    2. 准备:为类的静态变量分配内存,并将其初始化为默认值(int 初始值是:0;对象初始值是:null)

    例如:static int i=1; 在准备阶段时:static int i = 0;

3. 解析:把类中的符号引用转换为直接引用(把=号转换指针指向值的地址)

  • 初始化:为类的静态变量赋予正确的初始值

  • 类的实例化:

    为新的对象分配内存

    为实例变量符默认值

    为实例变量赋正确的初始值

    java编译器为它编译的每一个类都至少生成一个实例初始化方法,在java的class文件中,这个实例初始化方 法被成为“<init>”针对源代码中每一个类的构造方法,在java编译器都产生一个<init>方法

类的加载

文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest01.java

F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest02.java

类的加载值的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象用来粉妆泪的方法区内的数据结构

 

类加载器类型

1、java虚拟机自带的加载器

  • 根类加载器(Bootstrap)

  • 扩展类加载器(Extension)

  • 系统(应用)类加载(system)

2、用户自定义的类加载器

  • java.lang.ClassLoader的子类

  • 用户可以定制类的加载方式

     

    类加载器的父亲(双亲)委托机制

    双亲委派模式的工作原理的是:

    如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

    在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了跟类加载器之外,其余的类加载器都有且只有一个父加载器

    当用户定义加载器后,会让根类加载器先行加载,如果根类加载器不能加载会让子类执行加载,依次后推最终会由系统加载器执行

    如果有一个类加载器能够成功加载,那么这个类加载器被成为定义类在加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被成为初始类加载器

    优点:

    1、可以确保java核心库的类型安全:所有的java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.object这个类会被加载到java虚拟机中,如果这个加载过程时有java应用自己的类加载器所完成的,那么很可能会在JVM中存在多个版本的Object类,而且这些类之间还是不兼容的相互不可见(正是明明那个空间在发挥着作用)借助于双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了java应用使用的都是同一个版本的java核心类库,他们之间是相互兼容的

    2、可以确保java核心类库所提供的类不会被自定义的类所替代

    3、不同的类加载器可以为相同名称的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间时不兼容的,这就相当于在java虚拟机内部创建了一个又一个相互隔离的java类空间,这类技术在很多框架中都得到实际应用。

    所有自定义类的加载器都是系统加载器执行(AppClassLoader)

    java自带的类由根类加载器加载(BootstrapClassLoader),当使用getClassLoader()方法获得加载器时返回null代表根类加载器

    文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest05.java

     

    类加载器并不需要等到某个类被首次主动使用时在加载它

    JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了,class文件确实或存在错误,类加载器必须在程序首次主动使用时才报告做错误,如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

    例如:当通过子类使用父类的静态变量时,初始化父类但不会初始化子类,但是子类会被加载到JVM中

     

类加载器ClassLoader:

获得当前类的加载器ClassLoader

class.getClassLoader();

获得当前线程上下文的ClassLoader

Thread.currentTread().getContextClassLoader()

获得系统的Classloader

ClassLoader.getSystemClassLoader()

获得调用者的ClassLoader

DriverManager.getCallerClassLoader()

查看ClassLoader的API得知:

数组类的类对象不是由类加载器创建的,而是根据Java运行时的需要自动创建的。getclassloader()返回的数组类的类装入器与其元素类型的类装入器相同;如果元素类型是基本类型,则数组类没有类加载器,返回null。

自定义类加载器:

控制台将自定义类加载器设置为系统加载器:

java -Djava.system.class.loader=自定义加载器路径 自定义加载器路径

输出目前的系统加载器:

ClassLoader.getSystemClassLoader();

文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangCustomClassLoader.java

命名空间:
  • 每个类加载器都有自己的命名空间,命名空间有该加载器及所有父加载器所加载的类组成

  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类

  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

  • 相同的加载器不同的命名空间加载同一个类时,哈希值时一样的

线程上下文类加载器(Context ClassLoader)
线程上下文类加载器是从JDK1.2开始引用的,类Thread中的getCintextClassLoader()与setContextClassLoader(ClassLoader Cl)
分别用来获取和设置上下文类加载器

如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器
java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源

类的卸载

当某个类被加载、连接和初始化后,它的生命周期就开始了。当代表该列的Class对象不在被引用,即不可触及时,Class对象会结束生命周期,该类在方法区内的数据也会被卸载,从而结束该类的生命周期。

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载,由用户的自定义的累计在其所加载的类是可以被卸载的

类的连接

类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去

类的验证的内容
  • 类文件的结构检查

  • 语义检查

  • 字节码验证

  • 二进制兼容性的验证

准备

为类的静态变量分配内存,并将其初始化为默认值(int 初始值是:0;对象初始值是:null)

文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangClassMytest04.java

类的初始化

为类的静态变量赋予正确的初始值,在程序中静态变量的初始化有两种途径

1、在静态变量的声明出进行初始化

2、在静态代码块中进行初始化

静态变量的声明语句,以及静态代码块都被看做类的初始化语句,java虚拟机会按照初始化语句在类文件中的先后顺序在依次执行他们

 

类的初始化步骤

 

 

类的初始化时机:
java程序对类的使用方式:
1、主动使用
  • 创建类的实例(new 类)

  • 访问某个类或接口的静态变量,或者对该静态变量赋值(类.静态变量)

  • 调用该类的静态方法(类.静态方法)

  • 反射(如:Class.forName("com.test.Test"))

  • 初始化一个类的子类

  • java虚拟机启动时被标明为启动类的类(如:程序启动的入口main)

  • JDK1.7开始提供的而动态语言的支持:

java.lang.invoke,MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应 的类没有初始化则初始化。

2、被动使用

除了以上七种情况,其他的方式都被看做时对类的被动使用,都不会导致类的初始化

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化

文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest06.java

所有的Java虚拟机实现必须在每个类或接口被java程序“首次主动使用”时才初始化

实例项目:F:a.codeideaHome03JVMclassloadersrccomzhangclassloader.java

3、接口的初始化

文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest03.java

F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest04.java

当java虚拟机初始化一个类时,要求他的所有父类已经被初始化,但是这条规则并不适用于接口

  • 在初始化一个类时,并不会先初始化它所实现的接口

  • 在初始胡啊一个接口时,并不会出现初始化它的父接口

因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化

注意:调用类中的static final常量时并不会触发该类的初始化,但是调用接口中的static final常量时便会触发该接口的初始化

 

数据库连接知识:

Class.forName("com.mysql.jdbc.Driver"):

加载驱动会寻找java.Driver文件正常是返回null,然后根据jdbc的spi的约定机制加载相应的驱动

 

DriverManager.getConnection(....):

spi加载驱动可以不写Class.forName("com.mysql.jdbc.Driver"):然后内部加载驱动,对比调用者加载器和spi加载时的加载器是否一致(判断是不是一个命名空间和是否是一个加载器)如果是,开始连接数据库。

DriverManager.java文件内:Reflection.getCallerClass():获得调用者的class

 

运行JVM时,自定参数和选项

option:JVM启动时的选项

-XX:+<option>, 表示开启option选项

-XX:-<option>,表示关闭option选项

-XX:<option>=<value>,表示将option选项的值设置成value

例如: -XX:+TraceClassLoading:用于追踪加载信息并打印出来

常量编译阶段

文件示例:F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangconstant.java

F:a.codeideaHome03JVMJVM_01srcmainjavacomzhangMytest01.java

常量在编译阶段会存入调用这个常量的方法所在的类的常量池中
本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化
注意:常量的值如果在编译器不能确定还是会加载常量所在的类 例如使用随机数作为常量的值

 

java字节码

 

 

Terminal命令

进入outproductionclasses文件下

反编译命令:javap -c 要反编译的class文件:

 

javap -verbose -p 类相对路径 命令:

分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息

字节码解析
  1. 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数的固定值:0xCAFEBABE

    魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号),这里的版本号为00 00 00 34换算成十进制,表示次版本号为0,主版本好为52,所以该文件的版本号(JDK版本):1.8.0 可以通过javap -verbose命令验证这一点

  2. 常量池(constant pool):紧接着主版本号之后的就是常量池入口,一个java类中定义的很多信息都是由常量池来维护和描述,可以将常量池看作是class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,java中声明为final的常量值等,二符号引用如类的接口的全局限定名,字段的名称和描述符,方法的名称和描述符。

    常量池内存放不会改变的代码

  3. 常量池的总体结构:java类所对应的常量池主要由常量池数量与常量池数组这两部分共同构成。常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟在常量池数量之后,常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数-1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达(不引用任何一个常量池)的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以常量池的索引从1而非0开始

  4. 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型各代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示:B byte,C char,D double,F float,I int,J long,S short,Z boolean ,

    V void,L 对象类型:如Ljava/lang/String;

  5. 对于数组类型来说,每一个唯独使用一个前置的 [ 表示,如int[] 被记录为[I ,String[] [] 被记录为[[Ljava/lang/String;

  6. 用描述符描述方法是,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String test(int id,String name)的描述是:(I,Ljava/lang/String) Ljava/lang/String;

 

字节码例子:

 

 

 

 

 

 

 

 

this:

对于Java类中的每一个实例方法(非static静态方法),其在编译后所生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个(this),它位于方法的第一个参数位置处,这样,我们就可以在Java的实例方法中使用this来去访问当前对象的属性以及其他方法。

这个操作实在编译期间完成的,即由javac编译器在编译的时候滴哦this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间由JVM在调用实例方法时,自动向实例方法传入该this参数,所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。

解释执行和编译执行

现代的JVM在执行java代码时,通常都会将解释执行和编译执行二者结合起来进行。

解释执行:

通过解释器来读取字节码,遇到相应的指令就去执行该指令。

编译执行:

通过即时编译器(Just IN Time,JIT)将字节码转换为本地机器码来执行;现代JVM会根据代码热点来生成相应的本地机器码。

 

基于栈的指令集与基于寄存器的指令集之间的关系

1、JVM执行指令时所采取的方式是基于栈的指令集。

2、基于站的指令集主要的操作有入栈和出栈两种

3、基于栈的指令集的优势在于它可以在不同的平台之间移植,er基于寄存器的指令集是与应键架构紧密相关联的,无法做到可移植。

4、基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集实在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它实在告诉缓冲区中执行的,速度要快很多,虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些。

原文地址:https://www.cnblogs.com/baroque/p/JVM.html