猝死JVM--2_类加载机制详解
努力努力再努力xLg
前言
虚拟机吧描述类得数据从Class
文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以呗虚拟机直接使用得Java
类型,这就是虚拟机得类加载机制;
--引自《深入理解Java虚拟机》
与许多编程语言不同的是,在Java
语言里面,类型的加载、连接和初始化过程都是在程序运行期间
完成的。
那么为什么Java
会采取着这样的机制呢?带来了那些好处,又有什么弊端呢?
类加载
为什么Java
会采取这样的机制,让程序在运行期间完成类型的加载、连接和初始化工作呢?
好处
- 基于这种机制,
Java
天生可以动态扩展的语言特性,就是依赖运行期间动态加载和动态连接这个特点来实现的。- 例如:在编写一个接口的应用程序时,
Java
可以在其运行时,再指定具体实际的实现类,用户可以通过Java
预定义的和自定义类加载器,让一个本地的应用程序可以再运行时从网络或者其他地方加载一个二进制流作为程序代码的一部分(这里的二进制流为JVM编译后得到的Class文件流)。这样给Java
提供了更大的灵活性,增加了更多的可能性。 - 上述情况中,做典型的例子就是
WEB
开发中JSP
的编译过程,其核心原理:JSP
在JVM
运行时,会被加载到内存,转换成类似于Sevlet
的一个可执行的Class
二进制字节流。一般情况下名称为:index_jsp.class
(这里设计到一个面试问题:JSP于sevlet的区别,JSP何时生成编译过程,并且生成的文件存在什么路径下。
) -
Applet
、相对复杂的OSGi
技术都使用了Java
语言运行期类加载的特性。
- 例如:在编写一个接口的应用程序时,
坏处
这种预加载的策略会令类加载时稍微增加一些性能开销。(但可以为Java
应用提高高度的灵活--值了!!)
类的加载时机
类的生命周期
每一个类被加载到内存,到被卸载出内存为止,这是它的整个生命周期
而Java
类的整个生命周期包括以下七个阶段:
上述的加载、验证、准备、初始化、和卸载这5个阶段的顺序时确定的。类的加载顺序必须按照这5个顺序按部就班的开始。(这里特定的时开始,还不是进行或者是完成,说明这一点,因为是在这些阶段通常是互相交叉混合进行的,通常是在一个阶段执行的过程中激活或者调用另外一个阶段)。
类的加载、连接与初始化
初始化的时机
-
Java
程序对类的使用方式可分为两种- 主动引用
- 所谓主动引用:虚拟机规范中使用了一个很强烈的限定语:“有且只有”,在
JVM
指定的情况下才会触发初始化。(所有情况会在下面列举出来)。
- 所谓主动引用:虚拟机规范中使用了一个很强烈的限定语:“有且只有”,在
- 主动引用
- 被动引用
- 其他非主动引用的,都为被动引用,都不会触发初始化。
- 被动引用
- 所有的
Java
虚拟机实现必须在每个类或者接口被Java
程序“首次主动使用
”时才会初始化他们。
主动引用(七种)
这里列举的其中情况都是属于JVM
规范中。“有且只有”。
- 创建类的实例;(使用
New
关键字实例化对象的时候,“字节马指令对应的也是new
”) - 访问某个类或者接口的静态变量时,或者对该变量进行赋值;(其实就是对静态变量进行取值或者赋值时,(这里需要注意的是,被
final
修饰、已在编译期把结果放入常量池的静态字段除外)) - 调用类的静态方法(对应的字节码指令:getstatic(读取)、putstatic(赋值)、invokestatic(应用))
- 反射:使用
java.lang.reflect
包的方法对类进行反射调用的时候。如果类没有进行过初始化,则需要先触发其初始化。 - 初始化一个类的子类:(如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)
-
Java
虚拟机启动时被标明为启动类时。(例如main()
函数、Java Test
,虚拟机会先初始化这个主类) -
JDK1.7
开始提供的动态语言支持时,java。lang。invoke。MethodHandle
实例的解析结果为REF_getStatic
,REF_putStatic
,REF_invokeStatic
句柄对应的类没有初始化,则需要先触发初始化。
其他情况均为被动引用
主动引用与被动引用的区别
package com.lg.jvm.classloader;
/**
* @author by Mr. Li
* @date 2020/1/4 12:43
*/
// 主动使用: 启动类
public class MyTest1 {
public static void main(String[] args) throws Exception {
Singleton singleton = new Singleton();
Class.forName("com.lg.jvm.classloader.Singleton1");
new Sun();
System.out.println(MyChild1.str);
}
static {
System.out.println("Main static block init...");
}
}
class MyParent1 {
public static String str = "Hello World";
static {
System.out.println("MyParent1 static block init....");
}
}
class MyChild1 extends MyParent1 {
public static String str = "MyChild";
static {
System.out.println("MyChild1 static block init....");
}
}
class Singleton {
public static String str = "Singleton";
}
class Singleton1 {
public static String str = "Singleton";
}
class Father {
public static String str = "father";
}
class Sun extends Father {
public static String str = "sun";
}
执行结果:这里需要给JVM
添加参数-XX:+TraceClassLoading
,方可看到详细信息
可以看出上述7种主动引用都会触发类加载。
被动引用
通过子类引用父类的静态字段,不会导致子类初始化,但是该子类依然会被加载。
package com.lg.jvm.classloader;
/**
* 通过子类引用父类的静态字段,不会导致子类初始化,但是该子类依然会被加载。
*
* @author by Mr. Li
* @date 2020/1/4 22:55
*/
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyChild2.str);
}
}
class MyParent2 {
public static String str = "MyParent2";
static {
System.out.println("MyParent2 static block");
}
}
class MyChild2 extends MyParent2 {
static {
System.out.println("MyChild2 static block");
}
}
执行结果:
[Loaded java.lang.Void from D:\develop\java\jdk_1.8.172\jre\lib\rt.jar]
[Loaded com.lg.jvm.classloader.MyParent2 from file:/E:/idea_workspace/java_legendary/jvm_lecture/build/classes/java/main/]
[Loaded com.lg.jvm.classloader.MyChild2 from file:/E:/idea_workspace/java_legendary/jvm_lecture/build/classes/java/main/]
MyParent2 static block
MyParent2
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类。因此不会触发定义常量的类的初始化。
package com.lg.jvm.classloader;
/**
* 常量在编译阶段会存入调用类的常量池中,
* 本质上并没有直接引用到定义常量的类。
* 因此不会触发定义常量的类的初始化。
*
* @author by Mr. Li
* @date 2020/1/4 22:55
*/
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3 {
public static final String str = "MyParent3";
static {
System.out.println("MyParent3 static block");
}
}
class MyChild3 extends MyParent3 {
static {
System.out.println("MyChild3 static block");
}
}
[图片上传失败...(image-e257e8-1578153420455)]
这里可以讲MyParent3
的Class
文件删除,一样可以运行成功。因为此时的常量在编译时通过常量传播优化,已将将此常量的值存入到调用类的常量池中了。
反编译结果:
1578151004897.png这里ldc指令是说明:将常量字符串MyParent2 推送到栈顶,
推送到栈顶,表明马上需要用到该常量。
画外音:其实这里的推送栈顶的指令都是来自于JDK最底层的Java类,都是rt.jar 下的。
1578151196200.png
1578151726073.png
public static final int a = 1; // iconst_* : 将int类型的为-1 到5 之间的常量推送到栈顶(-1 为iconst_m1)
public static final int b = -1;
public static final short c = 127;//bipush:将单个字节(-128 -- 127)的常量推送到栈顶
public static final int d = 6;
// ldc 表示:将int,float或是String类型的常量推送到栈顶
数组实例也属于被动引用。
因为数组实际上是由JVM
自动生成的,类型为动态生成的,其父类就是Object
类
// todo