1. JVM
1.1 JVM简介
- Java虚拟机(JVM)一种用于计算机设备的规范,可用不同的方式(软件或硬件)加以实现。编译虚拟机的指令集与编译微处理器的指令集非常类似。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域,其主要作用就是在软件层面屏蔽不同操作系统在底层硬件和指令上的区别,通过使用不同系统对应的JVM,将同一个.class文件翻译成不同操作系统的机器码。
java编译器输出的指令流,基本上是一种基于栈的指令集架构。
.class文件
是其可执行文件,就是一些二进制字节码文件,表示了相应的jvm执行指令。只要是可以编译成.class文件的语言都可以在jvm上执行,如kotlin、scala等。- Java EE是以Java SE为基础的。所以并没有“JVM for Java EE”这么一说,只有“JVM for Java SE”,可以用于Java SE与Java EE。主要产品有:以前主流产品有
HotSpot VM(sun公司)
、JRockit VM(BEA公司)
、J9 VM(IBM)
三家,后来Oracle收购了前两家JRockit VM(BEA公司)
成了炮灰,三大家变成两大家。另外针对JavaME(移动端)的java虚拟机有:CLDC-HI VM(oracle/sun)
、J9 VM(IBM)
。 - 光谈部署量的话,搞不好现在部署量最多的JVM是
Dalvik / ART (Google)
,安卓就部署这两个。虽然Google会告诉大家Dalvik和ART不是“JVM”,但大家都知道骨子里它就是不折不扣的JVM,毫无疑问。它们的设计处处有标注对JVM规范的参考,以保证语义符合JVM规范的要求;那个基于寄存器的字节码设计只是一种实现优化而已。
主要参考文献:
目前主流的 Java 虚拟机
分配担保机制
1.2 类加载机制
- 已经加载的类不会加载第二次。
- 加载子类前先加载父类,实例化子类前先实例化父类。
- 一般情况是先初始化完成后再实例化对象。
1.2.1 类的生命周期
特别注意点:
1. 类被加载到内存中需要经过三个过程:加载、连接、初始化
,这三个过程就叫做类初始化或类加载
;
2. 区别类初始化过程
和类实例化过程
,一般来说类实例化是指类加载到内存之后创建对象的过程,但是类实例化也不一定会在类的初始化步骤完成之后再实例化。
3. 类的初始化步骤是将静态变量赋值,执行静态代码块,类实例化过程是对成员变量的赋值(如果有)、代码块以及构造方法。类初始化如果未卸载,只会执行一次,而类实例化过程或执行n次。我们new一个对象时,有时候会执行静态代码块内容,是因为类未加载,所以需要先加载。而有时候没有执行是因为,类已经加载了,不需要再次加载,当然只会执行成员变量、代码块、构造方法。
1. 加载
- 根据类的全名限定符,获取class二进制流(保存到硬盘的class文件,不是class二进制流获取的唯一方式,也有可能从网络中获取等)。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据存储结构。
- 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象(Class对象是对class字节码文件的描述),作为方法区这个类的各种数据的访问入口;
注意:class对象对于hotspot虚拟机是存在方法区,但jvm规范并没有相关规定
2. 验证
检验class是否符合JVM文件规范,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。具体见最后面。
3. 准备
类变量(static变量)分配内存&赋予默认值,注意是默认值不是你赋予的值。默认值如下:
特殊情况:static final
修饰的成员变量
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设类变量value定义为:public static final int value = 123;
。编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
4. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
基本概念
- 符号引用:以一组符号来描述所引用的目标(通过符号来找到目标),符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用可以是:1. 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针);2. 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量);3. 一个能间接定位到目标的句柄。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
猜想与疑问:
1、我感觉这个阶段就可以进行类的实例化了(还没有足够的证明),当然在开始类的实例化阶段时是可以开始类的实例化了,参考的博客中试验了的。
2、另外,静态成员方法和普通成员方法是不是在加载时就已经分配内存,这个阶段进行引用转换,在实例化阶段进行成员方法的引用转换?
5. 初始化
这个阶段主要是执行类变量赋值操作(如果有)和静态代码块;
详细初始化过程可以参考:类加载时机和加载过程
6. 使用
引用分为直接引用和被动引用,主动引用和被动引用在类的初始化时机中有讲解,这里把实例化对象过程放在使用来说。
实例化过程:
- 如果有父类先实例化父类;
- 在堆中为保存对象的实例变量分配内存;
- 为实例变量初始化为默认的初始值(有点像类加载的准备阶段);
- 为实例变量赋正确的初始值,有三种技术完成赋值:
- 如果对象是clone() 创建的,jvm把原实例变量中的值拷贝到新对象中;
- 如果是通过ObjectInputStream类的readObject()调用反序列化的,jvm从输入流中读取的值来初始化实例变量;
- jvm调用对象的实例化方法把对象的实例变量初始化为正确的初始值;
- 主要顺序为:先执行成员变量赋值、执行静态代码块、再执行构造方法。
总结:
类的实例化中,成员变量可能会被赋值四次,默认赋值,成员变量赋值,静态代码块赋值,构造方法赋值。
再次强调:实例化过程自身不会执行静态部分,之所以会执行是因为:如果没有类加载,实例前需要类加载,类加载过程的初始化阶段会执行静态部分。
具体可以参考:类的初始化与实例化
7. 卸载
对象的回收后面会将
如果以下三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
注意这里是类的回收,而不是对象的回收。
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
1.2.2 类的加载时机
对于类加载时机,虚拟机规范中并没有对此进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
1.2.3 类初始化时机
- 注意类没有卸载时,不会重复加载。
- 虚拟机规范中有严格规指明有且只有五种情况必须立即对类进行初始化(而这一过程自然发生在加载、验证、准备之后)。
- 这五种场景中的行为称为对一个类进行主动引用。除此之外,其它引用类的方式,都不会触发初始化,称为被动引用。
-
遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类java.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
-
使用new关键字实例化对象的时候;
-
读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
-
调用一个类的静态方法的时候。
-
-
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
-
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
-
当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
被动引用(不会导致类初始化)场景
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 通过数组定义来引用类,不会触发此类的初始化。
- 引用类的常量,不会引起类的初始化,如static final修饰的常量。
实际场景个人总结:
1. 遇到new时
某个类第一次new时会加载这个类,当然new除了加载还会实例化。
2. 子类被加载时
<font color="red">子类被加载时,必须加载其父类。所以类的继承层次不宜过多。
3. 启动类
jvm启动时,用户需要指定一个执行的主类(包含main()方法的类)虚拟机会先执行这个类。
4.调用类的静态变量或静态方法时
用类名直接调用相应的静态方法和静态变量也会触发类的加载;
5. 反射
调用`class.forName(“全类名”);`时,会加载类。
6. 注意
1. 当调用的是类名调用static final修饰变量名时不会初始化,常量在编译阶段已经确认,所以一般对于常量值,一般加上final;
2. 通过子类来引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化;
1.2.4 类实例化时机
实例化的时候,如果类未加载,也会触发类加载,所以上面的类加载时机很多也是类实例化时机
- 使用new关键字创建对象
- 使用Class类的newInstance方法(反射机制);
Student student2 = (Student)Class.forName("Student类全限定名").newInstance();
或者:
Student stu = Student.class.newInstance();
- 使用Constructor类的newInstance方法(反射机制)
- 使用Clone方法创建对象
- 使用(反)序列化机制创建对象
1.2.5 【类实例化】和【类的初始化阶段】的区别
- 两者都是需要在类加载的加载和连接执行完后执行,一般类实例化在类初始化阶段之后;
- 由于类实例化必然会发生类的初始化阶段(没有百分百把握肯定是这样),因此,实例化的触发时机必然是类初始化阶段的触发时机;
- 类的初始化阶段的作用是执行静态成员变量赋值&静态代码块,而类实例化是对普通成员变量赋值、执行普通方法块、执行构造方法;
- 一般情况下先类初始化阶段执行完,然后再类的实例化。
你需要明白的细节
1.类的加载时机、类的初始化时机、类的实例化时机;
2.类的初始化阶段干了什么,类的实例化干了什么;
主要参考文献:
1.2.6 案例
当你主动引用(这里以new为例)一个类时执行顺序如下,如果类未被初始化,那么肯定会先执行加载、连接、初始化(执行静态部分):
原因解释:
- 加载子类时先加载父类。
- 一般,先初始化:静态变量赋值、执行静态代码块。后实例化对象:普通变量赋值、执行普通代码块、执行构造方法。
- 当静态代码块或静态变量赋值中有其它类的主动引用时(如:new)会按上面顺序先执行完其它类的的一个引用过程,然后再按上面顺序执行本类剩下的。
- 当静态代码块或静态变量赋值中有本类的主动引用(如:new)时,由于类已加载,所以会直接先实例化(了解)。
- 已经加载的类不会重复加载,当然静态部分也不会被重复执行。
主方法(静态方法)并不会在创建对象后执行,试的的过程中并没有执行构造方法或代码块。
1.2.3 类加载器种类
1. 类加载器种类
2. 类加载器加载的类包
应用类加载器: 加载classpath下的,即自己打包时在classes目录和lib目录中的类
自定义类加载器: 加载哪个路径下的类,需要自己指定。
3. 自定义类加载器
ClassLoader
类源码上注解是这么写的,使用时调用父类ClassLoader
类中的loadClass()
方法就可以了,它会实现双亲委派原则,进行相应的调用:
1.2.4 类加载机制
1. 全盘负责委托机制
当一个 ClassLoader(类加载器)加载一个类的时候,该类所依赖和引用的类也由这个 ClassLoader载入。除非显示的使用另一个 Classloader。
就比如说在A类中创建了B类,A类是用 aclassload加载的,因为B也是依赖的A,所以B类也是由 Aclassload加载的。A引用的所有类也是会由 Aclassload这个类加载器进行加载。当然该加载器也只是调用了。
只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class字节码文件生成Class对象由“双亲委派”机制完成。
2. 双亲委派机制
1. 双亲委派机制图解
2. 双亲委派机制好处
左边看是为了防止重复加载;
右边看是为了java的核心API被篡改;
1.2.5 主要参考
怎么没看见加载方法?
不管是静态方法还是动态方法,方法应该都已经放在方法区了,在方法表(应该就是class的数据存储结构的一部分)中,也就是说在加载的时候就已经分配内存。只是静态方法在解析的时候会将符号引用转换为直接引用。而普通方法在调用的时候或实例化的时候进行的转换,像栈帧中的动态链接就存放的方法直接引用地址,这个地址指向了方法区中方法对应的指令码。当然只是猜想。