温故而知新-JVM类加载机制
从源码到运行,java都发生了什么
java是一门编译解释性型语言,如何理解这句话?
一个helloword.java是无法直接运行的。java需要通过编译器编译成helloword.class这样的字节码文件之后,jvm再去读取解释执行这些字节码文件。(针对不同的平台,jvm解释出机器指令不同,从而实现跨平台)
类是一开始就加载到jvm虚拟机中的吗
类,都是在第一次被用到时,动态加载到JVM的。这句话有两层含义:
- Java程序在运行时并不一定被完整加载,只有当发现该类还没有加载时,才去本地或远程查找类的.class文件并验证和加载;
- 当程序创建了第一个对类的静态成员的引用(如类的静态变量、静态方法、构造方法——构造方法也是静态的)时,才会加载该类。
如何看懂一个字节码文件
javap -c file.class
就可以看到class反编译后的在jvm虚拟机中运行的指令。具体指令操作,请查询 jdk1.8官网文档4.10. Verification of class Files
jvm是如何把class文件加载到jvm虚拟机中的
类从被加载到虚拟机内存中开始到卸载出内存为止的整个生命周期
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。以下陈述的内容都已HotSpot为基准。特别需要注意的是,类的加载过程必须按照这种顺序按部就班地“开始”,而不是按部就班的“进行”或“完成”,因为这些阶段通常都是相互交叉地混合式进行的,也就是说通常会在一个阶段执行的过程中调用或激活另外一个阶段。
加载 Loading
- 通过类的全限定名称(包名+类名)取得类的二进制字节流。
- 由类加载器加载至运行时数据区。
- 把二进制字节流中静态存储结构转化为jvm的方法区中。
- 在内存中生成代表这个类的java.lang.Class对象,并存放在jvm的堆中。
ClassLoad 类加载器
- BootstrapClassLoader 启动类加载器。主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或 -Xbootclasspath 参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)
- ExtensionClassLoader 扩展类加载器。是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载 <JAVA_HOME>/lib/ext 目录下的jar包或者由系统变量 -Djava.ext.dir 指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
- SystemClassLoader 系统类加载器。也称应用程序加载器是指 Sun公司实现的 sun.misc.Launcher$AppClassLoader 。它负责加载系统类路径 java -classpath或 -D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
- 用户自定义加载器 。通过继承java.lang.ClassLoader 实现。
如何保证一个类只会被类加载器加载一次
jdk 1.8 通过双亲委派机制实现。双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。
其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
如何判断两个class对象是否为同一个类对象
- 类的完整类名(包名+类名)相同
- 加载这个类的类加载器必须相同
连接 Linking
连接分为验证,准备,解析三个过程
- 验证 Verification,确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备 Preparation,为类变量(静态成员变量)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。
- 解析 Resolution,解析是根据运行时常量池中的符号引用动态确定具体值的过程将常量池内的动态引用改为直接引用)。会把该类所引用的其他类全部加载进来( 引用方式:继承、实现接口、域变量、方法定义、方法中定义的本地变量)
初始化 Initializing
类或接口的初始化包括执行其类或接口初始化方法。
什么时候会触发类的初始化
触发类的初始化有以下几种情况
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
- 使用new关键字实例化对象的时候;
- 读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
- 调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
为什么接口不能定义成员变量,而只能定义 final static 变量。
1.接口是不可实例化,它的所有元素都不必是实例(对象)层面的。static 满足了这一点。
2. 如果接口的变量能被修改,那么一旦一个子类实现了这个接口,并修改了接口中的非 final 变量,而该子类的子类再次修改这个非 final 的变量后,造成的结果就是虽然实现了相同的接口,但接口中的变量值是不一样的。
类初始化过程的线程是否安全
java规定对于每个类或接口C,都有一个唯一的初始化锁 LC。从C到LC Java 的映射由Java虚拟机实现决定。这个初始化锁,保证了类初始化是线程安全的。(可以通过类初始化机制实现)
For each class or interface C, there is a unique initialization lock LC.
The mapping from C to LC is left to the discretion of the Java Virtual Machine implementation.
For example, LC could be the Class object for C, or the monitor associated with that Class object.