on
java类加载流程,类加载机制及自定义类加载器
引言
当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。
类的加载、链接、初始化
加载
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。
加载的class来源
- 从本地文件系统内加载class文件
- 从JAR包加载class文件
- 通过网络加载class文件
- 把一个java源文件动态编译,并执行加载。
链接
通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。
-
验证
验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致。即是否满足java虚拟机的约束。
-
准备
类准备阶段负责为类的类变量分配内存,并设置默认初始值。
-
解析
我们知道,引用其实对应于内存地址。思考这样一个问题,在编写代码时,使用引用,方法时,类知道这些引用方法的内存地址吗?显然是不知道的,因为类还未被加载到虚拟机中,你无法获得这些地址。
举例来说,对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。
解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。
初始化
类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用**< clinit>**方法,进行类变量的初始化。java类中对类变量进行初始化的两种方式:
- 在定义时初始化
- 在静态初始化块内初始化
clinit相关
虚拟机会收集类及父类中的类变量及类方法组合为< clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的< clinit>方法先执行完毕。因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是java.lang.Object方法。
public class Test {
static int A = 10;
static {
A = 20;
}
}
class Test1 extends Test {
private static int B = A;
public static void main(String[] args) {
System.out.println(Test1.B);
}
}
//输出结果
//20
从输出中看出,父类的静态初始化块在子类静态变量初始化之前初始化完毕,所以输出结果是20,不是10。
如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成< clinit>方法。
接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。
public interface InterfaceInitTest {
long A = CurrentTime.getTime();
}
interface InterfaceInitTest1 extends InterfaceInitTest {
int B = 100;
}
class InterfaceInitTestImpl implements InterfaceInitTest1 {
public static void main(String[] args) {
System.out.println(InterfaceInitTestImpl.B);
System.out.println("---------------------------");
System.out.println("当前时间:"+InterfaceInitTestImpl.A);
}
}
class CurrentTime {
static long getTime() {
System.out.println("加载了InterfaceInitTest接口");
return System.currentTimeMillis();
}
}
//输出结果
//100
//---------------------------
//加载了InterfaceInitTest接口
//当前时间:1560158880660
从输出验证了:对于接口,只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。
虚拟机会保证一个类的< clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit>方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。
public class MultiThreadInitTest {
static int A = 10;
static {
System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread() + "start");
System.out.println(MultiThreadInitTest.A);
System.out.println(Thread.currentThread() + "run over");
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
//输出结果
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over
从输出中看出验证了:只有第一个线程对MultiThreadInitTest进行了一次初始化,第二个线程一直阻塞等待等第一个线程初始化完毕。
初始化时机
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
- 当遇到调用静态方法或者使用静态变量,初始化静态变量或方法所在的类;
- 子类初始化过程会触发父类初始化;
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口初始化;
- 使用反射API对某个类进行反射调用时,初始化这个类;
- Class.forName()会触发类的初始化
final定义的初始化
注意:对于一个使用final定义的常量,如果在编译时就已经确定了值,在引用时不会触发初始化,因为在编译的时候就已经确定下来,就是“宏变量”。如果在编译时无法确定,在初次使用才会导致初始化。
public class StaticInnerSingleton {
/**
* 使用静态内部类实现单例:
* 1:线程安全
* 2:懒加载
* 3:非反序列化安全,即反序列化得到的对象与序列化时的单例对象不是同一个,违反单例原则
*/
private static class LazyHolder {
private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
}
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return LazyHolder.INNER_SINGLETON;
}
}
看这个例子,单例模式静态内部类实现方式。我们可以看到单例实例使用final定义,但在编译时无法确定下来,所以在第一次使用StaticInnerSingleton.getInstance()方法时,才会触发静态内部类的加载,也就是延迟加载。
这里想指出,如果final定义的变量在编译时无法确定,则在使用时还是会进行类的初始化。
ClassLoader只会对类进行加载,不会进行初始化
public class Tester {
static {
System.out.println("Tester类的静态初始化块");
}
}
class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//下面语句仅仅是加载Tester类
classLoader.loadClass("loader.Tester");
System.out.println("系统加载Tester类");
//下面语句才会初始化Tester类
Class.forName("loader.Tester");
}
}
//输出结果
//系统加载Tester类
//Tester类的静态初始化块
从输出证明:ClassLoader只会对类进行加载,不会进行初始化;使用Class.forName()会强制导致类的初始化。
类加载器
类加载器负责将.class文件(不管是jar,还是本地磁盘,还是网络获取等等)加载到内存中,并为之生成对应的java.lang.Class对象。一个类被加载到JVM中,就不会第二次加载了。那怎么判断是同一个类呢?
每个类在JVM中使用全限定类名(包名+类名)与类加载器联合为唯一的ID,所以如果同一个类使用不同的类加载器,可以被加载到虚拟机,但彼此不兼容。
JVM类加载器分类
Bootstrap ClassLoader
Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是通过C++实现的。
public class BootstrapTest {
public static void main(String[] args) {
//获取根类加载器所加载的全部URL数组
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urLs).forEach(System.out::println);
}
}
//输出结果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes
根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。
我们将rt.jar解压,可以看到我们经常使用的类库就在这个jar包中。
Extension ClassLoader
Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。
System ClassLoader
System ClassLoader为系统(应用)类加载器,负责加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。
类加载机制
JVM主要的类加载机制
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显式使用另一个类加载器来载入。
- 父类委托(双亲委派):先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。
- 缓存机制:缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。
注意:类加载器之间的父子关系并不是类继承上的父子关系,而是实例之间的父子关系。
public class ClassloaderPropTest {
public static void main(String[] args) throws IOException {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemClassLoader);
/*
获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定,如果操作系统没有指定
CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径
*/
Enumeration<URL> eml = systemClassLoader.getResources("");
while (eml.hasMoreElements()) {
System.out.println(eml.nextElement());
}
//获取系统类加载器的父类加载器,得到扩展类加载器
ClassLoader extensionLoader = systemClassLoader.getParent();
System.out.println("系统类的父加载器是扩展类加载器:" + extensionLoader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parant:" + extensionLoader.getParent());
}
}
//输出结果
//系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系统类的父加载器是扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
//扩展类加载器的加载路径:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
//扩展类加载器的parant:null
从输出中验证了:系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null,这是因为父加载器不是java实现的,是C++实现的,所以获取不到。但扩展类加载器的父加载器是根加载器。
类加载流程图
图中红色部分,可以是我们自定义实现的类加载器来进行加载。
创建并使用自定义类加载器
自定义类加载分析
除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。
ClassLoader类有两个关键的方法:
- protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。
- protected Class findClass(String name) :根据指定类名来查找类。
所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部会调用findClass方法。
我们来看一下loadClass的源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//第一步,先从缓存里查看是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//第二步,判断父加载器是否为null
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) {
//第三步,如果前面都没有找到,就会调用findClass方法
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;
}
}
loadClass加载方法流程:
- 判断此类是否已经加载;
- 如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;
- 如果前面都没加载成功,则使用findClass方法进行加载。
所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。
实现自定义类加载器
基于以上分析,我们简单重写findClass方法进行自定义类加载。
public class Hello {
public void test(String str){
System.out.println(str);
}
}
public class MyClassloader extends ClassLoader {
/**
* 读取文件内容
*
* @param fileName 文件名
* @return
*/
private byte[] getBytes(String fileName) throws IOException {
File file = new File(fileName);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fin = new FileInputStream(file)) {
//一次性读取Class文件的全部二进制数据
int read = fin.read(raw);
if (read != len) {
throw new IOException("无法读取全部文件");
}
return raw;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//将包路径的(.)替换为斜线(/)
String fileStub = name.replace(".", "/");
String classFileName = fileStub + ".class";
File classFile = new File(classFileName);
//如果Class文件存在,系统负责将该文件转换为Class对象
if (classFile.exists()) {
try {
//将Class文件的二进制数据读入数组
byte[] raw = getBytes(classFileName);
//调用ClassLoader的defineClass方法将二进制数据转换为Class对象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException e) {
e.printStackTrace();
}
}
//如果clazz为null,表明加载失败,抛出异常
if (null == clazz) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public static void main(String[] args) throws Exception {
String classPath = "loader.Hello";
MyClassloader myClassloader = new MyClassloader();
Class<?> aClass = myClassloader.loadClass(classPath);
Method main = aClass.getMethod("test", String.class);
System.out.println(main);
main.invoke(aClass.newInstance(), "Hello World");
}
}
//输出结果
//Hello World
ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。
此例子很简单,写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。
双亲委派破坏
在java发展过程中,经历了3次"被破坏"的情况
-
在双亲委派模型发布之前,即JDK1.2之前。为了兼容之前JDK版本中自定义类加载器的实现。(即没有按照双亲委派模型来设计)
解决办法:把自己的类加载器逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己写的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
-
自身的缺陷所致。JNDI服务需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI)的代码,但是启动类加载器不认识这些代码。
解决办法:引入上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的**setContextClassLoaser()**方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围都没有设置过的话,那这个类加载器默认就是应用程序的类加载了。类似的服务还有:JDBC、JCE、JAXB、JBI等等。
-
用户对动态性的追求而导致的,例如:代码热替换、热部署
解决办法:OSGI实现模块化热部署的关键是它自定义的类加载机制实现的。OSGi每个模块都有自己独立的classpath。如何实现这一点呢?是因为OSGi采取了不同的类加载机制:
1、OSGi为每个bundle提供一个类加载器,该加载器能够看到bundle Jar文件内部的类和资源; 2、为了让bundle能互相协作,可以基于依赖关系,从一个bundle类加载器委托到另一个bundle类加载器。 3、当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热部署。
破坏举例
原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库提供的驱动去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
需要保留双亲委派模型:extends ClassLoader,重写 findClass() 破坏双亲委派模型:直接重写 loadClass()
如何实现动态加载
根据classloader机制,实现动态加载需要实现以下步骤
-
首先实现一个自定义的classloader,必须要重写loadClass方法,因为classloader默认会对已经load过的class进行缓存,已经加载过的class不会进行第二次加载;
-
创建实例
另外一个问题是,每个被加载的class都会进行链接(link),这是通过执行ClassLoader.resolve()来实现的,这个方法是 final的,因此无法重写。Resove()方法不允许一个ClassLoader实例link一个Class两次,因此,当你需要重新加载一个 Class的时候,你需要重新New一个你自己的ClassLoader实例。 刚才说到一个Class不能被一个ClassLoader实例加载两次,但是可以被不同的ClassLoader实例加载,这会带来新的问题:
NewObject object = (NewObject) classReloadingFactory.newInstance("com.sample.NewObject");这段代码会导致一个ClassCastException,因为在一个Java应用中,class是根据它的全名(包名+类名)和加载它的 ClassLoader来唯一标识的。在上面的代码中object对象对应的class和newInstance返回的实例对应的Class是有区别的:
全名 ClassLoader实例 Object对象的Class com.sample.NewObject AppClassLoader实例 newInstance返回对象的Class com.sample.NewObject 自定义ClassLoader实例 解决的办法是使用接口或者父类,只重新加载实现类或者子类即可。
NewObjectInterface object = (NewObjectInterface) classReloadingFactory.newInstance("com.sample.NewObject"); NewObjectSuperclass object = ( NewObjectSuperclass) classReloadingFactory.newInstance("com.sample.NewObject");