主要是阅读《深入理解Java虚拟机》 之后所做的一些笔记。
更多内容可以访问我的个人博客。
无关性的基石
-
Sun公司以及其他VM提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,实现了程序的“一次编写,到处运行”。
-
实现平台、语言无关性的基础是:虚拟机和字节码存储格式
-
Java VM不和任何语言绑定,只与“Class文件”这种特定的二进制文件格式关联,VM不关心Class文件的来源,使用JRuby等其他语言的编译器一样可以把程序代码编译成Class文件。
虚拟机类加载机制
VM把描述类的数据从Class字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
与在编译时进行连接工作的语言不同,在Java语言里,类型的加载、连接、初始化都是在程序运行期完成的。虽然稍微地增加了性能开销,但提供了高度的灵活性,Java动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如编写一个面向接口的应用程序,等到运行时再指定其实际的实现类;或者通过预定义的和自定义的类加载器,让一个本地的应用程序在运行时从网络或他处加载一个二进制流作为程序代码的一部分(Applet、Jsp、OSGi)
类的生命周期
加载 —> 验证 —> 准备 —> 解析 —> 初始化 —> 使用 —> 卸载
(验证、准备、解析统称为连接)
除了解析阶段,其他阶段都按这个顺序开始(不一定等前一个阶段结束,都一个阶段才开始,通常互相交叉地混合式进行),而解析阶段则不一定,某些情况下可以在初始化之后再开始(Java的运行时绑定,也称为动态绑定或晚期绑定)
类的引用场景
- 对类进行主动引用
虚拟机规范中规定**“有且只有”**这5种场景会触发类初始化(而加载、验证、准备自然在此之前开始)
①遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则先初始化。生成这四条指令常见的Java场景是:new关键字实例化对象时、读取或设置类的静态字段时(final修饰、已在编译期放入常量池的静态字段除外)、调用一个类的静态方法时。
②使用java.lang.reflect包的方法对类进行反射调用的时候,若未初始化,则初始化。
③当初始化一个类时,发现其父类还未初始化,则先触发其父类的初始化。
接口与类初始化的区别:接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口时才会初始化。
④虚拟机启动时,会先初始化主类(包含main()方法的那个类)
⑤当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄对应的类没有进行过初始化,则先初始化。(
没怎么看懂,动态连接的时候发现类未初始化?)
- 被动引用
除以上5种主动引用的场景外,其他所有引用类的方式都不会触发初始化,称为被动引用。
①通过子类引用父类的静态字段,不会导致子类初始化。 对于static字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类初始化
//父类 public class SuperClass{ static { System.out.println("SuperClass init!"); } public static int value = 123; } //子类 public class SubClass extends SuperClass{ static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String[] args){ System.out.println(SubClass.value); } }
输出
SuperClass init! 123
②通过数组定义来引用类,不会触发此类的初始化
public class NotInitialization { public static void main(String[] args){ SuperClass[] s = new SuperClass[10]; }
运行之后没有输出 SuperClass init!
③常量在编译阶段会存入调用类的常量池中(编译阶段通过常量传播优化),本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
public class ConstClass{ static{ System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization{ public static void main(String[] args){ System.out.println(ConstClass.HELLOWORLD); } }
运行之后没有输出 SuperClass init!
编译阶段,此常量HELLOWORLD的值已经被存储到了NotInitialization类的常量池中,之后NotInitialization对ConstClass.HELLOWORLD的引用实际都被转换为对自身常量池的引用了,也就是说,NotInitialization的Class文件中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
类加载的过程
加载 —> 验证 —> 准备 —> 解析 —> 初始化 —> 使用 —> 卸载
1. 加载
在加载阶段,虚拟机完成以下3件事:
①通过一个类的全限定名来获取定义此类的二进制字节流
②将字节流代表的静态存储结构转化为方法区的运行时数据结构
③在内存生成该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2. 验证
**①目的:**验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
②Java语言本身是安全的,编译器将拒绝编译不安全的行为(访问数组越界……)但Class文件不一定由源代码编译而来,甚至可以直接编写Class文件,所以验证是VM对自身保护的重要工作。
③验证包括:文件格式验证(是否符合Class文件格式)、元数据验证(语义校验,保证元数据符合Java语言规范)、字节码验证、符号引用验证
3. 准备
①是正式为类变量(static)分配内存并设置变量初始值阶段,这些变量所使用的内存都将在方法区中进行分配,注意这时候分配的是类变量不是实例变量,实例变量将在对象实例化时与对象一起分配在java堆中
②初始值是指对应类型的零值(**特殊情况:**被final修饰的变量,如public static final int value = 123; 则会在准备阶段即赋值123.
数据类型 零值 最大值 最小值 说明 boolean false true false 一位的信息,只作为一种标志来记录 true/false 情况 byte 0 127(2^7-1) -128(-2^7) 8位、有符号的,以二进制补码表示的整数 short 0 32767(2^15-1) -32768(-2^15) 16 位、有符号的以二进制补码表示的整数 char \u0000 \uffff(65535) \u0000(0) 单一的 16 位 Unicode 字符,可以储存任何字符 int 0 2^31-1 -2^31 32位、有符号的以二进制补码表示的整数 float 0.0f 2^128-1 2^-149 单精度、32位、符合IEEE 754标准的浮点数 long 0L -2^63 2^63-1 64 位、有符号的以二进制补码表示的整数 double 0.0d 2^1024-1 2^-1024 双精度、64 位、符合IEEE 754标准的浮点数
4. 解析
①解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程
②符号引用是以一组符号来描述所引用的目标,直接引用则是直接指向目标的指针、相对偏移量或是一个能直接定位到目标的句柄
③解析包括:类或接口的解析,字段解析,类方法解析,接口方法解析
④同一个符号引用在不同VM翻译出的直接引用一般不同,若有了直接引用,则引用目标必已存在内存中
5. 初始化
”初始化阶段“是根据程序员通过程序制定的主观计划去初始化类变量和其他资源的过程,即执行类构造器< client >()的过程。< client >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的
- 编译器收集的顺序和类中出现的顺序一致,因此静态语句块中只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,在静态语句块中可以赋值,但不能访问
static { i = 0; //到这句代码,可以正常编译通过 System.out.println(i); //报错 "非法向前引用" } static int i = 1;
虚拟机保证在子类的< client >()方法执行之前,父类的< client >()方法已经执行完毕,因此***父类中定义的静态语句块要优先于子类的变量赋值操作***
clinit方法不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不生成该方法
虚拟机会保证一个类的< client >()方法在多线程环境中被正确的加锁、同步
接口中不能使用静态语句块,但仍有变量初始化的赋值操作,因此也生成< client >()方法,但不需先执行父接口的< client >(),只有用到父接口中定义的变量时,才初始化父接口。另外,接口的实现类在初始化时也不执行接口的< client >()方法
同一类加载器下,一个类型只会初始化一次
类加载器
**“通过一个类的全限定名称来获取描述此类的二进制字节流”**就是类的“加载”阶段。
将这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块即“类加载器”
- 一个类是由它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。两个相同限定名的类,经过不同的类加载器加载也是代表两个不同的类,而且Class对象的equals(),isAssignableFrom(),isInstance()等方法返回的结果也会不一致,使用instanceof**关键字做对象所属关系判定的结果也会不同。
类加载器的种类
1. 从虚拟机的角度
①启动类加载器(Bootstrap Classloader):C++实现的,它是虚拟机的一部分;
②所有其他的类加载器:由Java实现的,独立于虚拟机外部,全部继承自抽象类java.lang.ClassLoader,且用户可以自定义
2. 从开发人员的角度
① 启动类加载器:负责加载JAVA_HOME/lib目录中的被虚拟机识别的类,无法被Java直接引用,用户在编写自定义的类加载器的时候,如果需要把类加载请求委托给引导类加载器,直接给加载器赋值为null就行
② 扩展类加载器:负责加载JAVA_HOME/lib/ext目录中的类
③ 应用程序类加载器:由AppClassLoader实现,一般称为系统类加载器,负责加载用户的ClassPath上说的指定的类,开发者可以直接使用这个类加载器,也是程序中默认使用的类加载器
④用户自定义的类加载器
- 优先级:启动类加载器 > 扩展类加载器 > 应用程序类加载器 > 自定义类加载器
双亲委派模型
-
要求除了顶层启动类加载器外,其余的类加载器都应当有自己的父类加载器,父子关系一般不以继承方式来实现,而是以组合关系来复用父加载器的代码
-
过程: 如果一个类加载器收到了类加载的请求,它首先不会尝试加载这个类,而是把请求往上委派给父类加载器去完成,每一个层次的类加载器都是如此,直到当父加载器反馈无法加载的时候(它的搜索范围中没有找到所需的类),子加载器才会尝试加载
-
双亲委派机制的优点:Java类随着它的类加载器一起具备了一种优先级关系,对于那些公用的类来说,都可以委托优先级高的类统一加载(比如可以保证Object类在程序的各种类加载器环境中都是同一个类)**