类的生命周期可以划分为 7 个阶段
其中,第 1~5 阶段,即加载、验证、准备、解析、初始化,统称为「类加载」,如下图所示。
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class
对象,这个 Class
对象就是这个类各种数据的访问入口。
该过程可以总结为「JVM 加载 Class
字节码文件到内存中,并在方法区创建对应的 Class
对象」。
当 JVM 加载完 Class
字节码文件,并在方法区创建对应的 Class
对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。
这个校验过程,大致可以分为下面几个类型
0x cafe babe
int
类型的参数,但是使用它的时候却传入了一个 String
类型的参数。准备阶段中,JVM 将为类变量分配内存并初始化。
准备阶段,有两个关键点需要注意
内存分配的对象
Java 中的变量有「类变量」和「类成员变量」两种类型。「类变量」指的是被 static
修饰的变量,而其他所有类型的变量都属于「类成员变量」。 在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
public static int factor = 3; public String website = "www.google.com"; 复制代码
如上代码,在准备阶段,只会为 factor
变量分配内存,而不会为 website
变量分配内存。
初始化的类型
在准备阶段,JVM 会为「类变量」分配内存并为其初始化。这里的「初始化」指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
public static int sector = 3; 复制代码
如上代码,在准备阶段后, sector
的值将是 0,而不是 3。
如果一个变量是常量(被 static final
修饰)的话,那么在准备阶段,变量便会被赋予用户希望的值。 final
关键字用在变量上,表示该变量不可变,一旦赋值就无法改变。所以,在准备阶段中,对类变量初始化赋值时,会直接赋予用户希望的值。
public static final int number = 3; 复制代码
如上代码,在准备阶段后, number
的值将是 3,而不是 0。
解析过程中,JVM 针对「类或接口」、「字段」、「类方法」、「接口方法」、「方法类型」、「方法句柄」、「调用点限定符」这 7 类引用进行解析。解析过程的主要任务是将其在常量池中的符号引用,替换成其在内存中的直接引用。
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。
一般来说,当 JVM 遇到下面 5 种情况的时候会触发初始化
new
、 getstatic
、 putstatic
、 invokestatic
这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
new
关键字实例化对象的时候、读取或设置一个类的静态字段(被 final
修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。main()
方法的那个类),虚拟机会先初始化这个主类。java.lang.invoke.MethodHandle
实例最后的解析结果是 REF_getstatic
、 REF_putstatic
、 REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,则需要先出触发其初始化。当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class
对象,最后负责运行的 JVM 也退出内存。
下面,将通过几个案例,对类加载的 5 个阶段加深理解。
public class Book { public static void main(String[] args) { System.out.println("Hello Liu Baoshuai"); } Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } { System.out.println("书的普通代码块"); } int price = 110; static{ System.out.println("书的静态代码块"); } static int amount = 112; } 复制代码
运行上述代码,输出信息如下。
书的静态代码块 Hello Liu Baoshuai 复制代码
下面对输出结果进行分析。
根据「类的生命周期和加载过程 / 5.初始化」章节中提到的「当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()
方法的那个类),虚拟机会先初始化这个主类」可知,我们将会进行类的初始化。
Java 源代码中有构造方法这个概念。但编译为字节码后,是没有构造方法这个概念的,只有「类初始化方法」和「对象初始化方法」。
上面的例子中,其类初始化方法如下。
static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
上面的例子中,其对象初始化方法如下。
{ System.out.println("书的普通代码块"); } int price = 110; //注意,构造函数的代码一定是被放在最后的 Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } 复制代码
结合「类初始化方法」和「对象初始化方法」的分析,再回过头看上述例子,就不难得出结论了。
main
方法中,并没有实例化对象,所以只执行「类初始化方法」,如下所示。因此,会输出 书的静态代码块
。static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
main()
方法。因此,会输出 Hello Liu Baoshuai
。案例引申
下面,对上述测试案例进一步引申,修改 main()
方法,代码如下所示。
public class Book { public static void main(String[] args) { System.out.println("Hello Liu Baoshuai" + new Book().price); } Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } { System.out.println("书的普通代码块"); } int price = 110; static{ System.out.println("书的静态代码块"); } static int amount = 112; } 复制代码
运行上述代码,输出信息如下。
书的静态代码块 书的普通代码块 书的构造方法 price=110,amount=112 Hello Liu Baoshuai110 复制代码
下面对输出结果进行分析。
书的静态代码块
static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
main()
方法。遇到了 new Book()
语句,所以触发执行「对象初始化方法」,如下所示。// part 1 { System.out.println("书的普通代码块"); } // part 2 int price = 110; // part 3 Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } 复制代码
part 1
和 part 2
的先后顺序,是根据它们在代码中出现的顺序决定的。 part 3
部分是构造函数部分,这部分永远是出现最后的,和它在代码中的顺序无关。在代码中, part 3
部分虽然出现在 part 1
和 part 2
的前面,但在「对象初始化方法」中,它永远是出现在最后的。part 2
出现在 part 3
前面,所以输出 price
的值是 110,而不是 0。class Grandpa { static { System.out.println("爷爷在静态代码块"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public static int factor = 25; public Father() { System.out.println("我是爸爸~"); } } class Son extends Father { static { System.out.println("儿子在静态代码块"); } public Son() { System.out.println("我是儿子~"); } } public class InitializationDemo { public static void main(String[] args) { System.out.println("爸爸的岁数:" + Son.factor); //入口 } } 复制代码
运行上述代码,输出信息如下。
爷爷在静态代码块 爸爸在静态代码块 爸爸的岁数:25 复制代码
下面对输出结果进行分析。
main
方法中,并没有实例化对象,所以只执行「类初始化方法」,不会执行「对象初始化方法」。Son
初始化时,会先进行父类 Father
的初始化。同理,进行 Father
初始化时,会先进行父类 Grandpa
的初始化。所以,程序会输出如下信息。爷爷在静态代码块 爸爸在静态代码块 复制代码
main()
方法中的 System.out.println
语句,程序会输出 爸爸的岁数:25
。也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?这是因为对于静态字段,只有直接定义这个字段的类才会被初始化,才会执行该类的「类初始化方法」。因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
class Grandpa { static { System.out.println("爷爷在静态代码块"); } public Grandpa() { System.out.println("我是爷爷~"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public Father() { System.out.println("我是爸爸~"); } } class Son extends Father { static { System.out.println("儿子在静态代码块"); } public Son() { System.out.println("我是儿子~"); } } public class InitializationDemo { public static void main(String[] args) { new Son(); //入口 } } 复制代码
运行上述代码,输出信息如下。
爷爷在静态代码块 爸爸在静态代码块 儿子在静态代码块 我是爷爷~ 我是爸爸~ 我是儿子~ 复制代码
下面对输出结果进行分析。
Son
初始化时,会先进行父类 Father
的初始化。同理,进行 Father
初始化时,会先进行父类 Grandpa
的初始化。所以,程序会输出如下信息。爷爷在静态代码块 爸爸在静态代码块 儿子在静态代码块 复制代码
main()
方法的 new Son()
语句将触发实例化对象,调用 Son
的构造函数,调用子类的构造函数时会先调用父类的构造函数。所以,程序会输出如下信息。我是爷爷~ 我是爸爸~ 我是儿子~ 复制代码
public class Book { public static void main(String[] args) { staticFunction(); } static Book book = new Book(); //注意该语句 static { System.out.println("书的静态代码块"); } { System.out.println("书的普通代码块"); } Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } public static void staticFunction(){ System.out.println("书的静态方法"); } int price = 110; static int amount = 112; } 复制代码
运行上述代码,输出信息如下。
书的普通代码块 书的构造方法 price=110,amount=0 书的静态代码块 书的静态方法 复制代码
下面对输出结果进行分析。
book
实例变量被初始化为 null
, amount
变量被初始化为 0。Book
类的 main()
方法是程序的入口,所以 JVM 会初始化 Book
类,执行「类初始化方法」,如下所示。static Book book = new Book(); //注意该语句 static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
static Book book = new Book()
。这条语句又触发了类的实例化,所以会执行「对象初始化方法」,如下所示。// part 1 { System.out.println("书的普通代码块"); } // part 2 int price = 110; // part 3 Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } 复制代码
part 1
和 part 2
的先后顺序,是根据它们在代码中出现的顺序决定的。 part 3
部分是构造函数部分,这部分永远是出现最后的,和它在代码中的顺序无关。在代码中, part 3
部分虽然出现在 part 2
的前面,但在「对象初始化方法」中,它永远是出现在最后的。part 2
出现在 part 3
前面,所以输出 price
的值是 110,而不是 0。书的普通代码块 书的构造方法 price=110,amount=0 复制代码
static Book book = new Book()
语句后,回到步骤 2 中,执行「类初始化方法」。此时,程序会输出 书的静态代码块
。main()
方法的 staticFunction();
语句。此时,程序会输出 书的静态方法
。当编译器将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行等整个过程。我们将这个过程称为 Java 虚拟机的「类加载机制」。
「类加载机制」中,通过类加载器( classloader
)来完成类加载的过程。
什么是类加载器
通过一个类全限定名称来获取其二进制文件( .class
)流的工具,被称为类加载器( classloader
)。
Java支持的4种classloader
如上图所示,Java 支持 4 种 classloader
Bootstrap ClassLoader
)
Bootstrap ClassLoader
的 parent
属性为 null
Extention ClassLoader
)
sun.misc.Launcher$ExtClassLoader
实现JAVA_HOME
下 libext
目录下的或者被 java.ext.dirs
系统变量所指定的路径中的所有类库Application ClassLoader
) sun.misc.Launcher$AppClassLoader
User ClassLoader
)
java.lang.ClassLoader
类。如果不想打破双亲委派模型,那么只需要重写 findClass
方法即可;如果想打破双亲委派模型,则需要重写 loadClass
方法前 3 种 classloader
均继承了抽象类 ClassLoader
,其源码如下,该抽象类拥有一个 parent
属性,用于指定其父类的加载器。
public abstract class ClassLoader { private static native void registerNatives(); static { registerNatives(); } // The parent class loader for delegation // Note: VM hardcoded the offset of this field, thus all new fields // must be added *after* it. private final ClassLoader parent; protected Class> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } // ... protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } 复制代码
可以通过下面这种方式,打印加载路径及相关 jar。
System.out.println("boot:" + System.getProperty("sun.boot.class.path")); System.out.println("ext:" + System.getProperty("java.ext.dirs")); System.out.println("app:" + System.getProperty("java.class.path")); 复制代码
自定义类加载器
此处给出一个自定义类加载器示例。
package com.lbs0912.java.demo; import java.io.IOException; import java.io.InputStream; public class ConsumerClassLoaderDemo extends ClassLoader { public static void main(String[] args) throws Exception { ClassLoader myClassLoader = new ConsumerClassLoader(); Object obj = myClassLoader.loadClass("com.lbs0912.java.demo.ConsumerClassLoaderDemo").newInstance(); ClassLoader classLoader = obj.getClass().getClassLoader(); // BootStrapClassLoader在Java中不存在的,因此会是null while (null != classLoader) { System.out.println(classLoader); classLoader = classLoader.getParent(); } } } class ConsumerClassLoader extends ClassLoader { @Override public Class> loadClass(String name) throws ClassNotFoundException { try { String classFile = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream in = getClass().getResourceAsStream(classFile); if (null == in) { return super.loadClass(name); } byte[] bytes = new byte[in.available()]; in.read(bytes); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } } 复制代码
控制台输入如下
com.lbs0912.java.demo.ConsumerClassLoader@266474c2 sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@63947c6b 复制代码
Java 9 中类加载器的变化
「类加载机制」中,通过「类加载器( classloader
)」来完成类加载的过程。Java 中的类加载机制,有如下 3 个特点
下面对「双亲委派」进行说明。
JVM 中,类加载器默认使用双亲委派原则。
双亲委派机制是一种任务委派模式,是 Java 中通过加载工具( classloader
)加载类文件的一种具体方式。 具体表现为
BootstrapClassLoader AppClassLoader
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself. —— Oracel Document Java 平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。
参考上述 Oracle 官网文档描述,Java 的类加载机制,更准确的说,应该叫做 “父委派模型”。但由于翻译问题,被称为了 “双亲委派机制”。参考 Java类加载机制-双亲委派机制还是应该叫做“父委派模型” | CSDN 了解更多。
classloader
类存在一个 parent
属性,可以配置双亲属性。默认情况下,JDK 中设置如下。
ExtClassLoader.parent=null; AppClassLoader.parent=ExtClassLoader //自定义 XxxClassLoader.parent=AppClassLoader 复制代码
需要注意的是,启动类加载器( BootstrapClassLoader
)不是一个 Java 类,它是由底层的 C++ 实现,因此启动类加载器不属于 Java 类库,无法被 Java 程序直接引用,所以 ExtClassLoader.parent=null;
。
双亲设置之后,便可以委派了。委派过程也就是类文件加载过程。
ClassLoader
里面有 3 个重要的方法,即
loadClass() findClass() defineClass()
实现双亲委派的代码都集中在 java.lang.ClassLoader
的 loadClass()
方法中。
public abstract class ClassLoader { // 委派的父类加载器 private final ClassLoader 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) { try { if (parent != null) { //父类加载器不为空,则用该父类加载器 c = parent.loadClass(name, false); } else { //若父类加载器为空,则使用启动类加载器作为父类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //若父类加载器抛出ClassNotFoundException , //则说明父类加载器无法完成加载请求 } if (c == null) { //父类加载器无法完成加载请求时 //调用自身的findClass()方法进行类加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } protected Class> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } } 复制代码
上述代码的主要步骤如下
loadClass()
方法进行加载ClassNotFoundException
异常后,再调用自己的 findClass()
方法进行加载此处给出一个加载时序图,加深理解。
ClassLoader
中和类加载有关的方法有很多,前面提到了 loadClass()
,除此之外,还有 findClass()
和 defineClass()
等。这3个方法的区别如下
loadClass()
:默认的双亲委派机制在此方法中实现findClass()
:根据名称或位置加载 .class
字节码definclass()
:把 .class
字节码转化为 Class
对象避免类的重复加载
通过委派的方式,可以避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
保证安全性
通过双亲委派的方式,可以保证安全性 。因为 BootstrapClassLoader
在加载的时候,只会加载 JAVA_HOME
中的 jar 包里面的类,如 java.lang.String
,那么这个类是不会被随意替换的,除非有人跑到你的机器上,破坏你的 JDK。
在双亲委派中,子类加载器可以使用父类加载器已经加载过的类,但是父类加载器无法使用子类加载器加载过的类(类似继承的关系)。
Java 提供了很多服务提供者接口(SPI, Service Provider Interface
),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC。这些 SPI 的接口由 Java 核心类提供,实现者确是第三方。如果继续沿用双亲委派,就会存在问题,提供者由 Bootstrap ClassLoader 加载,而实现者是由第三方自定义类加载器加载。这个时候,顶层类加载就无法使用子类加载器加载过的类。
要解决上述问题,就需要打破双亲委派原则。
双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的类加载器的实现方式。在一定条件下,为了完成某些操作,可以 “打破” 模型。
打破双亲委派模型的方法主要包括
loadClass()
在双亲委派的过程,都是在 loadClass()
方法中实现的,因此要想要破坏这种机制,可以自定义一个类加载器,继承 ClassLoader
并重写 loadClass()
方法即可,使其不进行双亲委派。
利用线程上下文加载器( Thread Context ClassLoader
)也可以打破双亲委派。
Java 应用上下文加载器默认是使用 AppClassLoader
。若想要在父类加载器使用到子类加载器加载的类,可以使用 Thread.currentThread().getContextClassLoader()
。
比如我们想要加载资源可以使用以下方式。
// 使用线程上下文类加载器加载资源 public static void main(String[] args) throws Exception{ String name = "java/sql/Array.class"; Enumerationurls = Thread.currentThread().getContextClassLoader().getResources(name); while (urls.hasMoreElements()) { URL url = urls.nextElement(); System.out.println(url.toString()); } } 复制代码
//程序输出 jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar!/java/sql/Array.class jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar!/java/sql/Array.class 复制代码
在 Maven 工程中,经常会出现依赖冲突,抛出 NoSuchMethodException
异常。如下图所示,业务依赖了消息中间件和微服务中间件,每个模块依赖的 fastjson
版本各不相同。根据引用路径最短原则,工程中实际最终引入的 fastjson
版本为 fastjson-1.0
。
因此,在调用 classA
的 method2()
时候,就会抛出 NoSuchMethodException
异常。
此处介绍一下阿里的潘多拉( pandora
) 是如何解决依赖冲突的。潘多拉中,通过自定义类加载器,为每个中间件自定义一个加载器,这些加载器之间的关系是平行的,彼此没有依赖关系。这样每个中间件的 classloader
就可以加载各自版本的 fastjson
。
一个类的全限定名以及加载该类的加载器,两者共同形成了这个类在 JVM 中的惟一标识,这也是阿里潘多拉实现依赖隔离的基础。
可能到这里,你又会有新的疑惑,根据双亲委托模型, App Classloader
分别继承了 Custom Classloader
,那么业务包中的 fastjson
的 class
在加载的时候,会先委托到 Custom ClassLoader
,这样不就会导致自身依赖的 fastjson
版本被忽略吗?确实如此,所以潘多拉又是如何做的呢?
如上图所示
ModuleClassLoader
在加载对应的 class
文件的同时,根据中间件配置的 export.index
信息,将要需要透出的 class
(主要是提供 API 接口的相关类)索引到 exportedClassHashMap
中exportedClassHashMap
loadClass
的时候,会优先判断 exportedClassHashMap
是否存在当前类。如果存在,则直接返回;如果不存在,则再使用传统的双亲委托机制来进行类加载。MoudleClassloader
不仅实现了中间件的加载,也实现了中间件关键服务类的透出。上述过程对应代码如下。
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { //导出类中是否存在 若存在则直接返回 if(classCache != null && classCache.containsKey(name)){ return classCache.get(name); } //双亲委托加载机制 synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class> c = findLoadedClass(name); if (c == null) { 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. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } 复制代码
我们知道,Tomcat 是一个 web 容器,那么一个 web 容器可能需要部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖 hollis.jar
,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class
。
如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
所以,Tomcat 破坏双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader
加载器。 工作流程如下
WebAppClassLoader
加载器,负责加载应用自身目录下的 class
文件,从而实现隔离。CommonClassLoader
进行加载。通过将一个模块和该模块的类加载器的替换,可以实现热加载。
结合下图,介绍下 Spring 官方推荐的热加载方案 —— Spring boot devtools。
RestartClassLoader
为自定义的类加载器,其核心是 loadClass
的加载方式。Spring boot devtools 中修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,则从 parent 进行加载。 这样保证了业务代码可以优先被 RestartClassLoader
加载,进而通过重新加载 RestartClassLoader
完成应用代码部分的重新加载。
上述过程对应代码如下。
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { String path = name.replace('.','/').concat(".class"); ClassLoaderFile file = this.updatedFiles.getFile(path); if(file != null && file.getKind() == Kind.DELETED){ throw new ClassNotFoundException(name); } //双亲委托加载机制 synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class> loadedClass = findLoadedClass(name); if (c == null) { try { //优先从自己加载(编译生成的target/classes目录) loadedClass = findClass(name); } catch (ClassNotFoundException e) { //如果没有加载到 则从父类加载 loadedClass = Class.forName(name,false,getParent()); } } if (resolve) { resolveClass(loadedClass); } return loadedClass; } } 复制代码
热部署原理大体同热加载,如上图所示,将每个业务方通过一个 classloader
来加载。基于「类的隔离机制」,可以保障各个业务方的代码不会相互影响,同时也可以做到各个业务方进行独立的发布。
出于技术保护或安全的目的,存在对 jar 包进行加密保护的诉求。
对 jar 包进行加密,本质上还是对字节码文件的操作。加密前后,不能影响 class
文件的正常加载过程,因此,加密保护步骤可划分为
class
进行正向的加密操作class
文件之前通过自定义 classloader
先进行反向的解密操作class
文件标准进行加载只有在实现了解密方法的 classloader
的加载下,加密的 jar 包才可以被正常加载。上述过程对应代码如下。
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class> clasz = findLoadedClass(name); if(clasz != null){ return clasz; } //提前对class文件进行解密 try{ //读取经过加密的类文件 byte classData[] = util.readFile(name + ".class"); if(classData != null){ byte decryptedClassData[] = cipher.doFinal(classData); //解密 //再把它转换成一个类 clasz = defineClass(name,decryptedClassData,0,decryptedClassData.length); } }catch (FileNotFoundException e){ e.printStackTrace(); } //必须的步骤2: 如果上面没有成功 //尝试用默认的classloader装入它 if(resolve && clasz != null){ clasz = findSystemClass(name); } if (resolve) { resolveClass(clasz); } return clasz; } 复制代码
服务提供接口( SPI
, Service Provider Interface
) 是 JDK 内置的一种「服务提供发现机制」,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件(可通过 SPI 机制实现模块化)。SPI 的整体机制图如下。
Java 的 SPI 机制可以为某个接口寻找服务实现。SPI 机制主要思想是将装配的控制权移到程序之外,在「模块化设计」中这个机制尤其重要,其核心思想就是「解耦」。Java SPI 实际上是 “基于接口的编程 + 策略模式 + 配置文件” 组合实现的动态加载机制。
Java 中使用 SPI 的步骤如下图所示,主要包括 3 步
META-INF/services
目录SPI
Service.load()
获得服务提供接口( SPI
)的所有实现类下面给出一个具体的例子,展示 SPI 的使用。项目结构如下图所示。
ICustomSvc
接口,作为服务提供接口( SPI
)public interface ICustomSvc { String getName(); } 复制代码
CustomSvcOne
、 CustomSvcTwo
。实际应用中,接口实现者为第三方厂商提供。开发者可通过 jar
包导入或 maven
依赖方式集成到自己的工程。public class CustomSvcOne { @Obverride public String getName(){ return "CustomSvcOne"; } } public class CustomSvcTwo { @Obverride public String getName(){ return "CustomSvcTwo"; } } 复制代码
Service.load()
获得服务提供接口( SPI
)的所有实现类。public class CustomTest { public static void main(String[] args){ ServiceLoadersvcs = Service.load(ICustomSvc.class); svcs.forEach(s -> System.out.println(s.getName())); } 复制代码
cbuc.life.spi.service.impl.CustomSvcOne cbuc.life.spi.service.impl.CustomSvcTwo 复制代码
从「SPI 的使用示例」中可知,使用 SPI 时,要通过 Service.load()
获得服务提供接口( SPI
)的所有实现类,得到的是一个 ServiceLoader
类型的数据结构。
不妨看一下 JDK 中 ServiceLoader
方法的具体实现。
//ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者 public final class ServiceLoaderimplements Iterable{ //查找配置文件的目录 private static final String PREFIX = "META-INF/services/"; //表示要被加载的服务的类或接口 private final Classservice; //这个ClassLoader用来定位,加载,实例化服务提供者 private final ClassLoader loader; // 访问控制上下文 private final AccessControlContext acc; // 缓存已经被实例化的服务提供者,按照实例化的顺序存储 private LinkedHashMapproviders = new LinkedHashMap<>(); // 迭代器 private LazyIterator lookupIterator; //重新加载,就相当于重新创建ServiceLoader了,用于新的服务提供者安装到正在运行的Java虚拟机中的情况。 public void reload() { //清空缓存中所有已实例化的服务提供者 providers.clear(); //新建一个迭代器,该迭代器会从头查找和实例化服务提供者 lookupIterator = new LazyIterator(service, loader); } //私有构造器 //使用指定的类加载器和服务创建服务加载器 //如果没有指定类加载器,使用系统类加载器,就是应用类加载器。 private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } //解析失败处理的方法 private static void fail(Class> service, String msg, Throwable cause) throws ServiceConfigurationError { throw new ServiceConfigurationError(service.getName() + ": " + msg, cause); } private static void fail(Class> service, String msg) throws ServiceConfigurationError { throw new ServiceConfigurationError(service.getName() + ": " + msg); } private static void fail(Class> service, URL u, int line, String msg) throws ServiceConfigurationError { fail(service, u + ":" + line + ": " + msg); } //解析服务提供者配置文件中的一行 //首先去掉注释校验,然后保存 //返回下一行行号 //重复的配置项和已经被实例化的配置项不会被保存 private int parseLine(Class> service, URL u, BufferedReader r, int lc, Listnames) throws IOException, ServiceConfigurationError { //读取一行 String ln = r.readLine(); if (ln == null) { return -1; } //#号代表注释行 int ci = ln.indexOf('#'); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); int n = ln.length(); if (n != 0) { if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) fail(service, u, lc, "Illegal configuration-file syntax"); int cp = ln.codePointAt(0); if (!Character.isJavaIdentifierStart(cp)) fail(service, u, lc, "Illegal provider-class name: " + ln); for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) fail(service, u, lc, "Illegal provider-class name: " + ln); } if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln); } return lc + 1; } //解析配置文件,解析指定的url配置文件 //使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去 private Iterator parse(Class> service, URL u) throws ServiceConfigurationError { InputStream in = null; BufferedReader r = null; ArrayList names = new ArrayList<>(); try { in = u.openStream(); r = new BufferedReader(new InputStreamReader(in, "utf-8")); int lc = 1; while ((lc = parseLine(service, u, r, lc, names)) >= 0); } return names.iterator(); } //服务提供者查找的迭代器 private class LazyIterator implements Iterator { Classservice;//服务提供者接口 ClassLoader loader;//类加载器 Enumerationconfigs = null;//保存实现类的url Iterator pending = null;//保存实现类的全名 String nextName = null;//迭代器中下一个实现类的全名 private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class> c = null; try { c = Class.forName(cn, false, loader); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedActionaction = new PrivilegedAction () { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } } //获取迭代器 //返回遍历服务提供者的迭代器 //以懒加载的方式加载可用的服务提供者 //懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成 public Iteratoriterator() { return new Iterator() { //按照实例化顺序返回已经缓存的服务提供者实例 Iterator> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; } //为指定的服务使用指定的类加载器来创建一个ServiceLoader public static ServiceLoaderload(Classservice, ClassLoader loader) { return new ServiceLoader<>(service, loader); } //使用线程上下文的类加载器来创建ServiceLoader public staticServiceLoaderload(Classservice) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } //使用扩展类加载器为指定的服务创建ServiceLoader //只能找到并加载已经安装到当前Java虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略 public staticServiceLoaderloadInstalled(Classservice) { ClassLoader cl = ClassLoader.getSystemClassLoader(); ClassLoader prev = null; while (cl != null) { prev = cl; cl = cl.getParent(); } return ServiceLoader.load(service, prev); } public String toString() { return "java.util.ServiceLoader[" + service.getName() + "]"; } } 复制代码
ServiceLoader
实现了 Iterable
接口,所以它有迭代器的属性。 ServiceLoader
实现了迭代器的 hasNext
和 next
方法。 ServiceLoader
持有了 private LazyIterator lookupIterator;
,这是一个懒加载类型的迭代器 迭代器(懒加载迭代器)。hasNextService()
方法中获取 fullName
时,使用到了前缀 PREFIX
,这个值是 "META-INF/services/"
。所以,在创建配置文件时,其路径是 ClassPath 下的 META-INF/services/
。Class.forName()
加载类对象,并用 newInstance
方法将类实例化,并把实例化后的类缓存到 providers
对象中(其类型为 LinkedHashMap
),最后返回实例对象。ServiceLoader
不是实例化以后,就去读取配置文件中的具体实现并进行实例化,而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用 hasNext
方法的时候会去加载配置文件进行解析,调用 next
方法的时候进行实例化并缓存。reload
方法。SPI 机制应用较为广泛,包括
以「 JDBC DriveManager」为例,简要介绍下 SPI 机制的应用。
java.sql.Driver
。me.cxis.sql.MyDriver
。META-INF/services
目录下定义一个名字为接口全限定名的文件,如 java.sql.Driver
文件。文件内容是具体的实现名字,如 me.cxis.sql.MyDriver
。jar
包进行业务逻辑开发。//获取ServiceLoader ServiceLoaderloadedDrivers = ServiceLoader.load(Driver.class); //获取迭代器 Iterator driversIterator = loadedDrivers.iterator(); //遍历 while(driversIterator.hasNext()) { driversIterator.next(); //可以做具体的业务逻辑 } 复制代码
Iterator
ServiceLoader
类的实例,是不安全的在服务/客户(S/C)系统中
SPI 机制不仅在 JDK 中实现,Spring 及 Dubbo 框架也都有对应的 SPI 机制。
在 Spring Boot 中好多配置和实现都有默认的实现,我们如果想要修改某些配置,我们只需要在配置文件中写上对应的配置,项目应用的便是我们定义的配置内容。这正是通过 SPI 机制实现的。
META-INF/spring.factories
文件中SpringFactoriesLoader
进行加载,扫描每个 jar 包 class-path
目录下的 META-INF/spring.factories
配置文件,然后解析 properties
文件,找到指定名称的配置Java SPI 与 Spring SPI 的区别
ServiceLoader
,而 Spring 使用的是 SpringFactoriesLoader
。META-INF/services/提供方接口全类名
,而 Spring 使用的是 META-INF/spring-factories
。