因为热爱所以坚持,因为热爱所以等待。熬过漫长无戏可演的日子,终于换来了人生的春天,共勉!!!
JVM类加载子系统
- 类加载子系统结构图
1.类加载分几步?
按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载
2.类的加载(Loading)
- ①.类的加载指的是将类的.class文件中的二进制数据读取到内存中, 存放在运行时数据区的方法区中,并创建一个大的Java.lang.Class对象,用来封装方法区内的数据结构 . 在加载类时,Java虚拟机必须完成以下3件事情:
- 1.通过一个类的全限定名获取定义此类的二进制字节流
- 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 3.在内存中生成一个代表这个类的java. lang.Class对象,作为方法区这个类
的各种数据的访问入口
所谓装载(加载),简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象
-
②. 对于类的二进制数据流,虚拟机可以通过多种途径产生或获得(只要所读取的字节码符合JVM规范即可), 加载的方式有如下:
1.从本地系统中直接加载
2.通过网络获取,典型场景: Web Applet
3.从zip压缩包中读取,成为日后jar、war格式的基础
4.运行时计算生成,使用最多的是:动态代理技术
5.由其他文件生成,典型场景: JSP应用
6.从专有数据库中提取.class文件,比较少见
7.从加密文件中获取,典型的防Class文件被反编译的保护措施
3.链接(Linking)
-
①.验证:
确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性
1.目的是确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
2.主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证
3.格式检查: 是否以魔术oxCAFEBABE开头,主版本和副版本是否在当前Java虚拟机的支持范围内,数据中每一项是否都拥有正确的长度等
-
②.准备:(静态变量,不能是常量)
1.为类静态变量分配内存并且设置该类变量的默认初始化值
2.这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式赋值
3.这里不会为实例变量分配初始化, 类变量会分配在方法区中, 而实例变量会随着对象一起分配到Java堆中
4.注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false
- ③. 解析: 将常量池中的符号引号转换为直接引用的过程(简言之,将类、接口、字段和方法的符号引用转为直接引用)
- 虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行起来时,需要从常量池中获得对应的符号引用,再在类加载过程中(初始化阶段)将其替换直接引用,并翻译到具体的内存地址中
- 符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
- 不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中, 加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符
4.初始化(Initialization)
-
①.为类变量赋予正确的初始化值
-
②. 初始化阶段就是执行类构造器方法< clinit >()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码快中的语句合并而来
public class ClassInitTest {
private static int num=1; //类变量的赋值动作
//静态代码快中的语句
static{
num=2;
number=20;
System.out.println(num);
//System.out.println(number); 报错:非法的前向引用
}
//Linking之prepare: number=0 -->initial:20-->10
private static int number=10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);
System.out.println(ClassInitTest.number);
}
}
- ③.若该类具有父类,Jvm会保证子类的< clinit >() 执行前,父类的< clinit >() 已经执行完成。
public class ClinitTest1 {
static class Father{
public static int A=1;
static{
A=2;
}
}
static class Son extends Father{
public static int B=A;
}
public static void main(String[] args) {
//这个输出2,则说明父类已经全部加载完毕
System.out.println(Son.B);
}
}
-
④. Java编译器并不会为所有的类都产生< clinit >()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含()方法?
1.一个类中并没有声明任何的类变量,也没有静态代码块时2.一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
3.一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
(如果这个static final 不是通过方法或者构造器,则在链接阶段) -
⑤.< clinit >() 不同于类的构造方法
-
⑥clinit()的调用会死锁吗?
1.虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit >()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕2.正是因为方法< clinit >()带锁线程安全的,因此,如果在一个类的< clinit >()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息
package com.xiaozhi;
/**
* @author TANGZHI
* @create 2021-05-25
*/
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.xiaozhi.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.xiaozhi.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public class StaticDeadLockMain extends Thread {
private char flag;
public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}
@Override
public void run() {
try {
Class.forName("com.xiaozhi.Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}
public static void main(String[] args) throws InterruptedException {
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}
5.主动引用(触发在初始化阶段的Clinit方法)
①. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
②. 访问某个类或接口的静态变量,或者对该静态变量赋值
③. 调用类的静态方法
④. 反射(比如:Class.forName(“com.xiaozhi.Test”))
⑤. 初始化一个子类(当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)
⑥. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
⑦. JDK7开始提供的动态语言支持
(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
⑧. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
6.被动引用
①. 除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。意味着没有< clinit >()的调用。
②. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化
③. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
当通过子类引用父类的静态变量,不会导致子类初始化
④. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了
⑤. 通过数组定义类引用,不会触发此类的初始化
# 这里不会进行初始化,因为相当于parent只开辟了空间,没赋值
Parent[]parent=new Parent[10];
7.卸载(Unloading)
①.方法区的垃圾回收
- 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
- HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收
- 判定一个常量是否"废弃”还是相对简单,而要判定一个类型是否属于"不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件
②.类的卸载
- 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
- 系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小
- 开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。
下一篇 03JVM类加载子系统(下) – 类加载器, 双亲委派机制,沙箱安全机制详解
参考视频 : 尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)
参考书籍 : 深入理解Java虚拟机