1 起源
- 起源:《Java 虚拟机 JVM 故障诊断与性能优化》——葛一鸣
- 章节:第十章
本文是第十章的一些笔记整顿。
2 概述
本文次要讲述了类加载器以及类加载的具体流程。
3 类加载流程
类加载的流程能够简略分为三步:
- 加载
- 连贯
- 初始化
而其中的连贯又能够细分为三步:
- 验证
- 筹备
- 解析
上面会别离对各个流程进行介绍。
3.1 类加载条件
在理解类接在流程之前,先来看一下触发类加载的条件。
JVM
不会无条件加载类,只有在一个类或接口在首次应用的时候,必须进行初始化。这里的应用是指被动应用,被动应用包含如下状况:
- 创立一个类的实例的时候:比方应用
new
创立,或者应用反射、克隆、反序列化 - 调用类的静态方法的时候:比方应用
invokestatic
指令 - 应用类或接口的动态字段:比方应用
getstatic
/putstatic
指令 - 应用
java.lang.reflect
中的反射类办法时 - 初始化子类时,要求先初始化父类
- 含有
main()
办法的类
除了以上状况外,其余状况属于被动应用,不会引起类的初始化。
比方上面的例子:
public class Main {public static void main(String[] args){System.out.println(Child.v);
}
}
class Parent{
static{System.out.println("Parent init");
}
public static int v = 100;
}
class Child extends Parent{
static {System.out.println("Child init");
}
}
输入如下:
Parent init
100
而加上类加载参数 -XX:+TraceClassLoading
后,能够看到 Child
的确被加载了:
[0.068s][info][class,load] com.company.Main
[0.069s][info][class,load] com.company.Parent
[0.069s][info][class,load] com.company.Child
Parent init
100
然而并没有进行初始化。另外一个例子是对于 final
的,代码如下:
public class Main {public static void main(String[] args){System.out.println(Test.STR);
}
}
class Test{
static{System.out.println("Test init");
}
public static final String STR = "Hello";
}
输入如下:
[0.066s][info][class,load] com.company.Main
Hello
Test
类基本没有被加载,因为 final
被做了优化,编译后的 Main.class
中,并没有援用 Test
类:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
在字节码偏移 3 的地位,通过 ldc
将常量池第 4 项入栈,此时在字节码文件中常量池第 4 项为:
#3 = Class #24 // com/company/Test
#4 = String #25 // Hello
#5 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
因而并没有对 Test
类进行加载,只是间接援用常量池中的常量,因而输入没有 Test
的加载日志。
3.2 加载
类加载的时候,JVM
必须实现以下操作:
- 通过类的全名获取二进制数据流
- 解析类的二进制数据流为办法区内的数据结构
- 创立
java.lang.Class
类的实例,示意该类型
第一步获取二进制数据流,路径有很多,包含:
- 字节码文件
JAR
/ZIP
压缩包- 从网络加载
等等,获取到二进制数据流后,JVM
进行解决并转化为一个 java.lang.Class
实例。
3.3 验证
验证的操作是确保加载的字节码是非法、正当并且标准的。步骤简略如下:
- 格局查看:判断二进制数据是否合乎格局要求和标准,比方是否以魔数结尾,主版本号和小版本号是否在以后
JVM
反对范畴内等等 - 语义查看:比方是否所有类都有父类存在,一些被定义为
final
的办法或类是否被重载了或者继承了,是否存在不兼容办法等等 - 字节码验证:会试图通过对字节码流的剖析,判断字节码是否能够正确被执行,比方是否会跳转到一条不存在的指令,函数调用是否传递了正确的参数等等,然而却无奈 100% 判断一段字节码是否能够被平安执行,只是尽可能查看出能够预知的显著问题。如果无奈通过查看,则不会加载这个类,如果通过了查看,也不能阐明这个类齐全没有问题
- 符号援用验证:查看类或办法是否的确存在,并且确定以后类有没有权限拜访这些数据,比方无奈找到一个类就抛出
NoClassDefFoundError
,无奈找到办法就抛出NoSuchMethodError
3.4 筹备
类通过验证后,就会进入筹备阶段,在这个阶段,JVM
为会类调配相应的内存空间,并设置初始值,比方:
int
初始化为0
long
初始化为0L
double
初始化为0f
- 援用初始化为
null
如果存在常量字段,那么这个阶段也会为常量赋值。
3.5 解析
解析就是将类、接口、字段和办法的符号援用转为间接援用。符号援用就是一些字面量援用,和 JVM
的内存数据结构和内存布局无关,因为在字节码文件中,通过常量池进行了大量的符号援用,这个阶段就是将这些援用转为间接援用,失去类、字段、办法在内存中的指针或间接偏移量。
另外,因为字符串有着很重要的作用,JVM
对 String
进行了特地的解决,间接应用字符串常量时,就会在类中呈现 CONSTANT_String
,并且会援用一个CONSTANT_UTF8
常量项。JVM
运行时,外部的常量池中会保护一张字符串扣留表(intern
),会保留其中呈现过的所有字符串常量,并且没有反复项。应用 String.intern()
能够取得一个字符串在扣留表的援用,比方上面代码:
public static void main(String[] args){String a = 1 + String.valueOf(2) + 3;
String b = "123";
System.out.println(a.equals(b));
System.out.println(a == b);
System.out.println(a.intern() == b);
}
输入:
true
false
true
这里 b
就是常量自身,因而 a.intern()
返回在扣留表的援用后就是 b
自身,比拟后果为真。
3.6 初始化
初始化阶段会执行类的初始化办法 <clint>
,<clint>
是由编译期生成的,由动态成员的赋值语句以及 static
语句独特产生。
另外,加载一个类的时候,JVM
总是会试图加载该类的父类,因而父类的 <clint>
办法总是在子类的 <clint>
办法之前被调用。另一方面,须要留神的是 <clint>
会确保在多线程环境下的安全性,也就是多个线程同时初始化同一个类时,只有一个线程能够进入 <clint>
办法,换句话说,在多线程下可能会呈现死锁,比方上面代码:
package com.company;
import java.util.concurrent.TimeUnit;
public class Main extends Thread{
private char flag;
public Main(char flag){this.flag = flag;}
public static void main(String[] args){Main a = new Main('A');
a.start();
Main b = new Main('B');
b.start();}
@Override
public void run() {
try{Class.forName("com.company.Static"+flag);
}catch (ClassNotFoundException e){e.printStackTrace();
}
}
}
class StaticA{
static {
try {TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){e.printStackTrace();
}
try{Class.forName("com.company.StaticB");
}catch (ClassNotFoundException e){e.printStackTrace();
}
System.out.println("StaticA init ok");
}
}
class StaticB{
static {
try {TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){e.printStackTrace();
}
try{Class.forName("com.company.StaticA");
}catch (ClassNotFoundException e){e.printStackTrace();
}
System.out.println("StaticB init ok");
}
}
在加载 StaticA
的时候尝试加载 StaticB
,然而因为StaticB
曾经被加载中,因而加载 StaticA
的线程会阻塞在 Class.forName("com.company.StaticB")
处,同理加载 StaticB
的线程会阻塞在 Class.forName("com.company.StaticA")
处,这样就呈现死锁了。
4 ClassLoader
4.1 ClassLoader
简介
ClassLoader
是类加载的外围组件,所有的 Class
都是由 ClassLoader
加载的,ClassLoader
通过各种各样的形式将 Class
信息的二进制数据流读入零碎,而后交给 JVM
进行连贯、初始化等操作。因而 ClassLoader
负责类的加载流程,无奈通过 ClassLoader
扭转类的连贯和初始化行为。
ClassLoader
是一个抽象类,提供了一些重要接口定义加载流程和加载形式,次要办法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
:给定一个类名,加载一个类,返回这个类的Class
实例,找不到抛出异样protected final Class<?> defineClass(byte[] b, int off, int len)
:依据给定字节流定义一个类,off
和len
示意在字节数组中的偏移和长度,这是一个protected
办法,在自定义子类中能力应用protected Class<?> findClass(String name) throws ClassNotFoundException
:查找一个类,会在loadClass
中被调用,用于自定义查找类的逻辑protected Class<?> findLoadedClass(String name)
:寻找一个曾经加载的类
4.2 类加载器分类
在规范的 Java
程序中,JVM
会创立 3 类加载器为整个应用程序服务,别离是:
- 启动类加载器:
Bootstrap ClassLoader
- 扩大类加载器:
Extension ClassLoader
- 利用类加载器(也叫零碎类加载器):
App ClassLoader
另外,在程序中还能够定义本人的类加载器,从总体看,层次结构如下:
一般来说各个加载器负责的范畴如下:
- 启动类加载器:负责加载零碎的外围类,比方
rt.jar
包中的类 - 扩大类加载器:负责加载
lib/ext/*.jar
下的类 - 利用类加载器:负责加载用户程序的类
- 自定义加载器:加载一些非凡路径的类,个别是用户程序的类
4.3 双亲委派
默认状况下,类加载应用双亲委派加载的模式,具体来说,就是类在加载的时候,会判断以后类是否曾经被加载,如果曾经被加载,那么间接返回已加载的类,如果没有,会先申请双亲加载,双亲也是依照一样的流程先判断是否已加载,如果没有在此委托双亲加载,如果双亲加载失败,则会本人加载。
在上图中,利用类加载器的双亲为扩大类加载器,扩大类加载器的双亲为启动类加载器,当零碎须要加载一个类的时候,会先从底层类加载器开始进行判断,当须要加载的时候会从顶层开始加载,顺次向下尝试直到加载胜利。
在所有加载器中,启动类加载器是最特地的,并不是应用 Java
语言实现,在 Java
中没有对象与之绝对应,系统核心类就是由启动类加载器进行加载的。换句话说,如果尝试在程序中获取启动类加载器,失去的值是null
:
System.out.println(String.class.getClassLoader() == null);
输入后果为真。