本篇是最近在学习JVM相关知识时的随笔,虽然我们现在大都通过Spring容器进行对象的实例化,但本身应当了解对象的各类创建方式,以及最基本的Class文件如何通过何种方式加载到内存中生成对应的对象的。
类加载的各种几种方式和区别
- new
- clazz.newInstance
- constructor.newInstance
- clone
- 反序列化
- ClassLoader
总结起来有对象的实例化方式分为:
new关键字进行实例化、反射机制进行实例化、克隆对象实例化、反序列化方式实例化,以及以上几种方式的最基本的类加载器加载对象。
注:clazz.newInstance在JDK1.9之后变为了Deprecated,不推荐使用。
clazz.getDeclearedConstructor().newInstance(),通过构造器类进行实例化。
ClassLoader
我们都知道JAVA程序是运行在JVM中,我们编写的.java文件编译后生成.class文件,而该文件用于描述类的数据结构,以及通过CLASSPATH加载其他相关类的支持。而.class文件加载到JVM内存中这个过程,就是由ClassLoader完成的。
类加载器主要分为两类,一类是JDK默认提供的,一类是用户自定义的。
JDK1.8以前:
JDK1.8之后:
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 | public static void main(String[] args) { |
java.security.SecureClassLoader
增加了一层权限验证,因为关注点不在安全,所以暂不讨论。
java.net.URLClassLoader
该类加载器用来加载URL指定的JAR文件或目录中的类和资源,以/结尾的URL认为是目录,否则认为是JAR文件。
1 | // 尝试通过 URLClassLoader 来加载桌面下的 Test 类。 |
破坏双亲委派机制
可以看出双亲委派机制是一种至下而上的加载方式,那么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()来加载实现类,实现在核心包里的基础类调用用户代码