深入理解Java虚拟机读后感
1、语言无关性
实现语言无关性的基础仍然是虚拟机和字节码存储格式。
Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与"Class"文件这种特定的二进制文件格式所关联。
虚拟机丝毫不关心Class的来源是什么语言,它与程序语言之间的关系与下图所示:
每个Class文件的头4个字节被称为魔数(0xCAFEBABE),它唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
2、常量池
常量池中主要存放两大类常量:字面量和符合引用。
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
符号引用则属于编译原理方面的概念,主要包括下面几类常量:
被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法的句柄和方法类型
- 动态调用点和动态常量
3、访问标志
在常量池结束之后,紧接着的2个字节代表访问标志,用于标识一些类或者接口的层次的访问信息。
包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final...等等。
4、类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据(u1、u2、u4、u8分别表示1byte、2byte、4byte、8byte)。
而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。
5、字段表集合、方法表集合、属性表集合
字段表用于描叙接口或者类中声明的变量,方法表用于描述方法,属性表用于描述类、方法、接口或字段的属性。
6、字节码指令
无
7、类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历如下其他阶段:
加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备、解析三个阶段统称为连接。
加载、验证、准备、初始化和卸载这五个阶段的顺序时确定的,而解析阶段则不一定:
它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性。
《Java虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化(如果类型没有初始化):
遇到 new、getstatic、普通static、invokestatic这四条字节码指令,这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候、读取或设置一个类型的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、调用一个类型的静态方法的时候
- 使用java.lang.reflect包的方法对类型进行反射调用的时候
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。
- 当使用JDK7使用的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄对应的类没有进行初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK8新加入的默认方法(default关键字修饰的方法)时,如果这个接口的实现类发生初始化,那该接口要在其之前被初始化。
例如,以下代码:
public class SuperClass { public final static int superValue = 0; static { System.out.println("SuperClass init!"); } } public class SubClass extends SuperClass { public static int subValue = 1; static{ System.out.println("SubClass init"); } } public class Main { static { System.out.println("Main init"); } public static void main(String[] args) { System.out.println(SubClass.superValue); System.out.println(SubClass.subValue); } }
猜猜结果输出啥?
没错,首先是第四条,所以先输出"Main init",然后呢?
然后是第一条,对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
但是由于父类的静态字段加了final关键字,所以父类不会初始化,所以只会输出"0"。然后就是第一条和第三条:所以会输出"SuperClass init"、"SubClass init"、"1"。
Main init 0 SuperClass init! SubClass init 1
8、类加载过程
加载、验证、准备、解析跳过
初始化阶段就是执行类构造器<clinit>方法的过程,该方法不是程序员在java代码中直接编写的方法,而是javac编译器的自动生成物。
该方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并而成的。
编译器收集的顺序由语句在源文件中出现的顺序决定,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量可以赋值但不能访问。
<clinit>方法对于类或接口来说并不是必须的,如果一个类没有静态语句块,也没有对变量的赋值操作,那么编译器不会为这个类生成<clinit>方法。
另外,Java虚拟机必须保证一个类的<clinit>方法在多线程环境中被正确地加锁同步:
如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>方法,其他线程阻塞等待。