Java 类加载

Java 类加载

在java代码中,类型的加载,连接与初始化过程都是在程序运行期间完成的(类class文件信息在编译期间已经确定好)。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、使用Using和卸载Unloading7个阶段。其中准备、验证、解析3个部分统称为连接Linking

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

注意,本文的JDK版本是Java 1.8,在Java 9 引进模块化后,ClassLoader也有了一些变化。

何时类加载

什么时候开始加载?Java 虚拟机规范中没有进行强制约束,这点可以交由虚拟机自行把握。

但是对于初始化阶段,虚拟机规范定义了有且只有5种情况需要对类进行初始化。这5种行为被称为主动引用。

  1. 使用new、getstatic、putstatic和invokestatic这4条字节码指令时,如果类没有进行初始化,需要先初始化。对应的代码场景是:使用new实例化对象时、读取或设置类的静态字段(被final修饰,在编译期把结果放入常量池的静态字段除外)和调用类的静态方法时。
  2. 使用java.lang.reflect包下方法对类进行反射时,如果类没有进行过初始化,先初始化。
  3. 初始化一个类时,如果其父类还没有进行过初始化,先初始化其父类。
  4. 当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的解析结果为REF_getStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则对其初始化。

除了这5种行为,其他的引用类的方式都被称为被动引用。

  • 通过子类引用父类的静态字段,不会导致子类初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class SuperClass{
    public static int value;
    }
    public class SubClass extends SuperClass{}
    public class test{
    public static void main(String[] args){
    System.out.println(SubClass.value);
    // 没有输出
    }
    }
  • 通过数组定义来引用类,不会触发类的初始化。

    1
    2
    3
    4
    5
    6
    7
    public class test{
    public static void main(String[] args){
    SuperClass superclass = new SuperClass[10];
    // superclass 的类型是 [LSuperClass ,有虚拟机生成的数组Class
    // 没有输出
    }
    }
  • 常量会在编译期存入常量池,不会触发定义常量的类的初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    public class ConstantClass{
    public static final String hello = "hello";
    }
    public class Test{
    public static void main(String[] args){
    System.out.println(ConstantClass.hello);
    }
    }

    编译期已经把ConstantClass.hello 转化为test类的常量池中常量, 所以此处的字节码是直接从 Test class 的常量池中取出,不会加载ConstantClass. 如果将 hello 定义成 UUID.randomUUID().toString(), 当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池当中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。

  • 接口也有初始化过程,接口与类的区别仅在第3种情况: 接口初始化是,并不要求其父接口全部完成初始化,可以懒加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test5 {

public static void main(String[] args) {
System.out.println(ChildInterface.b);
}
}

interface ParentInterface{

public static final int a = 5;
}

interface ChildInterface extends ParentInterface{

public static int b = new Random().nextInt(4);
}

JVM 日志验证:使用-XX:+TraceClassLoading 或者是 -verbose:class 命令行选项开启查看类加载信息的功能.

java -XX:+TraceClassLoading

加载动作

加载是查找具有特定名称的类或接口类型的二进制表示并从该二进制表示创建类或接口的过程。在加载阶段,虚拟机需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

首先要说明的是Java获取类的二进制流有多种方式:

  1. 从Zip包中读取,例如 JAR,WAR,EAR等格式。
  2. 从网络中获取,例如Applet。
  3. 运行时动态生成,例如Java的动态代理。
  4. 其他文件生成,例如JSP生成对应的Class类。

… …

值得一提的是,对于非数组类,加载阶段是通过类加载器来实现的(可以是系统提供的引导类加载器,也可以是自定义的类加载器);而数组类并不通过类加载器加载,它是由JVM直接创建的。当然,数组类的元素类型(数组去掉所有维度的类型)还是要靠类加载器加载。

数组类的创建遵循以下原则:

  1. 如果数组的组件类型(数组去掉一个维度的类型)是引用类型,那么就采用类加载器去加载这个组件类型,数组将会在加载组件类型的类加载器的类名空间上被标识。
  2. 如果数组的组件类型不是引用类型,如int[],那么Java虚拟机会把数组标记为与引导类加载器关联。
  3. 数组类的可见性与它的组件类型可见性一致,如果组件类型不是引用类型,那么数组类的可见性默认为Public。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区的数据存储格式由虚拟机自行定义实现,虚拟机规范未规定具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(未规定存储在java堆中,hotspot虚拟机将Class对象存储在方法区。)

加载阶段和连接阶段的部分内容是交叉进行的,加载尚未完成时,连接阶段可能已经开始了。

验证

验证是连接的第一步。这一阶段的目的是保证 class 文件符合虚拟机规范。java 虚拟机规范对 class 文件验证给出了一些建议:

Java虚拟机实现可以使用两种策略进行验证:

  • 必须使用类型检查进行验证来验证class版本号大于或等于50.0的文件(JDK 1.6及之后)。
  • 所有Java虚拟机实现都必须支持通过类型推断进行验证,以验证class版本号小于50.0的文件,但那些符合Java ME CLDC和Java Card配置文件的实现除外。

1.6 之后的jvm 进行了优化,在 方法体的code属性的属性表中增加了名为 StackMapTable 的属性,这项属性描述了方法体中所有基本块(按控制流拆分的代码块)开始时,本地变量表和操作栈应有的状态。在字节码验证阶段,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable中记录是否合法。

具体的验证实现,此处就不赘述了,可以通过上面的链接查看。

准备

准备工作包括为类或接口创建静态字段,并将这些字段初始化为其类型默认值。在通常情况下,静态字段在准备阶段是默认值,但是,如果字段的属性表中有 ConstantValue 属性,那么在准备阶段变量就还会被初始化成ConstantValue 属性所指定的值。

1
2
3
public static int value = 123;         // 0
public static final int value = 123; // 123
// final 修饰的值在class 中有 ConstantValue

初始默认值如下:

type value
int 0
long 0L
short (short)0
char ‘\u0000’
boolean false
float 0.0f
double 0.0d
reference null

解析

在前面的 JVM 内存介绍中有提到过运行时常量池,Java虚拟机为每个类型维护一个常量池,这是一种运行时数据结构,它的很多功能类似于符号表。

class 文件中的constant_pool 表用于在类或接口创建时构造运行时常量池,运行时常量池中的所有引用最初都是符号的。运行时常量池中的符号引用是从class中派生的。

解析阶段是虚拟机将常量池中的符号引用换成直接引用的过程。

  • 符号引用: 符号引用使用一组符号来描述所引用目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用和虚拟机内存无关,其字面量明确定义在class 文件中。
  • 直接引用: 直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位目标的句柄。直接引用和虚拟机的内存布局相关,同一符号引用在不同虚拟机实例上的直接引用一般不会相同,有直接引用,说明对象已在内存中。

虚拟机规范未规定解析的发生时间,只要求在执行 anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, 和putstatic这些用于操作符号引用的指令之前,先对符号引用进行解析。

对同一个符号引用进行多次解析,是很常见的事情。除了 invokedynamic 指令外,虚拟机实现可以对第一次解析的结果进行缓存,在运行时常量池记录直接引用,并把常量标识为已解析状态。这样可以避免多次重复解析。

对于 invokedynamic指令,因为该指令是用于动态语言支持的,所以,只有当程序执行到这的时候,解析动作才能进行。

初始化

类的初始化是类加载的最后一步。在准备阶段,静态变量已经赋过初始值。而在初始化阶段,则根据代码中的逻辑去初始化,初始化阶段是执行类构造器<cinit>()方法的过程。

  • <cinit>()方法是编译器自动收集类中所有静态变量赋值和 static 代码块合并而成。编译器收集的顺序是在源文件中出现顺序。
  • <cinit>()方法与类的构造方法<init>()不同,不需要显式调用父类的构造器,虚拟机会保证子类的<cinit>()方法执行前,已经执行了父类的<cinit>()方法。
  • 执行接口中的<cinit>()方法,不需要先执行父接口的<cinit>()方法,只有使用父接口的静态变量时,才会执行<cinit>()方法。
  • 接口的实现类,在初始化时,不需要执行接口的<cinit>()方法。
  • <cinit>()方法对于类或接口不是必须的,如果一个类中没有静态变量,编译器可以不生成<cinit>()方法。
  • 虚拟机会保证多线程环境下,<cinit>()方法被正确地加锁,同步。如果有多个线程同时去初始化一个类,那么只有一个线程会执行<cinit>()方法,其它的线程都会被阻塞。直到类的构造器执行完毕。(单例模式的实现方案之一)

代码示例见Java 对象启动顺序

类加载器

类加载器的设计目的是,通过一个类的全限定名来获取定义此类的二进制字节流。但在 Java 程序中的作用并不仅局限于类的加载-初始化阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同才能确定其在 JVM 中的一致性,也就是说: 比较两个类是否”相等”,只有在这两个类是同一个类加载器的前提下才有意义,否则即使两个类来自同一个class 文件,被同一个虚拟机加载,若加载的类加载器不同,这两个类就不同。

这里的相等包括Class对象的equals()方法,isAssignableFrom()方法,isInstance()方法的返回结果,也包括 instanceof 关键字的返回结果。

java classloader

如上图所示,java 提供了3种类加载器:

  • Bootstrap ClassLoader: 启动类加载器,这个加载器负责加载$JAVAHOME/jre/lib下的,或是被-Xbootclasspath参数所指定的路径下的类库加载到虚拟机内存。用户在编写自定义加载器时,如果想把加载请求指派给启动类加载器,可以直接使用 null 代替。这个类加载器是使用 C++ 语言实现的。
  • Extension ClassLoader: 扩展类加载器,这个加载器由 sun.misc.Launcher$ExtClassLoader实现,这个加载器负责加载$JAVAHOME/jre/lib/ext下的,或是被java.ext.dirs变量所指定的路径下的所有类库。
  • Application ClassLoader: 应用程序类加载器,这个加载器由 sun.misc.Launcher$AppClassLoader实现,这个类加载器是ClassLoader中的 getSystemClassLoader()方法的返回值,负则加载 ClassPath上所指定的类库,一般是程序中默认的加载器。

像图中的类加载器之间的关系,也被称为双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都要有自己的父加载器,这里的父子关系,一般不是通过继承,而是通过组合实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package java.lang;
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// ... ...
}
// ClassLoader的继承关系
// java.lang.ClassLoader
// -> java.security.SecureClassLoader
// -> java.net.URLClassLoader
// -> sun.misc.Launcher$ExtClassLoader
// -> sun.misc.Launcher$AppClassLoader

一个类加载器查找class和resource时,是通过“委托模式”进行的:它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

ClassLoader源码分析

介绍完了JVM 默认的类加载模型,我们来分析下ClassLoader的源码。

并行加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class ClassLoader {
// 并行加载ClassLoader 配置
private static class ParallelLoaders {
private ParallelLoaders() {}
// set中存储了并行加载的类加载器
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
// 保证 所有classLoader的父类是可并行的
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

/**
* 将给定的ClassLoader子类,注册为并行加载。
* 注册成功返回true, 如果父类未注册成功,返回false
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}

// 注册并行类加载的方法
@CallerSensitive
protected static boolean registerAsParallelCapable() {
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
return ParallelLoaders.register(callerClass);
}

上面的ParallelLoaders中存储了所以可以并行加载的classLoader。支持并行加载的ClassLoader可以注册自己例如:

1
2
3
4
5
6
public abstract class FlinkUserCodeClassLoader extends URLClassLoader {

static {
ClassLoader.registerAsParallelCapable();
}
}

下面来看下普通加载和并行加载的具体流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public abstract class ClassLoader {
// key是类名,value 是锁对象
private final ConcurrentHashMap<String, Object> parallelLockMap;

// This method is invoked by the virtual machine to load a class.
// 加载流程的入口,
private Class<?> loadClassInternal(String name) throws ClassNotFoundException{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 获取锁,此时如果支持并行加载,获取的是class对应的锁,
// 不支持并行加载,获取的是当前classloader类.
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) {
long t1 = System.nanoTime();
// 如果还是找不到,就自己找
// 该方法由ClassLoader的子类自己实现
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;
}
}
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
// .. ...
}

时序图如下:

sequenceDiagram
    Jvm->>ClassLoader: loadClassInternal
    ClassLoader->>ClassLoader:loadClass
    ClassLoader->>ClassLoader:getClassLoadingLock
    ClassLoader->>ClassLoader:findLoadedClass
    opt if class not founded
        alt if Parent_ClassLoader is not null
            ClassLoader->>Parent_ClassLoader:loadClass
            Parent_ClassLoader-->>ClassLoader:return
        else else Parent_ClassLoader is null
            ClassLoader->> BootstrapClassLoader:findBootstrapClassOrNull
            BootstrapClassLoader-->>ClassLoader:return
        end
        opt if class still not founded
            ClassLoader->>ClassLoader:findClass
        end 
    end 
    ClassLoader-->>Jvm:return

获取类加载器的几种方法:

1
2
3
4
5
6
// 获取当前类的加载器
String.class.getClassLoader();
// 获取线程上下文类加载器
Thread.currentThread().getContextClassLoader();
// 获取系统类加载器
ClassLoader.getSystemClassLoader()

线程上下文类加载器

线程上下文类加载器的一般使用模式:

  1. 获取
  2. 使用
  3. 还原
1
2
3
4
5
6
7
8
9
10
11
12
//获取
ClassLoader classLoader = Thread.currentThread.getContextClassLoader();
try{
//设置要使用的类加载器, 如cls就是启动类加载器就可以加载到实现类了
//ServiceLoader加载SPI时,默认就是采用的线程类加载器
Thread.currentThread.setContextClassLoader(cls);
//使用,里面调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情
SomeMethod();
}finally{
//还原
Thread.currentThread.setContextClassLoader(classLoader);
}

在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层的加载。但是对于SPI来说,有些接口是java核心库所提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现来自于不同的jar包(厂商提供)java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

ClassLoader.getSystemClassLoader

ClassLoader.getSystemClassLoader() 会返回系统类加载器。系统类加载器的实例是由系统属性 “java.system.class.loader” 控制的。

  1. 如果系统属性是默认值null,那么getSystemClassLoader的返回值就是上面的sun.misc.Launcher$AppClassLoader.
  2. 当定义了系统属性时系统会让默认的AppClassLoader这个类加载器去加载即将成为系统类加载器的这个自定义加载器。
  3. 该系统属性的值(方法名)将作为系统类加载器的类名。必须在自定义的类加载器中编写一个构造函数,该构造函数只接受一个参数,类型是类加载器,用作类加载器的委托父类。然后使用应用程序类加载器,作为这个构造函数的参数。此时自定义的类加载器将成为系统类加载器。
1
2
3
4
5
6
public class TestSystemClassLoader extends ClassLoader{
public TestSystemClassLoader(ClassLoader parent){
super(parent);
}
}
// java -Djava.system.class.loader=info.victorchu.type.TestSystemClassLoader info.victorchu.type.ClassLoaderTest

getSystemClassLoader用于哪里?

  1. 返回用于委托的系统类加载器。他是新的(自定义)类加载器的默认委托双亲(父类),并且是通常用于启动应用程序(main方法)的类加载器。
  2. 此方法首先在程序运行时启动时很早被调用,此时创建并设置系统类加载器;并将其设置为调用该方法的线程的上下文类加载器。

下面来看看getSystemClassLoader的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
//初始化系统类加载器
initSystemClassLoader();
if (scl == null) {
return null;
}
//获取安全管理器
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
//初始化系统类加载器
private static synchronized void initSystemClassLoader() {
//如果系统类加载器没有被设置
if (!sclSet) {
//系统类加载器没有没有被设置又不为空 矛盾 就抛异常
if (scl != null)
throw new IllegalStateException("recursive invocation");
//Launcher.getLauncher 获取该实例
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
//创建成功
Throwable oops = null;
//把Launcher 中的系统类加载器付给当前类的类加载器
scl = l.getClassLoader();
try {
//获取类加载器对象
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));//传入系统类加载器作为父加载器
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
//此时系统类加载器设置完成
sclSet = true;
}
}

接下来看看 SystemClassLoaderAction中的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader> {
private ClassLoader parent;

SystemClassLoaderAction(ClassLoader parent) {
this.parent = parent;
}

public ClassLoader run() throws Exception {
//获取java.system.class.loader 系统属性
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
//如果改系统属性没被设置返回 appClassLoader
// 此时的 TCCL 是 APPClassLoader,见sun.misc.Launcher 中代码
return parent;
}
//如果自定义了系统类加载器
//获取cls名的Class参数为(ClassLoader.class)的构造函数
//Class.forName 加载cls对应的二进制名 并使用父类(parent)初始化(true)
Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
//将父类加载器即系统类加载器传给了自定义类加载器
//所以当自定义类加载器在设置完时是由appClassLoader去加载的
//获取自定义的ClassLoader类
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
Thread.currentThread().setContextClassLoader(sys);
//返回自定义类加载器
return sys;
}
}

Class.forName

Class.forName()有两个重载的方法,都是public方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 参数initialize 表示该类是否必须被初始化
public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

// 实际调用forName0
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
//获取调用者的Class对象 单参默认使用调用者的类加载器
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (loader == null) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (ccl != null) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}

Class.forName 和 loadClass的区别:

  • Class.forName() 方法中,initialize参数控制类在加载的过程中是否进行初始化。
  • ClassLoader.getSystemClassLoader().loadClass()方法中,resolve参数控制类在加载的过程中是否进行链接(resolve为true,会执行静态代码块)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package info.victorchu.type.classload;

public class DemoClass {
private String name;

private static boolean flag;
static {
flag = false;
System.out.println("flag 的值为:" + flag);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

public class ClassLoaderDemo {
public static void main(String [] args){
try {
ClassLoader system = ClassLoader.getSystemClassLoader();
Class<DemoClass> cls = null;
System.out.println("----------方法1----------");
cls = (Class<DemoClass>)Class.forName("info.victorchu.type.classload.DemoClass");

System.out.println("----------方法2----------");
cls = (Class<DemoClass>)Class.forName("info.victorchu.type.classload.DemoClass", false, system);

// 类加载过程中的缓存机制,由于方法1已经加载了该类,因此方法3不会再次加载该类
System.out.println("----------方法3----------");
cls = (Class<DemoClass>)Class.forName("info.victorchu.type.classload.DemoClass", true, system);

} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class ClassLoaderDemo1 {
public static void main(String [] args){
try {
ClassLoader system = ClassLoader.getSystemClassLoader();
//没有执行静态代码块,由此可见DemoClass只是进行了装载,没有进行链接与初始化。
System.out.println("----------方法4----------");
Class<DemoClass> cls = (Class<DemoClass>)ClassLoader.getSystemClassLoader().loadClass("info.victorchu.type.classload.DemoClass");

} catch (Exception e) {
e.printStackTrace();
}
}
}
// output1
//----------方法1----------
//flag 的值为:false
//----------方法2----------
//----------方法3----------
// output2
//----------方法4----------

自定义类加载器

有些类可能不是起源于一个文件;他们可能会产生从其他来源,如网络。ClassLoader的方法defineClass将字节数组转换为类的实例。可以很好的帮助自定义类加载器的实现。

编写一个自定义classLoader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NetworkClassLoader extends ClassLoader {
String host;
int port;

public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);//通过名字将Class对象返回给调用者
}

private byte[] loadClassData(String name) {
// load the class data from the connection
.
}
}

类命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。同一个命名空间内的类是相互可见的。只是可见,但不一定能互相访问,能不能访问由修饰符决定,比如private public 等。
    • 在同一个命名空间中,不会出现类的完整名字(包括类的包名) 相同的两个类。
    • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
    • 父加载器加载的类不可以看到子类加载器加载的类
    • 但是子类加载器加载的类可以看到父类加载器加载的类

命名空间的一个大的应用场景是隔离。

应用App依赖库LibA和LibB,而LibA和LibB又同时依赖LibBase,而LibA和LibB都是其他团队开发的,其中LibA发布了一个重要的修复版本,但是依赖LibBase v2.0,而LibB还没有升级版本,LibBase还不是兼容的,那么此时升级就会面临困难。在生产环境中这种情况往往更恶劣,可能是好几层的间接依赖关系。

隔离容器用于解决这种问题。它把LibA和LibB的环境完全隔离开来,LibBase即使类名完全相同也不互相冲突,使得LibA和LibB的升级互不影响。众所周知,Java中判定两个类是否相同,看的是类名和其对应的class loader,两者同时相同才表示相等。隔离容器正是利用这种特性实现的。

隔离的核心:

  • class的区分由class name和载入其的class loader共同决定。
  • 当在class A中使用了class B时,JVM默认会用class A的class loader去加载class B。

我们有两种方案来实现隔离。

  1. (parent first): 覆写自定义classLoader的findClass 方法,这也是JDK的注释中推荐的,ClassLoader的findClass的实现是直接抛出ClassNotFound异常,我们在这里可以直接使用URLClassLoader,findClass方法支持从路径加载Class。此外,我们还要保证当前自定义ClassLoader的URL不在上级parent ClassLoader的URL中,这样才可以保证父类ClassLoader加载不到该类,将该任务交给子类实现。
  2. (child first): 覆写自定义ClassLoader的loadClass 方法,这样我们会直接打破双亲委派模型。

下面是 ClassLoader的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 双模类加载器。
* 1. parent first 模式。
* 2. child first 模式。
*/
public class DualModeClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}

private final String[] alwaysParentFirstPatterns;

private Boolean parentFirst;

public DualModeClassLoader(URL[] urls, ClassLoader parent, String[] alwaysParentFirstPatterns, Boolean parentFirst) {
super(urls, parent);
this.alwaysParentFirstPatterns = alwaysParentFirstPatterns;
this.parentFirst = parentFirst;
}

@Override
public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if(parentFirst){
return loadClassParentFirst(name, resolve);
}else{
return loadClassChildFirst(name,resolve);
}
}
public final Class<?> loadClassChildFirst(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// check whether the class should go parent-first
for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {
if (name.startsWith(alwaysParentFirstPattern)) {
return super.loadClass(name, resolve);
}
}
try {
// check the URLs
c = findClass(name);
} catch (ClassNotFoundException e) {
// let URLClassLoader do it, which will eventually call the parent
c = super.loadClass(name, resolve);
}
} else if (resolve) {
resolveClass(c);
}
return c;
}
public final Class<?> loadClassParentFirst(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
}

类卸载

  • 当某个类被加载、连接和初始化后,它的生命周期就开始了。
  • 当代表某个类的Class对象不再被引用,即不可触及(没有引用指向)时,Class对象就会结束生命周期,某个类在方法区内的数据也会被卸载,从而结束MySample类的生命周期。
  • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
  • 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
  • 由用户自定义的类加载器所加载的类是可以被卸载的。
  • 运行程序时,某个类由loader加载。在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。 另一方面,一个Class对象总是会引用它的类加载器, 调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与loader之间为双向关联关系。
  • 一个类的实例总是引用代表这个类的Class对象。 在 Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

要使用JVM参数 -XX:+TraceClassUnloading 来打印类的卸载消息

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
UserClassLoader userClassLoader = new UserClassLoader();
userClassLoader.setPath("~/test/");
Class<?> clazz = userClassLoader.loadClass("XXX");//不能是当前类
Object object = clazz.newInstance();
//让类和自定义类加载器不再互相引用
userClassLoader = null;
clazz = null;
object = null;
System.gc();//实际场景不这么用
// 此时可以看到 unload class 的日志。
}