对象的创建及类的加载机制

本篇是最近在学习JVM相关知识时的随笔,虽然我们现在大都通过Spring容器进行对象的实例化,但本身应当了解对象的各类创建方式,以及最基本的Class文件如何通过何种方式加载到内存中生成对应的对象的。

类加载的各种几种方式和区别

  1. new
  2. clazz.newInstance
  3. constructor.newInstance
  4. clone
  5. 反序列化
  6. ClassLoader

总结起来有对象的实例化方式分为:

new关键字进行实例化、反射机制进行实例化、克隆对象实例化、反序列化方式实例化,以及以上几种方式的最基本的类加载器加载对象。

注:clazz.newInstance在JDK1.9之后变为了Deprecated,不推荐使用。
clazz.getDeclearedConstructor().newInstance(),通过构造器类进行实例化。

ClassLoader

我们都知道JAVA程序是运行在JVM中,我们编写的.java文件编译后生成.class文件,而该文件用于描述类的数据结构,以及通过CLASSPATH加载其他相关类的支持。而.class文件加载到JVM内存中这个过程,就是由ClassLoader完成的。

类加载器主要分为两类,一类是JDK默认提供的,一类是用户自定义的。

  • JDK1.8以前:

    classloader01-photo

  • JDK1.8之后:

    classloader02-photo

JDK 默认提供三种类加载器:

  • Bootstrap ClassLoader 启动类加载器

    每次执行java命令时都会使用该加载器为虚拟机加载核心类。该加载器是由nativecode实现,而不是Java代码,加载类的路径为<JAVA_HOME>/jre/lib。特别的 <JAVA_HOME>/jre/lib/rt.jar中包含了sun.misc.Launcher 类, 而sun.misc.Launcher$ExtClassLoader和sun.misc.Launcher$AppClassLoader都是 sun.misc.Launcher的内部类,所以拓展类加载器和系统类加载器都是由启动类加载器加载的。

  • PlatformClassLoader,平台类加载器;(JDK1.8以前为Extension ClassLoader, 拓展类加载器)

    JDK1.8之前:JDK目录下提供的ext目录,可以直接将需要执行的扩展jar包直接放入运行,但并不提倡使用,因为不安全,现在已经废除。用于加载拓展库中的类。拓展库路径为<JAVA_HOME>/jre/lib/ext/。实现类为sun.misc.Launcher$ExtClassLoader

  • System ClassLoader 系统类加载器:

    用于加载CLASSPATH中的类。实现类为sun.misc.Launcher$AppClassLoader

用户自定义的类加载器

  • Custom ClassLoader, 一般都是java.lang.ClassLoder的子类

    正统的类加载机制是基于双亲委派的,也就是当调用类加载器加载类时,首先将加载任务委派给双亲,若双亲无法加载成功时,自己才进行类加载。

    在实例化一个新的类加载器时,我们可以为其指定一个parent,即双亲,若未显式指定,则System ClassLoader就作为默认双亲。

    具体的说,类加载任务是由ClassLoader的loadClass() 方法来执行的,他会按照以下顺序加载类:

    通过findLoadedClass() 看该类是否已经被加载。该方法为nativecode 实现,若已加载则返回。

    若未加载则委派给双亲,parent.loadClass(),若成功则返回。

    若未成功,则调用 findClass() 方法加载类。java.lang.ClassLoader中该方法只是简单的抛出一个ClassNotFoundException所以,自定义的ClassLoader都需要 Override findClass() 方法。

ClassLoader的API

java.lang.ClassLoader

  • ClassLoader 是一个抽象类。
  • 待加载的类必须用The Java™Language Specification 定义的全类名,全类名的定义请查阅The Form of a Binary。
  • 给定一个全类名,类加载器应该去定位该类所在的位置。通用的策略是将全类名转换为类文件路径,然后通过类文件路径在文件系统中定位。
  • 每一个加载到内存的类都由一个Class对象来表示,每一个Class对象都有一个指向加载该类的类加载器的引用。但是数组的Class对象是由Java运行时环境创建的,通过 Class.getClassLoader()方法返回的是数组元素的类加载器,若数组元素是基本类型,则返回null,若类是由Bootstrap ClassLoader加载的话也是返回null。
  • ClassLoader默认支持并行加载,但是其子类必须调用ClassLoader.registerAsParallelCapable()来启用并行加载
  • 一般来说,JVM从本地文件系统加载类的行为是与平台有关的。
  • defineClass() 方法可以将字节流转换成一个Class对象。然后调用Class.newInstance()来创建类的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
// Object 类在 <java_home>/jre/lib/rt.jar 中,
// 由 Bootstrap ClassLoader 加载,由于该类加载器是由 native code 编写
// 所以输出为 null
Object[] objects = new Object[5];
System.out.println();
System.out.println(objects.getClass().getClassLoader());

// ZipFileAttributes 类在 <java_home>/jre/lib/ext/zipfs.jar 中,
// 由 Extension ClassLoader 加载,
// 输出为 sun.misc.Launcher$ExtClassLoader@4b67cf4d
ZipFileAttributes[] attributes = new ZipFileAttributes[5];
System.out.println();
System.out.println(attributes.getClass().getClassLoader());

// Main 类是自定义的类,
// 默认由 System ClassLoader 加载,
// 输出为 sun.misc.Launcher$AppClassLoader@18b4aac2
Main[] array = new Main[5];
array[0] = new Main();
System.out.println();
System.out.println(array.getClass().getClassLoader());
}

java.security.SecureClassLoader

增加了一层权限验证,因为关注点不在安全,所以暂不讨论。

java.net.URLClassLoader

该类加载器用来加载URL指定的JAR文件或目录中的类和资源,以/结尾的URL认为是目录,否则认为是JAR文件。

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
// 尝试通过 URLClassLoader 来加载桌面下的 Test 类。
public static void main(String[] args) {
try {
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File("/home/chen/Desktop/");
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator))
.toString();
urls[0] = new URL(null, repository, streamHandler);

ClassLoader loader = new URLClassLoader(urls);

Class testClass = loader.loadClass("Test");

// output: java.net.URLClassLoader@7f31245a
System.out.println(testClass.getClassLoader());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

破坏双亲委派机制

可以看出双亲委派机制是一种至下而上的加载方式,那么SPI是如何打破这种关系?

以JDBC加载驱动为例:
在JDBC4.0之后支持SPI方式加载java.sql.Driver的实现类。SPI实现方式为,通过ServiceLoader.load(Driver.class)方法,去各自实现Driver接口的lib的META-INF/services/java.sql.Driver文件里找到实现类的名字,通过Thread.currentThread().getContextClassLoader()类加载器加载实现类并返回实例。

先看下如果不用Thread.currentThread().getContextClassLoader()加载器加载,整个流程会怎么样。

  • 从META-INF/services/java.sql.Driver文件得到实现类名字DriverA
  • Class.forName(“xx.xx.DriverA”)来加载实现类
  • Class.forName()方法默认使用当前类的ClassLoader,JDBC是在DriverManager类里调用Driver的,当前类也就是DriverManager,它的加载器是BootstrapClassLoader。

用BootstrapClassLoader去加载非rt.jar包里的类xx.xx.DriverA,就会找不到。要加载xx.xx.DriverA需要用到AppClassLoader或其他自定义ClassLoader

最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类。这样就出现了一个问题:如何在父加载器加载的类中,去调用子加载器去加载类?

jdk提供了两种方式,

  • Thread.currentThread().getContextClassLoader()
  • ClassLoader.getSystemClassLoader()
    一般都指向AppClassLoader,他们能加载classpath中的类

SPI则用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码

参考资料

投食入口