深入理解 Java 虚拟机(二)
这一篇主要讲 JVM 的类加载机制,本来很久之前就写了,但是这几天又重新学习了一遍,纠正了之前很多错误的观点,然后又补充了很多东西
类加载的过程
前言:
在 Java 语言中,类型的加载,连接和初始化过程都是在运行期间完成的,这与那些在编译期间需要进行链接工作的语言(C/C++)不同,这样毫无疑问会增加类加载的性能开销,但是会为 Java 提供高度的灵活性,Java 天生可以动态扩展的就是依赖于运行时期动态加载和动态链接这个特点实现的,比如一个接口,完全可以在运行时期动态的指定其具体的实现类。又或者用户可以通过类加载器让一个本地的引用运行时从其他地方(网络等)加载一个二进制的流作为程序代码的一部分。
①加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象
类加载的最终产物
类加载的最终产物是位于堆中的
Class
对象
《深入理解 Java 虚拟机》里面介绍的 JVM 内存结构 (p39)
堆(Heap):最大的一块区域,线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
The Java Virtual Machine has a heap that is
shared
amongall
Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.The heap is created on virtual machine
start-up
. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector(GC));摘自 官方文档
方法区(Method Area):又称为非堆
,线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。
The Java Virtual Machine has a method area that is
shared
among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
运行时常量池 (run-time constant pool):
是方法区的一部分,用于存放编译期生成的各种字面量"123",“LGW” 等字符串常量池,和符号引用。运行时常量池具有动态性,并非只有 Class 文件中的内容才能进入运行时常量池,运行期间也能将新的常量放入池中。如 String.intern() 方法。
方法栈(JVM Stack):
线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针
。
本地方法栈(Native Method Stack):
线程私有。为虚拟机使用到的 Native 方法服务。如 Java 使用 c 或者 c++编写的接口服务时,代码在此区运行。
程序计数器(Program Counter Register):
线程私有,它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
加载类的方式
- 本地磁盘 classpath
- 内存中加载 ,动态代理?RPC?
- 通过网络加载。class
- 从 zip,jar 中加载
- 数据库中提取。class
- 动态编译
②连接:
验证
文件格式验证
-
是否以魔数
0xCAFEBABE
(咖啡宝贝)开头 -
主、次版本号是否在当前虚拟机处理范围之内
-
常量池中的常量中是否有不被支持的常量类型(检查常量 tag 标志)
-
…..
元数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求
字节码验证
主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
由于数据流验证的高复杂性,虚拟机设计团队为了避免过多的时间消耗在字节码验证阶段,在 JDK 1.6 之后的 Javac 编译器和 Java 虚拟机中进行了一项优化,给方法体的 Code 属性的属性表中增加了一项名为" StackMapTable" 的属性,只需要检查 StackMapTable 属性中的记录是否合法皆可以了
符号引用验证
符号引用的校验发生在虚拟机将符号引用
转化为直接引用
的时候,这个转化动作将在连接的第三阶段—-解析阶段中发生,通常需要校验以下内容:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述以及简单名称说描述的方法和字段。
- 符号引用中的类,字段,方法的访问性是否可以被当前类访问
- …….
如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
准备
为类的静态变量分配内存,并初始化为默认值
数据类型 | 默认值 |
---|---|
boolean | false |
char | ‘/u0000’ |
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
double | 0.0d |
float | 0.0f |
reference | null |
🎯 当然并不是所有情况下都会初始化为零值,如果字段表的属性中有ConstantValue
,准备阶段就会直接初始化为这个这个ConstantValue
属性的值
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候能无歧义的定位到目标中就行。
- 直接引用: 直接引用可以是直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄。
除 invokedynamic
(动态语言支持) 指令以外,虚拟机实现可以对第一次解析的结果进行缓存。
解析动作主要针对类或接口、字段解析、类方法解析、接口方法解析、方法类型解析、方法句柄解析和调用点限定符 7 类符号引用进行。
关于这个也可以看看 R 大的回答 JVM 符号引用转换直接引用的过程 ( 吹爆我 R 大 😁
③初始化:
初始化阶段就是执行类构造器<clinit>
方法的过程
类构造器<client>
💡<clinit>
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问(比如 print)
💡 和实例构造器<init>
不同,不需要显式的去调用父类的构造器,虚拟机会保证子类的<clinit>
方法执行之前,父类的<clinit>
方法已经执行完毕,因此虚拟机中第一个被执行的<clinit>
一定是 Object 类
💡 <clinit>
方法对于类或者接口来说并不是必须的,如果类中没有静态语句块也没有静态变量的赋值操作,那么编译器可以不为这个类生成<clinit>
方法
💡 对于接口,不能使用 static 块,但是可以有静态变量的赋值操作。子类接口的<clinit>
方法调用并不保证父接口的<clinit>
方法被先调用,只有用到父接口的静态变量的时候,父接口<clinit>
方法才会被调用。接口的实现类在初始化时也一样不会执行接口的<clinit>
方法。
💡 虚拟机会保证一个类的<clinit>
方法在多线程环境中被正确地加锁、同步。如果一个线程的<clinit>
方法调用时间过长,就可能造成多个线程阻塞。Demo 如下
public class ClinitThreadTest {
public static void main(String[] args) {
new Thread(()->new SimpleObj()).start();
}
static class SimpleObj{
private static AtomicBoolean init=new AtomicBoolean(true);
static {
System.out.println(Thread.currentThread().getName()+" i will be init");
while (init.get()){
}
System.out.println("i am finished");
}
}
}
为类的静态变量赋予正确的初始值
下面这个 Demo 很经典,可以帮助我们理解类加载的过程
public class SingleTon {
private static SingleTon ins=new SingleTon();
public static int x=0;
public static int y;
private SingleTon(){
x++;y++;
}
public static SingleTon getIns(){
return ins;
}
public static void main(String[] args) {
SingleTon singleTon=getIns();
System.out.println(singleTon.x);
System.out.println(singleTon.y);
}
}
如果不熟悉类加载的过程可能一眼就觉得应该是(1,1),其实不然,结合上面的加载过程分析
🔺 首先加载连接,然后执行准备工作,在执行完准备阶段工作后三个静态变量都有了默认的初始值,然后进入初始化阶段
🔺 <clinit>
初始化阶段会按顺序从上往下依次赋予正确的初始值,所以先执行了new Singleton()
给ins
赋初始值,会调用它的构造器,然后 x,y 都++变为 1,再然后就会给 x,y 赋予正确的初始值,x 初始值为 0,而 y 没有初始值所以就是(0,1)
Class 对象在哪里?
先说结论,Class 对象和其他普通的 Java 对象一样都是存放在堆中的。
存放在方法区的是类的元数据 (InstanceKlass,包括类的常量池 ( constant pool) ,域 (Field) 信息 ,方法 (Method) 信息 ,除了常量外的所有静态 (static) 变量 等),java.lang.Class 实例
并不负责记录真正的类元数据,而只是对 VM 内部的InstanceKlass
对象的一个包装供 Java 的反射访问用,在《深入理解 Java 虚拟机》一书里面说的存放在方法区中应该是有问题的。
类(静态)变量存放在哪里
从 JDK 1.3 到 JDK 6 的 HotSpot VM,静态变量保存在类的元数据(InstanceKlass)的末尾(永久代)。而从 JDK 7 开始的 HotSpot VM,静态变量则是保存在类的 Java 镜像(java.lang.Class 实例)的末尾,也就是堆中
参考 R 大 知乎回答
Java 程序对类的使用方式
所有的 java 虚拟机实现必须在每个类或接口被 java 程序首次主动使用时才初始化它们
主动使用
- 创建类的实例(new)
- 对某个类或接口的静态变量进行读写(getstatic,putstatic)
- 调用类的静态方法(invokestatic)
- 反射某个类 (Class.forName() 也可以设置不初始化类)
- 初始化子类时会先初始化父类
- 启动类 java HelloWorld 包含 main 函数的类
- Jdk1.7 的动态语言支持
被动使用
除了上面 7 个之外,其它的都是被动使用,不会初始化类,下面的 Demo 有几个很容易出错的的例子
package classloader_study.misc;
public class ClassActiveUse {
static {
System.out.println("main is init");
}
public static void main(String[] args) throws ClassNotFoundException {
//System.out.println(Obj.t);
//Obj.getObj();
//Class.forName("classloader_study.Obj");
//System.out.println(ObjChild.age);
//父类会被初始化 通过子类调用父类的静态变量,子类不会初始化但是会被加载
//1. System.out.println(ObjChild.n);
//不会初始化 定义应用数组也不会初始化类,但是会加载类
//2. Obj [] arrays=new Obj[10];
//不会初始化 常量会在编译期间放到常量池中不会初始化类也不会加载,子类加载也一样
//3. System.out.println(Obj.t);
//Obj 会被初始化 final 修饰的复杂化类型再编译期间无法计算得到,会初始化类
//4. System.out.println(Obj.x);
// 类加载器去加载
//5. ClassLoader.loadClass();
}
}
class Obj{
public static final int t=10; //编译期间就已经确定了就是 10
public static int n=111;
public static final int x=new Random().nextInt(100); //值不是常量,运行期间才会确定
static {
System.out.println("Obj is init");
}
public static void getObj(){
System.out.println("NULL");
}
}
class ObjChild extends Obj{
public static int age=12;
static {
System.out.println("Child is init");
}
}
interface I{
final static int a=0;
}
⚡new 一个 Obj 数组的时候,会加载 Obj 类,不会初始化 Obj 对象,但是会导致另一个类的初始化:Lclassloader_study.misc.Obj
这个类就代表了一个元素类型为 Obj 的一维数组,数组中应有的属性 length 和方法 clone() 都是在这个类实现的,这个类是由 JVM 在运行期间动态的为我们生成的,这个动作由anewarray
指令触发,而基本类型的数组由 newarray
指令触发。
⚡ final 修饰的常量会在编译期间就放到调用这个变量的方法的类的常量池中,既不会加载也不会初始化,这一点可以通过反编译ClassActiveUse
看的到,两个类不存在任何关系了,甚至可以在编译完成后将 Obj 的 class 文件删掉
仍然可以执行,但是后面的另一个 final 常量很明显在编译期间无法确定值,只有在运行期间才能回去到值,所以会加载并初始化类
⚡ 对接口的初始化和对类的初始化有一点不同,接口也有初始化过程,接口和类真正有区别是在上面主动使用的第 5 点,在子接口被加载的时候并不要求其父接口全部完成了初始化,只有在真正使用到父接口的时候才会初始化,这一点可以参考前文的 类构造器部分
-XX:+TraceClassLoading
可以用来追踪类的加载信息并且打印出来
对象的访问定位
如果直接使用句柄访问,java 堆中将会划分出一块内存来作为句柄池,reference 中存储的是对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息,如下图所示。
如果使用直接指针访问,那么 java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址,如下图所示,每个对象都有一个自己 Class 对象的引用 (getClass
)
这两种对象访问方式各有优势,使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot 虚拟机使用的是直接指针访问的方式。句柄来访问的情况也十分常见。
JVM 类加载器
概述
虚拟机设计团队把加载阶段中的
“通过一个类的全限定名来获取描述此类的二进制字节流”
这个动作被放到 Java 虚拟机外部去实现,以便于让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”
类加载器并不需要等到某个类被 “首次使用” 时才加载它,这一点从前面讲解的 被动使用的例子 哪里就看得出来
JVM 规范允许类加载器在预料某个类将要被使用的时候就预先加载它,如果在预先加载的过程中遇到了 class 文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误)
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
根(Bootstrap)类加载器
该加载器没有父加载器。它负责加载虚拟机的核心类库,如 java.lang. *等。根类加载器从系统属性 sun.boot.class.path 所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承 java.lang.ClassLoader 类 (c/c++实现的)。
System.out.println(System.getProperty("sun.boot.class.path"));
D:\java\jre\lib\resources.jar;D:\java\jre\lib\rt.jar;D:\java\jre\lib\sunrsasign.jar;D:\java\jre\lib\jsse.jar;D:\java\jre\lib\jce.jar;D:\java\jre\lib\charsets.jar;D:\java\jre\lib\jfr.jar;D:\java\jre\classes
扩展(Extension)类加载器
它的父加载器为根类加载器。它从 java.ext.dirs 系统属性所指定的目录中加载类库,或者从 JDK 的安装目录的 jre.lib.ext 子目录(扩展目录)下加载类库,如果把用户创建的** JAR **文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。
System.out.println(System.getProperty("java.ext.dirs")); //java.ext.dirs 属性
D:\java\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
系统(System)类加载器
也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器
。系统类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。
System.out.println(System.getProperty("java.class.path"));
D:\java\jre\lib\charsets.jar;D:\java\jre\lib\deploy.jar;
D:\java\jre\lib\ext\access-bridge-64.jar;
D:\java\jre\lib\ext\cldrdata.jar;
D:\java\jre\lib\ext\dnsns.jar;
D:\java\jre\lib\ext\jaccess.jar;
D:\java\jre\lib\ext\jfxrt.jar;
D:\java\jre\lib\ext\localedata.jar;
D:\java\jre\lib\ext\nashorn.jar;
D:\java\jre\lib\ext\sunec.jar;
D:\java\jre\lib\ext\sunjce_provider.jar;
D:\java\jre\lib\ext\sunmscapi.jar;
D:\java\jre\lib\ext\sunpkcs11.jar;
D:\java\jre\lib\ext\zipfs.jar;
D:\java\jre\lib\javaws.jar;
D:\java\jre\lib\jce.jar;
D:\java\jre\lib\jfr.jar;
D:\java\jre\lib\jfxswt.jar;
D:\java\jre\lib\jsse.jar;
D:\java\jre\lib\management-agent.jar;
D:\java\jre\lib\plugin.jar;
D:\java\jre\lib\resources.jar;
D:\java\jre\lib\rt.jar;
C:\WorkSpace\concurrent_package\out\production\concurrent_package;
C:\JetBrains\IntelliJ IDEA 2018.1.4\lib\idea_rt.jar
其实所谓的父子加载器并不是继承的父子关系,而是包含的关系,子加载器中包含一个父加载器的引用
自定义类加载器
先看下 JDK 的 ClassLoader(1.8) 源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//没有父加载器就交给根加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//需要子类去实现
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这是 ClassLoader 类加载类的方法,可以看到中间有一段
if(parent!=null){ c = parent.loadClass(name, false);}
这就是双亲委派机制的实现,委托父类,让父类去加载,父类(根加载器)没有就会再层层下降如果有一个加载成功就会成功返回,除此之外还调用了一个没有实现的findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
也就是说想自定义类加载器的话就得重写findClass
方法,实际上这个findClass
才是类加载的核心,真正加载 Class 文件转换为 CLass 实例的就是findClass
方法, loadClass()
只是实现加载的逻辑,比如双亲委派机制
实现一个简易的 ClassLoader
public class MyClassLoader extends ClassLoader {
//将字节码放到这个路径下
private static final String DEFAULT_DIR = "D:\\ClassLoaderTest";
private String dir = DEFAULT_DIR;
private String classLoaderName;
public MyClassLoader() {
}
public MyClassLoader(String classLoaderName) {
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
}
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
}
public String getClassLoaderName() {
return classLoaderName;
}
/**
* xx.xx.xx.xx.xx.AAA
* xx/xx/xx/xx/xx/.AAA.class
*
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = name.replace(".", "/");
File classFile = new File(dir, classPath + ".class");
if (!classFile.exists()) {
throw new ClassNotFoundException("The class " + name + " not found under " + dir);
}
//字节码文件的字节流
byte[] classBytes = loadClassBytes(classFile);
if (null == classBytes || classBytes.length == 0)
throw new ClassNotFoundException("load the class " + name + " failed");
//defineClass 方法可以把二进制流字节组成的文件转换为一个 java.lang.Class
return this.defineClass(name, classBytes, 0, classBytes.length);
}
//将文件流转换为字节流
private byte[] loadClassBytes(File classFile) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(classFile)) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.flush();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
defineClass 方法可以把二进制流字节组成的文件转换为一个 java.lang.Class,前提是 Class 文件是合法的。
//自定义的需要加载的类
public class MyObject {
static {
System.out.println("MyObject static is init");
}
public void Hello(){
System.out.println("Hello World");
}
}
测试自定义的 ClassLoader
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader loader=new MyClassLoader("Resolmi");
//Class<?> aClass = loader.loadClass("classloader_study.myClassLoader.MyObject");
Class<?> aClass = Class.forName("classloader_study.myClassLoader.MyObject", true, loader);
System.out.println(aClass);
System.out.println(aClass.getClassLoader());
Object obj=aClass.newInstance();
Method hello = aClass.getMethod("Hello", null);
hello.invoke(obj, new Object[]{});
}
}
这里直接loadClass
或者Class.forName()
都可以,通常我们的 forName() 都是默认用的** AppClassLoader **也就是系统加载器,但是我们也可以把我们的自定义加载器传递进去。
tip: loadClass 不会初始化类,不属于上面提到的 6 种主动使用的方式,属于被动使用,Class.forName 第二个参数就是控制是否初始化
MyObject static is init
class classloader_study.myClassLoader.MyObject
classloader_study.myClassLoader.MyClassLoader@74a14482
Hello World
如果使用了ide
的话,这里很有可能编译器帮你自动编译了,也就是在你的 classpath 里面已经有字节码文件了,所以就直交给 AppClassLoader 加载了,所以需要将 classpath 里面的删掉,将字节码拷贝到你自定义的 classLoader 指定的目录里面。
双亲委派模式
即在类加载的时候,系统会判断当前类是否已经被加载,如果被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载
这里光看几个内置的 ClassLoader 可能还不太清晰这里用我们自定义的 Loader 来试试
public class MyClassLoaderTest2 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, ClassNotFoundException {
//loader1 的加载目录是 D:\\ClassLoaderTest(默认的)
MyClassLoader loader1=new MyClassLoader("Resolmi-father");
//设置 loader1 为 loader2 的父加载器
MyClassLoader loader2=new MyClassLoader(loader1,"Resolmi2-son");
//这个类存放在 D:\\ClassLoaderTest\\classloader_study\\myClassLoader 下面
Class<?> aClass = loader2.loadClass("classloader_study.myClassLoader.MyObject");
//设置 Loader2 的加载目录,这是个空目录,下面什么都没有
loader2.setDir("D:\\ClassLoaderTest2");
System.out.println(aClass);
System.out.println(aClass.getClassLoader());
System.out.println(((MyClassLoader)aClass.getClassLoader()).getClassLoaderName());
}
}
控制台打印如下
class classloader_study.myClassLoader.MyObject
classloader_study.myClassLoader.MyClassLoader@74a14482
Resolmi-father
我们用Loader2
去加载类,但是这个类的加载目录是空的,然后我们指定Loader2
的父加载器为Loader1
而loader1
得加载路径就是要加载得类的路径,可以看到这个这个类最终还是被加载出来了,而且是被 loader1
加载出来的,也就是loader2
把加载任务委托给了父加载器loader1
, 然后层层委托再回到loader1
,由它加载。
到这里可能会有疑问,为什么要用双亲委派模式?这样走一圈多慢啊,其实这样做主要有两个方面的原因
💡 提高系统安全性,使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。对于一些系统核心类,用户自定义的不起作用了,因为都会交给BootStrapLoader
去加载,比如自定义了一个 java.lang.String 的类,然后在加载的时候经过层层委派最后会交给 BootStrapLoader
去加载然后返回,所以你自定义的 String 根本没有加载的机会,这样就避免了用户篡改 Java 核心的类
💡 避免重复加载,父 Loader 已经加载过的类,子 Loader 就不用再加载了,比如Object 类
这个类在rt.jar
下,所以无论是在哪种环境下,最终都会交给BootStrapClassLoader
去加载这个类,加载得到的都是同一个Object 类
,如果不采用双亲委派机制,让各个 Loader 自己加载那么可能加载出来的就会有很多个 Object 类(不是同一个 Object 类,下面会说到)
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
加密解密类加载器
本质上和上面的没什么区别,就是多了解密的功能,这里首先用加密工具类加密 class
public final class EncryptUtil {
//相当于密钥
public static final byte ENCRYPT_FACTOR = (byte) 0xff;
private EncryptUtil() {
}
public static void encrypt(String source, String target) throws FileNotFoundException {
try (FileInputStream in= new FileInputStream(source); FileOutputStream out = new FileOutputStream(target)) {
int data;
while ((data=in.read())!=-1){
out.write(data^ENCRYPT_FACTOR);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws FileNotFoundException {
encrypt("D:\\ClassLoaderTest\\classloader_study\\myClassLoader\\MyObject.class","D:\\ClassLoaderTest\\classloader_study\\myClassLoader\\MyObject2.class");
}
}
然后用加密解密类加载器加载这个类
public class DecryptClassLoader extends ClassLoader {
private final String DEFAULT_DIR = "D:\\ClassLoaderTest";
private String dir = DEFAULT_DIR;
public DecryptClassLoader() {
super();
}
public DecryptClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = name.replace(".", "/");
File classFile = new File(dir, classPath + ".class");
if (!classFile.exists()) {
throw new ClassNotFoundException("没找到对应的类文件 :" + dir);
}
byte[] classBytes = loadClassByte(classFile);
if (null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("加载失败");
}
return this.defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] loadClassByte(File classFile) {
try (FileInputStream in = new FileInputStream(classFile); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int data;
while ((data=in.read())!=-1){
//主要就是这里发生了变化,异或了 0xff
baos.write(data^0xff);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public void setDir(String dir) {
this.dir = dir;
}
}
当然结果是加载成功啦😋,这里如果用其他的类加载器加载,或者把 0xff 那里去掉,就会报如下错误
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 889275713 in class file classloader_study/myClassLoader/MyObject
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at classloader_study.myClassLoader.MyClassLoader.findClass(MyClassLoader.java:64)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at classloader_study.encryption_study.ClassLoaderTest.main(ClassLoaderTest.java:15)
这个异常是链接阶段验证的错误,是上面提到的defineClass()
抛出来的,因为你加了密,JVM 在加载这个二进制流的时候就无法识别了自然就无法加载。
打破双亲委派机制
覆盖 loadClass()
要打破双亲委派机制主要就是要覆盖loadClass()
方法,自己定义加载类的方式。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
//让父加载器加载 java 核心的包,因为有些类是继承的 Java 内部的核心类比如 Object 类
if (name.startsWith("java.")) {
try {
ClassLoader system = ClassLoader.getSystemClassLoader();
//这里仍然是委托上级
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (Exception e) {
e.printStackTrace();
}
}
try {
//自己先找
clazz = findClass(name);
} catch (Exception e) {
e.printStackTrace();
}
//找不到就交给父加载器
if (clazz == null && getParent() != null) {
getParent().loadClass(name);
}
return clazz;
}
其实就是自己先找,找不到才会交给父加载器,然后一个需要注意的就是你想加载的这个类可能继承了 Java 内部核心的类像Object
类,然后要加载子类就要先加载它的父类,而你的这个包里面肯定是加载不到这些 Java 内部的核心类的,所以这些还是得交给上层的加载器去加载。
测试效果
public class SimpleClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
SimpleClassLoader simpleClassLoader = new SimpleClassLoader();
Class<?> aClass = simpleClassLoader.loadClass("classloader_study.break_parent.SimpleObject");
System.out.println(aClass.getClassLoader());
}
}
注意这里在classpath
里面是有这个类的字节码文件的,按照双亲委托机制应该由 AppClassLoader 去加载
但是仍然是由我们的 SimpleClassLoader 加载的,说明我们成功了打破了双亲委派机制(貌似 Tomcat 也是这种加载机制,有时间看看 Tomcat 的类加载器)
面试题
❓ 能不能自己写个类比如java.lang.String
去覆盖 Java 的String
?如果不覆盖loadClass()
方法使用双亲委托肯定是不行,但是既然上面已经打破了双亲委托那是不是就可以了呢?
Talk is cheap,show me the code
试试就知道了
先准备一个 String 类
package java.lang;
/**
* @author imlgw.top
* @date 2019/4/18 12:01
*/
public class String {
static {
System.out.println("i am init");
}
public int getVal(){
return 250;
}
}
编译好之后放到我们自定义的ClassLoader
的目录下,然后将我们loadClass()
方法加载核心包的地方注释掉(不然还是会交给父加载器去加载)然后为了表示是我们自定义的ClassLoader
加载的我们把 classpath 里面的字节码文件也删掉。
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
SimpleClassLoader simpleClassLoader = new SimpleClassLoader();
Class<?> aClass = simpleClassLoader.loadClass("java.lang.String");
System.out.println(aClass.getClassLoader());
}
然后就会看到如下的SecurityException
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at classloader_study.break_parent.SimpleClassLoader.findClass(SimpleClassLoader.java:52)
at classloader_study.break_parent.SimpleClassLoader.loadClass(SimpleClassLoader.java:77)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at classloader_study.break_parent.SimpleClassLoaderTest.main(SimpleClassLoaderTest.java:7)
其实 Java 为了安全,自定义类取名不能取java.*
等核心包名开头,在preDefineClass
里面做了检查,所以即使你打破了双亲委托机制你依然不能去覆盖 Java 的核心类(肯定不行啊😄)。
其实这里后来了解了Unsafe
后也尝试用Unsafe
去加载这个类
protected Class<?> findClass(String name) throws ClassNotFoundException {
Unsafe unsafe=UnsafeTest.getUnsafe();
String classPath = name.replace(".", "/");
File classFile = new File(dir, classPath + ".class");
if (!classFile.exists()) {
throw new ClassNotFoundException("The class " + name + " found under " + dir);
}
byte[] classBytes = loadClassBytes(classFile);
if (null == classBytes || classBytes.length == 0)
throw new ClassNotFoundException("load the class " + name + " failed");
return unsafe.defineClass(null,classBytes,0,classBytes.length,SimpleClassLoader.this,null);
}
loadClass
同上,尝试加载你会发现 会提示找不到Object
类,嗯?已经在加载父类了,难不成还真可以?这里其实已经和上面的方法不同了,上面的方法是不会进入到加载父类的环节的,直接在加载前就被检测了包名然后 GG 了,随后我在loadClass
中让系统加载器去加载 Object 类,再次尝试加载
java.lang.SecurityException: Prohibited package name: java.lang
at sun.misc.Unsafe.defineClass(Native Method)
at classloader_study.break_parent.SimpleClassLoader.findClass(SimpleClassLoader.java:57)
at classloader_study.break_parent.SimpleClassLoader.loadClass(SimpleClassLoader.java:83)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at classloader_study.break_parent.SimpleClassLoaderTest.main(SimpleClassLoaderTest.java:7)
Exception in thread "main" java.lang.NullPointerException
at classloader_study.break_parent.SimpleClassLoaderTest.main(SimpleClassLoaderTest.java:8)
还是熟悉的味道😂 defineClass
虚拟机本地方法抛了异常
类加载器细节深入
定义加载器&初始加载器
⚡真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载
工作是通过调用 defineClass(findClass)
来实现的;而启动
类的加载过程是通过调用 loadClass
来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer
引用了类 com.example.Inner
,则由类 com.example.Outer
的定义加载器负责启动类 com.example.Inner
的加载过程。
命名空间&运行时包
每个类都有自己的命名空间,命名空间由
该加载器及所有父加载器所加载的类
组成,子加载器的命名空间包含所有父加载器的命名空间
,因此子加载器可以加载的类可以看就按父加载器加载的类,例如系统类加载器可以看见根加载器加载的类。由父加载器加载的类看不见子加载器加载的类,如果两个类之间没有直接或者间接的父子关系,那么他们各自加载的类相互不可见
⚡数组类的 Class 不是由类加载器加载的,是由 JVM 在运行期间动态生成的,但是通过getClassLoader
返回的类加载器和数组的元素的类加载器是一样的,原生的类型比如int
之类的没有类加载器返回的是 null
⚡每个类加载器都有其自己的命名空间,命名空间由该加载器和其所有父类加载器所加载的类组成,同一份字节码两个不同的类加载器加载出来的不是同一个类。
public class MyClassLoaderTest2 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, ClassNotFoundException {
//注意这里的 ClassLoader 要么利用前面的打破了双亲委托机制的,要么把 classpath 里面字节码的删掉。
MyClassLoader loader1=new MyClassLoader("Resolmi1");
MyClassLoader loader2=new MyClassLoader("Resolmi2");
Class<?> aClass2 = loader2.loadClass("classloader_study.myClassLoader.MyObject");
Class<?> aClass1 = loader1.loadClass("classloader_study.myClassLoader.MyObject");
//两个不同的加载器(没有父子关系)加载同一个类加载出来的不是同一个
System.out.println(aClass1.hashCode()); //2133927002
System.out.println(aClass2.hashCode()); //1836019240
}
}
⚡父类加载器无法访问子类加载器加载的类,而子加载器可以访问到父加载器所加载的类
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//使用上面自定义的 ClassLoader
MyClassLoader loader=new MyClassLoader("Resolmi");
Class<?> aClass = loader.loadClass("classloader_study.myClassLoader.Parent");
//创建实例
aClass.newInstance();
}
}
Parent 类
public class Parent {
public Parent() {
System.out.println("Parent is load by" + this.getClass().getClassLoader());
Hello();
}
public void Hello() {
//父加载器访问子加载器加载的类
System.out.println("Parent can see the " + Sub.class);
}
}
Sub 类
public class Sub {
public Sub(){
System.out.println("Sub is load by"+this.getClass().getClassLoader());
new Parent(); //构造 Parent 类
}
}
做完了这些工作之后,编译代码,然后将 classpath 中的Parent.class
拷贝到自定义的 ClassLoader 路径下面,然后删掉 classpath 中的Parent.class
,然后运行
Sub is load byclassloader_study.myClassLoader.MyClassLoader@74a14482
Parent is load bysun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: classloader_study/myClassLoader/Sub
at classloader_study.myClassLoader.Parent.Hello(Parent.java:15)
at classloader_study.myClassLoader.Parent.<init>(Parent.java:10)
at classloader_study.myClassLoader.Sub.<init>(Sub.java:10)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at classloader_study.myClassLoader.ClassLoaderTest.main(ClassLoaderTest.java:14)
Caused by: java.lang.ClassNotFoundException: classloader_study.myClassLoader.Sub
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 9 more
可以看到,我们的Sub
是由我们自定义的MyClassLoader
加载的,因为我们删掉了 Classpath 中的字节码,而在自定义的路径中有所以最后还是由我们的 MyClassLoader
加载了,所以我们的Parent
类首先也会由我们的自定义类加载器 MyClassLoader
去作为初始加载器加载,由于我们的 Classpath 中还有字节码文件,所以在双亲委派机制下Sub 类
最终由AppClassLoader
加载 ,两个类由不同的类加载器加载,然后我们在Parent
类中试图访问Sub
类,结果抛出了ClassNotFoundException
和NoClassDefFoundError
异常
❓ 那我们反过来在Sub
类中访问 Parent
类会发生什么,改造一下 Parent 和 Sub
Parent 类
public class Parent {
public Parent() {
System.out.println("Parent is load by" + this.getClass().getClassLoader());
//Hello();
}
public void Hello() {
//父加载器访问子加载器加载的类
System.out.println("Parent can see the " + Sub.class);
}
}
Sub 类
public class Sub {
public Sub(){
System.out.println("Sub is load by"+this.getClass().getClassLoader());
new Parent(); //构造 Sub 类
//访问父加载器加载的类
System.out.println("Sub can see "+Parent.class);
}
}
和上面一样删掉 Classpath 中 Sub 类的 class 文件没然后运行
findclass is invoke MyClassLoader is loadclassloader_study.myClassLoader.Sub
Sub is load byclassloader_study.myClassLoader.MyClassLoader@74a14482
Parent is load bysun.misc.Launcher$AppClassLoader@18b4aac2
Sub can see class classloader_study.myClassLoader.Parent
Process finished with exit code 0
没有任何问题,由此就可以证明我们上面的结论是正确的。
❓ 面试题:如何让一个类的 static 代码块执行两次
用不同的类加载器去加载这个类,至于为什么应该不用我多说了吧。
类的卸载和 ClassLoader 的卸载
由 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中始终不会被卸载,Java 虚拟机本身会引用这些类加载器,而这些类加载器则会始终引用他们所加载的类的 Class 对象,因此这些 Class 对象始终是可达的。
⚡JVM 中的 Class 只有满足以下三个条件,才能被 GC 回收,也就是该 Class 被卸载(unload)
- 该类所有的实例都已经被 GC。
- 加载该类的 ClassLoader 实例已经被 GC。(Class 对象里面有对 ClassLoader 的引用)
- 该类的 java.lang.Class 对象没有在任何地方被引用。
GC 的时机我们是不可控的,那么同样的我们对于 Class 的卸载也是不可控的,使用-XX:+TraceClassUnloading
或者 jvisualvm 可以看到类的卸载
线程上下文加载器 (TCCL)
在说 TCCL 之前不得不说一下另一个话题,SPI(Service Provider Interface,SPI)服务提供接口,由第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(classpath)里。SPI 接口中的代码经常需要加载具体的实现类。
为什么要使用 SPI? SPI 是 JDK 内置的一种服务提供发现机制。这样做的好处主要是为了解耦,实现动态替换,减少硬编码(比如 jdk1.6 之前的 Class.forName(“XXXX”) )面向接口编程,在很多开源框架中都有体现,比如 Dubbo,Spring 等
那么问题来了,SPI 的接口
是 Java 核心库的一部分位于rt.jar
中,是由根加载器 (Bootstrap Classloader) 来加载的,而SPI 的实现类
是一般是第三方的提供的,位于classpath
目录中,而根加载器很明显是无法直接加载到这个目录下的 SPI 的实现类的 (双亲委派),那SPI
是如何自动加载到实现类的呢?
为了解决这个问题,虚拟机提供了**线程上下文加载器(TCCL)**配合ServiceLoader
来帮助上层加载器加载类,TCCL
破坏了“双亲委派模型”,可以在执行过程中切换为TCCL
来加载第三方的 SPI 实现类,抛弃双亲委派机制,使程序可以逆向使用类加载器。**TCCL **默认是系统类加载器,也可以通过setContextClassLoader
去设置
JDBC 源码案例分析
翻到了最开始学 JDBC 的时候写的代码😄
贾琏欲执事,可以看到第一步还是加载并且初始化驱动,前面已经提到这一步其实没有必要了,jdk1.6 之后因为 ServiceLoader,SPI 机制的出现,就不用再显示的加载驱动,但是正如上面所说Driver
只是个接口存放于rt.jar
中,由根加载器所加载,那 SPI 是怎么自动的加载到mysql
的Driver
实例的呢?😕
我们一步步的来看,首先DriverManager.getConnection()
这里,getConnection
是个静态方法,调用它就会先执行DriverManager
类的静态代码块,而静态代码块里面主要执行的就是loadInitialDrivers()
loadInitialDrivers() 源码
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
// 如果驱动正确打包为 jar 就会用 ServiceLoader 去加载它
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
/****************************************************/
/*ServiceLoad 工具类,注意这个 ServiceLoad 的加载器,默认就是 TCCL*/
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
/****************************************************/
/*在这里会获取到一个 Drivers 的迭代器,但是其实还没有开始加载类*/
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
//迭代的过程中通过 next 反射加载并初始化这个驱动字节码
//没有接收返回的数据库驱动实例
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
//加载 Jdk 中的驱动实例(虽然我并不知道是什么)总之我们第三方的驱动已经加载好了
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
可以看到中间有一行很关键的代码
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这就是我上面所提到的 ServiceLoader
对于 SPI 机制,JDK 提供了
java.util.ServiceLoader
工具类,在使用某个服务接口时,它可以帮助我们查找该服务接口的实现类,加载和初始化,前提条件是基于它的约定:当服务的提供者提供了服务接口的一种实现之后,在jar
包的META-INF/services/
目录里同时创建一个以服务接口命名的文件
。该文件里就是实现该服务接口的具体实现类(去解压看看那些 jar 包就可以看见这些信息 o( ̄▽ ̄) ブ)
ServiceLoader 类源码
为了节约篇幅删掉了一些注释,发现其实整个类也没多少行大概 2,3 百行的样子,需要注意这个类并不是线程安全的,所以使用的时候需要注意
public final class ServiceLoader<S> implements Iterable<S> {
//目录前缀就是从这里来的
private static final String PREFIX = "META-INF/services/";
//实现类 Service
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
// 按照实例的顺序,来缓存服务提供者避免重复的加载,具体可以看下面的 iterator 方法
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
public void reload() {
providers.clear();
//初始化懒加载迭代器
lookupIterator = new LazyIterator(service, loader);
}
//构造器
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
// Private inner class implementing fully-lazy provider lookup
// 看名字就知道了,懒迭代器,在迭代的时候才真正的加载
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
//迭代器的 next
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//利用 TCCL 加载实现类,但是不初始化
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//newInstance() 初始化了对应的实现类
S p = service.cast(c.newInstance());
//放到 providers 中
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* Lazily loads the available providers of this loader's service.
* @return An iterator that lazily loads providers for this loader's
* service
*/
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/* 重载的 ServiceLoad */
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
//DriverManage 里面就是调用的这个方法
public static <S> ServiceLoader<S> load(Class<S> service) {
//拿到了 TCCL
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//调用上面重载的 ServiceLoad 方法
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
首先我们要明白一点,
Driver 接口
,DriverManage 类
,以及ServiceLoader
都是由根加载器去加载的(如果不相信的话可以用 TraceClassLoading 去查看),所以在ServiceLoader
中也是无法直接加载具体得实现类的
前面loadInitialDriver()
调用的就是这里的ServiceLoader.load(Class< S> service)
方法,这个方法中悄悄的拿到了TCCL
,而 TCCL 在Launcher
类(系统加载器和扩展加载器都是在 Launcher 中实现的)中默认设置成了系统加载器,具体可以去看一下源码这里我就不展开了,然后调用另一个重载的构造方法将TCCL
传递进去,最终调用了 reload()
方法
public void reload() {
providers.clear();
//初始化懒加载迭代器
lookupIterator = new LazyIterator(service, loader);
}
在这个方法中首先清空服务提供者 (providers) 缓存,然后初始化了一个LazyIterator
看名字就知道是啥意思了,其实到这里仍然没有任何具体的加载动作,因为这里采用的是按需加载,也就是懒加载,在迭代的时候才会去加载类
private class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//拿到接口全名
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//解析
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
//迭代器的 next
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//具体加载类的地方就是在这里
//利用前面传进来的 TCCL 加载实现类,但是不初始化类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//newInstance() 实例化对应的实现类
S p = service.cast(c.newInstance());
//put 到 providers 中
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
🔸 可以看到在迭代器的nextService()
中才开始利用的Class.forName()
加载的具体实现类,而这个加载器正器正是前面reload
传递过来的 TCCL
也就是默认的系统类加载器
🔸 随后在紧跟的 try 语句中通过 newInstance()
实例化了具体的实现类 (MySql 的驱动) ,然后 put 进 providers 队列并且返回实例化的实现类,但是在loadInitialDrivers
中并没有接收这个返回,那他这里实例化是什么用意呢?
我们回到getConnection()
方法
getConnection() 源码
前面的静态方法调用完毕驱动已经加载,下面就是获取数据库连接了。
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
// Reflection.getCallerClass() 调用者的 Class 对象
return (getConnection(url, info, Reflection.getCallerClass()));
}
一般获取连接都是调用的上面这个方法,这个方法最终会调用另一个重载的方法,同时传入一个调用者的 Class 对象
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
//Caller 就是调用者的 CLass 也就是我们的应用代码类
//获取到我们应用类的类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
//如果为空就,获取线程线下文加载器
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
//遍历这个 registeredDrivers 里面都是 DriverInfo
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
//检查加载驱动的加载器是不是调用者的类加载器
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//获取连接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
🔸 可以看到中间有一个 foreach 循环,遍历registeredDrivers
,这是个CopyOnWriteArrayList
这个类看名字就知道存放的是已经注册的Drivers
实现类,那这些实现类是什么时候注册进来的呢?回到我们之前抛出的一个问题,在ServiceLoader
的迭代器中加载了具体的类之后进行了实例化,但是DriverManager
中并没有接收这个实例,我们来看一下具体的驱动实现类
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
//注册到 DriverManager 中去
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
🔸 相信看到这里大家就明白了,前面初始化这个类的作用就是为了能触发这个静态代码块,进而执行 registerDriver() 方法进行注册,真是妙啊👏一环套一环
🔸 还有一个需要注意的地方就是这里注册的虽然是个空的 Driver 类,但是别忘了它还有个父类 NonRegisteringDriver
这个类才是真正的实现类具体的connect()
等方法都是在这个里面实现的,而 Driver 继承了它的方法
🔸 在遍历registeredDrivers
的时候还调用了一个isDriverAllowed(aDriver.driver, callerCL)
方法这个方法第一个参数就是驱动的实现类,第二个参数就是前面获取到的调用者的类加载器 ,作用就是通过利用传进来的加载器尝试加载这个类,然后判断是不是同一个类,(众所周知不同的加载器因为命名空间的存在,即使加载同一份字节码文件得到的也不是一个类) 如果是就允许加载,否则不允许,为啥要这样做呢?其实还是因为命名空间的问题,因为有了 SPI 的机制,你加载初始化这个实现类的加载器 (TCCL) 和最终去调用实现类的方法的类的加载器有可能不是同一个,因为程序员可以很容易的将 TCCL 修改成其他的类加载器,如果不保证一致的话后面就会出现ClassCastException
等异常
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
总结
当高层提供了统一接口让低层去实现(面向接口编程,解耦),同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的 ClassLoader 找到并加载该类。
Jar Hell
Jar 包地狱,参考
这个问题其实可以通过OSGI
等组件化框架来解决,使用 OSGI 可以完美解决这个问题,OSGI 是基于模块(Bundle)驱动的,每个模块都有属于自己的 classpath 和类加载器,模块之间通过包暴露和引入进行关联,每个模块有着自己独立的生命周期,我们可以动态地对模块进行加载、卸载、更新。如此看来,OSGI 可以用一句话描述,就是一个为 Java 提供的动态模块化的系统。但是 OSGI 太过复杂,实用性并不强
这里我主要想说的是怎么在代码中利用类加载器来检测 Jar Hell
public class JarHell {
public static void main(String[] args) {
try {
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources("org/apache/log4j/Logger.class");
while(urls.hasMoreElements()) {
URL url = urls.nextElement();
System.out.println(url);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这样就可以找到 classpath 中冲突的 jar 包,当然通过 idea 的工具会更方便 😂
参考
《深入理解 Java 虚拟机》