1 类加载机制
1.1 类的生命周期
使用和卸载不属于载加载的过程
1.2 加载
- 类的加载:
1. 通过类的全限定名获取该类的二进制字节流。
2. 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。
3. 在内存中创建一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
- 数组的加载:
数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:
1. 如果数组的组件类型是引用类型,那就递归采用类加载加载。
2. 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为启动类加载器关联。
3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
4. 如果数组的组件类型不是引用类型(例如int[]),Java虚拟机将会把数组C标记为与引导类加载器关联
- 二进制字节流的来源
1. 从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础
2. 从网络中获取,这种场景最典型的应用就是applet
3. 运行时计算生成,这种场景使用得最多的就是动态代理基础
4. 由其它文件生成,典型场景就是JSP应用,即由JSP文件生成赌赢的Class类
5. 从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
1.3 验证
加载阶段与连接阶段的部分内容交叉进行,但这两个阶段的开始仍然保持先后顺序。验证主要有如下4方面的验证。
1. 文件格式验证
2. 元数据验证
3. 字节码验证
4. 符号引用验证
1.4 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区汇总进行分配。
- 初始化时各类型数据值对应关系如下:
官网
* For type byte, the default value is zero, that is, the value of (byte)0.
* For type short, the default value is zero, that is, the value of (short)0.
* For type int, the default value is zero, that is, 0.
* For type long, the default value is zero, that is, 0L.
* For type float, the default value is positive zero, that is, 0.0f.
* For type double, the default value is positive zero, that is, 0.0d.
* For type char, the default value is the null character, that is, '\u0000'.
* For type boolean, the default value is false.
* For all reference types (§4.3), the default value is null.
- 存在特殊情况:
/**
* 准备阶段过后的初始值为 0 而不是 123,这时候尚未开始执行任何 Java 方法
*/
public static int value = 123;
/**
* 同时使用 final 、static 来修饰的变量(常量),并且这个变量的数据类型是基本类型或者 String 类型,就生成 ConstantValue 属性来进行初始化。
* 没有 final 修饰或者并非基本类型及 String 类型,则选择在 <clinit> 方法中进行初始化。
* 准备阶段虚拟机会根据 ConstantValue 的设置将 value 赋值为 123
*/
public static final int value = 123;
1.5 解析
虚拟机将常量池内的符号引用替换为直接引用。会把该类所引用的其他类全部加载进来( 引用方式:继承、实现接口、域变量、方法定义、方法中定义的本地变量)
符号引用:一个 java 文件会编译成一个class文件。在编译时,java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
直接引用:直接指向目标的指针(指向方法区,Class 对象)、指向相对偏移量(指向堆区,Class 实例对象)或指向能间接定位到目标的句柄。
1.6 初始化
类加载过程的最后一步,是执行类构造器 () 方法的过程。
1.6.1 初始化触发(主动引用)
1.在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发初始化。
2.对类进行反射调用时,如果类还没有初始化,则需要先触发初始化。
3.初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
4.虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
5.当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化。
1.6.2 不触发初始化过程(被动引用)
- 通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
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!
}
}
- 通过数组定义来引用类,不会触发此类的初始化。
class SuperClass2 {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class NotInitialization2 {
public static void main(String[] args) {
SuperClass2[] superClasses = new SuperClass2[10];
}
}
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
编译通过之后,常量存储到 NotInitialization 类的常量池中,NotInitialization 的 Class 文件中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就没有任何联系了。
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLO_BINGO = "Hello Bingo";
}
public class NotInitialization3 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO_BINGO);
}
}
1.6.3 加载顺序
- () 方法是由编译器自动收集类中的所有类变量的赋值动作语句和静态块(static {})中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。
static {
i = 0; // 给后面的变量赋值,可以正常编译通过
System.out.println(i); // 使用后面的变量,编译器会提示“非法向前引用”
}
static int i = 1;
- 虚拟机会保证在子类的 () 方法执行之前,父类的 () 方法已经执行完毕。由于父类的 () 方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
static class Parent {
static {
A = 2;
}
public static int A = 1;
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出 1
}
- 类属性的加载顺序。
public class JvmTest {
public static JvmTest jt = new JvmTest();
public static int a;
public static int b = 0;
static {
a++;
b++;
}
public JvmTest() {
a++;
b++;
}
public static void main(String[] args) {
/**
* 准备阶段:为 jt、a、b 分配内存并赋初始值 jt=null、a=0、b=0
* 解析阶段:将 jt 指向内存中的地址
* 初始化:jt 代码位置在最前面,这时候 a=1、b=1
* a 没有默认值,不执行,a还是1,b 有默认值,b赋值为0
* 静态块过后,a=2、b=1
*/
System.out.println(a); // 输出 2
System.out.println(b); // 输出 1
}
}
1.7 使用
程序调用,相关工作在Java运行时数据区域。
1.8 卸载
在类使用完之后,如果满足下面的情况,类就会被卸载。
* 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
* 加载该类的ClassLoader已经被回收。
* 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
2 类加载器
Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等
2.1 加载器分类
2.1.1 启动类加载器(Bootstrap ClassLoader)
它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
查看启动类加载器的加载路径:
public class ClassLoaderTest {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
2.1.2 拓展类加载器(Extension ClassLoader)
它负责加载JRE的扩展目录,lib/ext或者由-Djava.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
2.1.3 应用类加载器(Application ClassLoader)
被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
2.2 加载器逻辑步骤
类加载器加载Class大致要经过如下8个步骤:
1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
6. 从文件中载入Class,成功后跳至第8步。
7. 抛出ClassNotFountException异常。
8. 返回对应的java.lang.Class对象。
2.3 类加载方式
2.3.1 全盘负责
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
2.3.2 双亲委派
先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
2.3.2.1 双亲委派机制图解
2.3.2.2 双亲委派机制的优劣势
- 优点:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
- 缺点:加载会更耗时,可通过继承方式破坏双亲委派模式。
2.3.3 缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
3 引用
- 《深入理解Java虚拟机 JVM高级特性与最佳实践》-周志明著
- https://www.cnblogs.com/jhxxb/p/10900405.html
- https://blog.csdn.net/m0_38075425/article/details/81627349