Java 类加载

从外部文件载入类信息

类加载过程


大体上分为3个过程

  • 装载Loading: 类文件从文件系统载入内存
  • 链接Linking
    • 校验Verifying: 检查读入数据是否符合Java规范
    • 准备Preparing: 创建类结构储存信息,给静态变量分配存储空间填充默认值,如果final直接赋常量
    • 解析Resolving: 常量池符号引用替换成直接引用,编译时,被引用目标尚未载入内存,用常量符号引用(Symbol Reference)代替
  • 初始化Initializing: 执行类构造器<client>过程,静态变量赋值,执行静态初始化块。不涉及实例因此只有静态行为

类加载器工作行为


  • 动态加载:class第一次被引用的时候进行加载
  • 不可卸载:class载入后不能再删除,可以删除整个加载器
  • 层级代理:加载器具有层级结构,首先询问上级,实在没有再自己加载

类加载是延迟的,不必要就推迟

  • 类只声明变量时不会加载,因此加载一个类并不会一并加载成员类
Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
new A(); //加载A 不加载B
B b; //不加载B
}
}

class A {
private B b;
}

class B {
}
  • 只使用类信息,会被加载,但是不会初始化
Sample
1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Class c = A.class; //加载A 不执行初始化
}
}

class A {
static {
System.out.println("init");
}
}

类加载器类型


1
2
3
4
5
6
Bootstrap
|---Extension
|---System
|---User-defined
|---User-defined
|---User-defined
  • Bootstrap Classloader: 启动类加载器,C实现,加载Java核心代码jre/lib-Xbootclasspath指定的路径
  • Extension Classloader:扩展类加载器,加载扩展代码比如加密压缩等类库,jre/lib/extjava.ext.dirs指定的路径
  • Application Classloader(System ClassLoader):应用加载器,可以进一步由开发者用代码自定义实现,加载-classpath-Djava.class.path指定的路径

名义上启动类加载是扩展类加载的父级,但是代码中不能引用到

Sample
1
2
3
4
ClassLoader c = ClassLoader.getSystemClassLoader();
System.out.println(c); //sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(c.getParent()); //sun.misc.Launcher$ExtClassLoader@3796751b
System.out.println(c.getParent().getParent()); //null

类加载机制


  • 双亲委派(Parent Delegation)
    一个加载器要加载一个类,先转给父级加载器,直到最顶层加载器,父级加载不了再由子加载器加载
    好处是同一路径只能被加载一次,比如自定义了一个和库同名的java.lang.String,因为已经由启动类加载了库类,自定义类无法加载

  • 负责制
    一个类由一个类加载器加载,那么这个类所依赖和引用的类也由同一类加载器加载,除非显式指定其他加载器
    也就是说Java类库中的类由启动加载器加载,那么类库中引用的其他类也是由启动加载器加载

类加载机制破坏


JDNI,JDBC接口都是位于核心库,由启动加载器加载,但是要调用具体的应用实现类,需要应用加载器加载
使用线程上下文的加载器Thread.currentThread().getContextClassLoader(),如无特殊更改,默认是应用加载器
即效果是上级的启动加载器调用下级的应用加载器进行加载

类加载实现


ClassLoader是个抽象基础类
loadClass()是加载入口,默认不解析
loadClass()是个模板方法,实现委派逻辑
自定义加载器主要是实现findClass()方法, 逻辑主要解决两块内容

  1. 去哪里找类,即获取类的字节数组
  2. 找到后如何把字节数据转成Class对象,可以使用已提供的defineClass方法
ClassLoader.java
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public abstract class ClassLoader {
//构造时传入父级
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
//默认构造时采用系统类加载器作为父级委派
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}


//默认是不解析的
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

//核心加载流程
//如果不初始化,只是单纯检测是否存在,就不用解析
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
//加锁保护,避免同时加载同一个类
synchronized (getClassLoadingLock(name)) {
//先检查是不是已经加载过
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();
//上级没加载到,加载器开始找类尝试加载
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 Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

//尝试用系统加载器加载
protected final Class<?> findSystemClass(String name) throws ClassNotFoundException
{
ClassLoader system = getSystemClassLoader();
if (system == null) {
if (!checkName(name))
throw new ClassNotFoundException(name);
Class<?> cls = findBootstrapClass(name);
if (cls == null) {
throw new ClassNotFoundException(name);
}
return cls;
}
return system.loadClass(name);
}

//将二进制数据转为Class对象,二进制可以是各种来源
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
//...
}

显式加载


Class的forName方法可以传入加载器参数,指定由某一加载器加载

Class.java
1
2
3
4
5
@CallerSensitive
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) {
//...
return forName0(name, initialize, loader, caller);
}

不指定加载器,则使用调用者的加载器,并且要执行初始化

Class.java
1
2
3
4
5
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

两个方法都标记成@CallerSensitive表示只有通过启动加载器或者扩展加载器加载的类才能调用
动态加载forName默认就初始化,而loadClass默认只加载没有链接初始化,因此数据库加载驱动要使用forName才能执行静态初始化块

调试


通过JVM参数可以观察类加载细节

  • -XX:+TraceClassLoading
  • -XX:TraceClassUnloading

自定义加载器


自定义应用场景

  • 隔离影响
    可能引入不同版本同名类,隔离确保框架的依赖不会受到应用依赖的影响

  • 延迟加载
    不必要的类库后期加载

  • 扩展来源
    从外部来源加载,比如网络、数据库

  • 加解密
    防止代码泄露

  • 操作字节码
    动态修改

卸载


条件

  • 类所有实例已经回收
  • 类对应的Class对象无引用
  • 类加载器被回收

NoClassDefFoundError和ClassNotFoundException


两者都是找不到类,但有所区别

  • NoClassDefFoundError: 编译时存在,但在运行时找不到,是一种不该出现的错误
  • ClassNotFoundException:运行时动态加载找不到类,是一种可补救的异常

NoSuchMethodError调试


当有两个版本jar并存,一个方法只存在一个版本,加载错误就会找不到方法
可以通过Class类getProtectionDomain方法输出加载类是从哪个文件加载的

Sample
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
public class ClassLocationUtils {
public static String where(final Class cls) {
if (cls == null)
throw new IllegalArgumentException("null input: cls");
URL result = null;
final String clsAsResource = cls.getName().replace('.', '/').concat(".class");
final ProtectionDomain pd = cls.getProtectionDomain();
if (pd != null) {
final CodeSource cs = pd.getCodeSource();
if (cs != null) {
result = cs.getLocation();
}
if (result != null) {
if ("file".equals(result.getProtocol())) {
try {
if (result.toExternalForm().endsWith(".jar")
|| result.toExternalForm().endsWith(".zip"))
result = new URL("jar:".concat(result.toExternalForm()).concat("!/").concat(clsAsResource));
else if (new File(result.getFile()).isDirectory())
result = new URL(result, clsAsResource);
} catch (MalformedURLException ignore) {
}
}
}
}
if (result == null) {
final ClassLoader clsLoader = cls.getClassLoader();
result = clsLoader != null ? clsLoader.getResource(clsAsResource) : ClassLoader.getSystemResource(clsAsResource);
}
return result.toString();
}
}