0%

类加载机制

我们知道一个类创建的过程是: 首先是开辟空间,其次是初始化和指向空间地址,后面两个顺序可能不固定. 在此之前,JVM需要加载类文件到JVM,如果不加载那怎么知道该分配多大的存储空间呢,今天我们就来捋捋这个过程.

conan 父母

这次柯南要委托他爹妈了.

类加载的过程

JVM 加载类的机制有点类似懒加载, 只有在类用到了的时候才会进行加载.

一个类加载进入虚拟机到能够给调用者使用的过程大概是这样的:

  • 1:加载, 将Class文件加载进JVM.
  • 2:验证, 验证Class文件的正确性.
  • 3:准备, 给类的静态变量分配内存,并赋默认值.
  • 4:解析, 将类的静态方法根据JVM指令替换成直接引用.
  • 5:初始化, 类的静态变量初始化 和 执行静态代码块.

类经过上面5个过程,就加载进JVM了, 我们就已经可以使用其静态变量 以及 静态方法了. 如果需要使用其实例对象,那就在执行new 的过程,就是前面说的,开辟空间,初始化然后变量执行该地址.

加载

JVM 中有专门负责加载类的对象ClassLoader 类加载器.

类加载器可以分为4大类:

  • bootstrap class loader 引导类加载器
  • extension class loader 扩展类加载器
  • application class loader 应用类加载器
  • 自定义ClassLoader 自定义类加载器

bootstrap class loader 引导类加载器: 负责加载jdk核心类库,比如:

rt.jar , jce.jar , charsets.jar,jsse.jar 等

extension class loader 扩展类加载器: 负责加载jdk扩展类库(ext目录下的类库),比如:

cldrdata,dnsns,jaccess,jfxrt,localedata,nashorn,sunec,sunjce_provider,sunpkcs11,zipfs

application class loader 应用类加载器:负责加载classPath路径下的类文件.比如自己写的代码,第三方类库.

就像我们在记事本里面编程一样,我们通过 java命令将.java文件编译成.class 文件, 在通过javac 执行. java javac 都是 c命令. 类的加载底层其实也是c去负责发起加载. C++ 发起加载到 最终加载到class类对象.

过程大概如下:

cld1

4种类加载器之间的关系

顺着调用的顺序,我们来看下Launcher类;

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
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;

public static Launcher getLauncher() {
return launcher;
}

public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}

try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...

}

public ClassLoader getClassLoader() {
return this.loader;
}

bootstrap class loader 引导类加载器

bootstrap class loader 负责加载jdk核心类库,如果我们通过下面代码来看看

1
2
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(" classLoader = " + classLoader);

执行结果发现是

1
2
classLoader = null

会不会感觉很意外? 因为bootstrap class loader 是C层面的,所以java中获取bootstrap class loader 时返回的是null.

extension class loader 扩展类加载器

我们先来看下ExtClassLoader的依赖关系.

class

ExtClassLoaderAppClassLoader继承了URLClassLoader 都是ClassLoader的子类.ClassLoader类中维护了一个private final ClassLoader parent;字段记录其层级关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {

//...
return new Launcher.ExtClassLoader(var0);

}

}

public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}


Launcher类的构造方法可以看到,最终调用 new Launcher.ExtClassLoader(var0) 其层级关系的parent传的是null,也就是类似我们获取不到的bootstrap class loader 引导类加载器.

extension class loader 扩展类加载器层级关系的上级是 bootstrap class loader 引导类加载器

application class loader 应用类加载器

Launcher类的构造方法中可以看到,AppClassLoaderExtClassLoader作为上级传入其构造.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
return new Launcher.AppClassLoader(var1x, var0);
}

AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}

}

到这里,我们可以认定,引导类加载器,扩展类加载器,应用类加载器的关系如下:

cld3

自定义ClassLoader 自定义类加载器

我们自己也可以定义类加载器,实现URLClassLoader / ClassLoader自己定制加载类的逻辑.

如果是自定义的类加载器,其层级关系又该是怎样的呢?

首先我们来看下ClassLoader的构造,ClassLoader有一个无参构造方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}

其中scl = l.getClassLoader(); lLauncher.

getSystemClassLoader()最终获取的类加载器 是 AppClassLoader.

因此加入自定义加载器后,4 种加载器之间的关系应该是这样子的.

cld2

双亲委派机制

类加载的双亲委派机制: 加载某个类,先从子层级委托父层级查找,如果父层级能找到就直接加载,所以父层级都找不到该类,再向下通知子层级,让子层级自己实现加载的机制.

cld4

打个比方来理解:

假如祖孙三代人, 孙子遇见一个问题, 就问父亲, 如果父亲知道问题就会直接告诉儿子,如果父亲也解决不了, 父亲在去问爷爷,爷爷如果知道就会告诉父亲,如果爷爷也不知道该问题, 爷爷就如实告诉父亲,”我解决不了这个问题, 你自己好好研究研究吧”,然后父亲也回来告诉儿子解决不了. 最终儿子自己想办法解决问题.

下面我们看看源码,看其是怎么实现的.

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

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;
}
}

这里我没有把自定义类加载器放进去,类加载的双亲委派机制大致流程如下:

cld5

jvm 类加载双亲委派机制好处

  • 1 沙箱安全机制.
  • 2 保证类加载的唯一性.
  • 3 加载效率更高.

JVM 采用这种加载机制可以保证沙箱安全机制,以及加载类的唯一性.

沙箱安全机制: jdk提供的核心类库,保证核心类库API不会被篡改.

保证类加载的唯一性: 如果父层级类加载器以及加载该类,其子加载器就无需在加载了.

加载效率更高: 一般我们更多的是运行自己的代码而不是jdk的代码,通过这种自下而上的加载方式, 根据局部性原理,一个类用到了,说明后面很有可能还会被用到, 虽然加载自己的类每次都会绕一圈,但是如果加载过一次,后面就可以自己从 findLoadedClass方法中获取到. 如果不是使用这种方式,每次都得全局查找一次.

破坏双亲委派机制

破坏双亲委派机制 其实就是破坏这种自下而上的调用方式,中断调用parent.loadClass( ) 方法. 我们可以通过自定义加载器来达到这种效果.

自定义类加载器

项目中有两个一模一样的User 只是两个路径不一样, 根据前面所说的,双亲委派机制不会重复加载的特性, 下面打印出来的类加载器信息,应该都是AppClassLoader,通过实现自定义加载器,实现不同的类用不同的类加载器就可以轻松实现同时加载进两个不同版本的User 类进JVM中.

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

public static void main(String[] args) throws Exception {

System.out.println("========");
User user = new User();
user.print();
System.out.println("========" + user.getClass().getClassLoader());
MyClassLoader loader = new MyClassLoader("/Users/zhangwenhao/Desktop/test");
Class<?> aClass = loader.loadClass("com.iz.study.pojo.User");
Object o = aClass.newInstance();
Method print = aClass.getMethod("print", null);
print.invoke(o, null);
System.out.println("========" + aClass.getClassLoader());
}

static class MyClassLoader extends ClassLoader {

private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
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();

if (!name.contains("com.iz.study")) {
c = this.getParent().loadClass(name);
}

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;
}
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

try {
byte[] bytes = loadClassByte(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}

return super.findClass(name);
}

private byte[] loadClassByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream inputStream = new FileInputStream(classPath + "/" + name + ".class");
int available = inputStream.available();
byte[] bytes = new byte[available];
inputStream.read(bytes);
inputStream.close();
return bytes;
}
}

Tomcat 底层也是用的类似这种逻辑实现,即使不同war用的相同类库不同版本也一样可以正常运行.