类的加载过程
本节参考了 Oracle 的 jvms: Chapter 5. Loading, Linking, and Initializing:
Java虚拟机动态加载、链接和初始化类和接口。加载是查找具有特定名称的类或接口类型的二进制表示并从该二进制表示创建类或接口的过程。链接是获取类或接口并将其组合到Java虚拟机的运行时状态以使其能够执行的过程。类或接口的初始化包括执行类或接口初始化方法
<clinit>
(§2.9)。
在本章中,§5.1描述了Java虚拟机如何从类或接口的二进制表示中派生符号引用。§5.2说明Java虚拟机如何首先启动加载、链接和初始化过程。§5.3规定类加载器如何加载类和接口的二进制表示,以及类和接口是如何创建的。链接在§5.4中进行了描述。§5.5详细说明类和接口是如何初始化的。§5.6引入了绑定本机方法的概念。最后,§5.7描述了Java虚拟机何时退出。
一个 Java Class 对象被创建的完整过程:加载 – 链接 – 初始化,
- 在加载阶段, 创建的 class 对象存储在堆(Heap)、以及 Metaspace 的 Klass、Non-Klass 对象;
- 在链接阶段, final 常量和字符串在方法区分配空间(jdk 8 变成了元空间);
- 在初始化阶段, 初始化 static 成员, 也在堆;
①加载(Loading)
- 由对应的 ClassLoader 从磁盘读取.class 文件字节 // 这里的类加载器可以自定义
- ClassLoader 的 loadClass 最终通过
ClassLoader.defineClass()
方法创建一个 java.lang.Class 的对象, 对象存储在堆(Heap),
这一步除了创建 Class 对象,还会在 Metaspace 创建 Klass 对象等… @ref: Advanced-Java.02b1.MetaSpace解析
②链接(Linking)
链接阶段还分 3 个步骤:
1 验证(Verification): 验证加载类的字节码, 验证成功则载入到方法区(Method Area), 验证项包括如下:
- 变量使用前要初始化
- 方法调用与对象引用之间类型要匹配
- 访问私有数据和方法的规则没有违反
- 运行时堆栈没有溢出
2 准备(Preparation):
- 为静态字段分配内存,这一步仅是分配内存但并未对其初始化;
除了分配内存外,部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的 方法表 /方法表分为 vtable 和 itable,前者是非私的“实例方法”,后者是接口方法/;
JDK8 移除了方法区,类的元数据移到了 Native 内存的 Metaspace,类的 static 成员在堆区 jvm - Where are static methods and static variables stored in Java? - Stack Overflow
3 解析(Resolution): 把类中的 符号引用 转换为 直接引用:
- 对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。解析阶段的目的,正是将这些符号引用解析成为实际引用。对于 Java 来说,大部分方法调用都是通过 invokevirtual 字节码调用的实例方法,这类方法调用被解析为在 vtable 位置的索引;
- 如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化)
③初始化(Initialization)
类加载的最后一步是初始化,为标记为常量值的字段赋值,以及执行 <clinit>
方法的过程。
如果 filed 是在声明时被直接赋值的:
- 如果 field 是 static final 修饰,且是基本类型(例如
final static int m = 1
),那么此 filed 会被编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成; - 除了上面的情况之外的所有直接赋值操作,连同static 代码块,一并被放入
<clint>
方法,该方法是由JVM加锁执行,保证该方法的线程安全。这个特性常被用来保证单例模式的线程安全,如果是在“static的内部类”中定义的static field,还可以利用内部类实现延后单例。
@ref: 03 | Java虚拟机是如何加载Java类的?
JVM 规范枚举了下述多种 触发初始化 的情况(但不限于这几种):
- Java 虚拟机启动时, 被标明为启动类(有 main 方法)会被初始化
- 初始化一个类的时候如果发现其父类还没用初始化, 则先初始化其父类, 这种属于 被动初始化;
- 用 new 明确创建一个类实例, 这里用的是
new
字节码指令, 当且类还没有完成初始化; - 首次对类的 static (同时必须满足非 final)的成员属性进行读写, 一般是调 getter/setter 方法的时候, 对应字节码指令:
getstatic
,putstatic
- 首次调用类的 static 方法, 对应字节码指令:
invokestatic
- 调用
Class.forName("xxx")
; - …
➤ 比较四种指令 new
, getstatic
, putstatic
, invokestatic
:
除了
new
是主动初始化, 后面三种都是被动初始化.
➤ 比较 Class.forName("xxx")
和 ClassLoader.loadClass()
:
作用都是返回 Class 对象;
Class.forName()只能通过应用加载器(AppClassLoader)创建 Class 对象, 还会调用类的 static 代码块;
ClassLoader.loadClass()可以通过自定义 ClassLoader 创建 Class 对象,
内部类的初始化
- 对于非静态内部类, 不允许有 static 成员, 也不允许有 static 代码块;
- 静态内部类是可以有
static{}
代码块的, 我们在new Outter()
的时候, 其内部类的 static 代码块并没有被调用到, 直到对内部类进行getstatic
,invokestatic
等操作的时候, 内部类的 static 代码块才会被调用, 才会初始化. 单例模式就用到了这个”延迟初始化”的特性.
通过内部类实现单例:
public class Singleton { |
ClassLoader
使用Classloader 创建 Class对象
JVM 在加载(Loading)阶段依靠 ClassLoader 完成, ClassLoader 的加载类过程主要使用 ClassLoader.loadClass(String name)
方法:
MyClassLoader cl = new MyClassLoader(); |
loadClass()
:它会先检查类是否被加载过,如果否则将请求转发给父类加载器。如果父类加载器也无法加载此类,再尝试使用自己的findClass 加载;findClass()
:从文件or网络流..,加载字节码,并可能对字节码进行预处理(例如解密等),然后调用defineClass()
创建Class的对象;defineClass()
:最终调用了defineClass1()
,是一个native 方法,返回Class 类型的对象
获得 Class类型的对象后,就可以使用 newInstance()创建实例对象,以及通过反射中的方法获取class 的方法 Java-Tutorials.03.反射(Reflection)
加载完成后, 虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中, 在 JVM 堆中也创建一个 java.lang.Class 类的对象.
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 object.getClass().getClassLoader()
方法就可以获取到此引用。
ClassLoader 分类
Java 中的类加载器大致可以分成3类:
- 启动类加载器(Bootstrap ClassLoader): 它负责加载存放在
JDK\jre\lib\rt.jar
里java.*
开头的类;
谁加载了java.lang.ClassLoader本身? 答案就是 Bootstrap ClassLoader How is the Java Bootstrap Classloader loaded? - Stack Overflow
扩展类加载器(Extension ClassLoader): 它负责加载
JDK\jre\lib\ext
目录中所有类库(如javax.*
开头的类);应用程序类加载器(Application ClassLoader): 它负责加载
-classpath
或-cp
路径下的类, 开发者可以直接使用该类加载器, 如果应用程序中没有自定义过自己的类加载器, 一般情况下用的都是 Application ClassLoader , ps 有些资料也把它翻译为系统类加载器“System ClassLoader”用户定义类加载器(User Defined ClassLoader),需要继承 ClassLoader,它的 parent 是 Application ClassLoader
public void printClassLoaders() throws ClassNotFoundException { |
运行上面的代码,可以看到返回的3个 Cl 分别是:
jdk.internal.loader.ClassLoaders$AppClassLoader
jdk.internal.loader.ClassLoaders$PlatformClassLoader
null
也即上面提到的 Application ClassLoader、Extension ClassLoader、Bootstrap ClassLoader,可是为什么最后一个是null ? 因为 Bootstrap ClassLoader 是C++实现的而非Java,所以没有一个Java 的 “Class”。
另,在Java 9 之后,扩展类加载器变为了 PlatformClassLoader。
ClassLoader是否是继承关系?
三种层次的 ClassLoader 并不是继承关系(Inheritance),子 ClassLoader 通过是 组合(Composite) 的方式复用父 ClassLoader 的代码:
public abstract class ClassLoader { |
- User defined ClassLoader 的 parent 是 App ClassLoader;
- App ClassLoader 的 parent 是 Ext ClassLoader;
- Bootstrap ClassLoader 作为其他所有 ClassLoader 的 parent;
以上 4类加载器通过这种方式组织起来形成树状结构,如下图所示(图中的 System Class Loader 即 App ClassLoader),虚线上方是 JDK 提供的 Cl,下方是用户自定义的 Cl:
在Java 12上测试:
- 用户自定义的加载器,getParent()返回的是 AppClassLoader;
- AppClassLoader.getParent() 返回的是 PlatformClassLoader;
- PlatformClassLoader.getParent() 返回 Null,因为 Bootstrap Cl 并不是一个 Java 类,而是一个 Native 类;
Bootstrap class loader serves as the parent of all the other ClassLoader instances https://www.baeldung.com/java-classloaders
A ClassLoader with a null parent ClassLoader appears to only have visibility into the java.base module. In Java 8 and earlier, such a ClassLoader would have visibility to all of the classes comprising the Java platform. https://bugs.openjdk.org/browse/JDK-8161269
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
ClassLoader 如何工作?双亲委派机制
使用自定 ClassLoader 加载 class,并创建实例对象:
MyClassLoader cl = new MyClassLoader(); |
在 ClassLoader.loadClass()
方法内实现了双亲委派:
- 先检查类是否被加载过
- 如果否则将请求转发给父类加载器
- 如果父类加载器也无法加载此类,再尝试使用自己的findClass 加载
概括双亲委派就是 总是向上委托、可见性原则(每个加载器总是负责加载自己可见的类)
protected Class<?> loadClass(String name, boolean resolve) |
这样做的好处是可以保证类的唯一性,例如 可以确保 Java 核心类的唯一性,因为向上委托的模式下,核心类总是由 Bootstrap 加载的。
ps:外文资料里一般都写 “Parents delegate”,国内资料大都默认了翻译成“双亲”,这是一种词不达意的翻译,理解起来好像是要委托2个父 ClassLoader,其实本意是“委托给 parents 一级的 ClassLoader”。
再看 ClassLoader 的几个方法:
Class<?> loadClass(String cn, boolean resolve)
:如上分析,loadClass 里实现了 双亲委派,并最终调用了 findClass,抛出 ClassNotFoundException 异常;Class<?> findClass(String cn)
:findClass 里一般是加载.class的字节流然后调用 defineClass,例如 URLClassLoader.findClass 里实现的是通过URI获取.class的字节流,抛出 NoClassDefFoundError 异常;Class<?> defineClass(String name, byte[] b, int off, int len)
:defineClass 把字节码转化为Class对象;
实现自定义 ClassLoader
从 ClassLoader 的层级关系可知,我们自定义的 ClassLoader 的 parent 是 App ClassLoader;
从上面 ClassLoader 方法的介绍可知,如果我们的自定义 Cl 需要改变双亲委派机制,那么重写 loadClass 方法,如果不想改变,则只需重写 findClass 即可:
// 1继承CLassLoader |
可见性(Visibility)
子类加载器对父类加载器加载的类是可见的。
例如,System Cl 加载的类,对 Ext Cl 和 Bootstrap Cl 加载的类具有可见性,但反之则不然。
为了说明这一点,如果类A由 App Cl 加载,而类B由 Ext Cl 加载,则就由 App Cl 加载的其他类而言,A和B类都是可见的。
然而,类B是唯一对 Ext Cl 加载的其他类可见的类。
原文: Visibility
In addition, children class loaders are visible to classes loaded by their parent class loaders.
For instance, classes loaded by the system class loader have visibility into classes loaded by the extension and bootstrap class loaders, but not vice-versa.
To illustrate this, if Class A is loaded by the application class loader, and class B is loaded by the extensions class loader, then both A and B classes are visible as far as other classes loaded by the application class loader are concerned.
Class B, however, is the only class visible to other classes loaded by the extension class loader.
可见性的另一个例子见下面的「ConextClassLoader」一节
上下文类加载器(ConextClassLoader)
有时当JVM核心类需要动态加载应用程序开发人员提供的类或资源时,我们可能会遇到问题。
例如在JNDI中,核心功能由rt.jar中的引导类实现。但这些JNDI类可能会加载由第三方厂商实现的 JNDI Provider 库(这些库放在应用的 classpath 中)。此场景要求 Bootstrap 加载器(父类加载器)加载“对应用程序加载器(子类加载器)可见”的类。
这些类对于 Bootstrap 来说,是不可见的,双亲委派机制在这里也不起作用(双亲委派是 ClassLoader.loadClass 实现的,在 Bootstrap 没有实现这个机制,且 Bootstrap 也没有 parent loader),要绕过这个问题,我们需要找到类加载的替代方法,这可以使用线程上下文加载器来实现。
Thread类有一个方法 getContextClassLoader()
,该方法返回特定线程的 “ConextClassLoader”。
ConextClassLoader 由线程的创建者在加载资源和类时提供。如果未设置该值,则默认为父线程的类加载器上下文。「The ContextClassLoader is provided by the creator of the thread when loading resources and classes. If the value isn’t set, then it defaults to the class loader context of the parent thread.」
@ref: https://www.baeldung.com/java-classloaders#context-classloaders
ConextClassLoader 在实际应用,以 JDBC.DriverManager 创建连接为例:
String url = "jdbc:mysql://xxxx:xxxx/xxxx"; |
JDBC 定义了接口 java.sql.Driver,具体的实现都是由数据库厂商来提供的。
DriverManager 是由 PlatformClassLoader 加载,根据 Cl 的职责,它只能加载 JDK 扩展包,那数据库厂商提供的 Jar 是如何加载到的呢?
private static Connection getConnection( |
上面的代码可以看到,getConnection 最后是由 ServiceLoader 来加载数据库的 Driver,其 load
方法如下:
public static <S> ServiceLoader<S> load(Class<S> service) { |
- 创建了一个
Thread.currentThread().getContextClassLoader()
,然后这个 contextClassLoader 被赋值给了ServiceLoader.load
成员变量 - …
- 在
driversIterator.hasNext()
里,会搜索 classpath 下以及 jar 包中所有的META-INF/services
目录下的java.sql.Driver
文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类 - 接着在
driversIterator.next()
里,根据实现类的全名创建 Class 对象。这里是用了Class.forName(cn, false, loader)
来创建,第三个参数 loader 即第一步创建的 contextClassLoader
@ref: Java常用机制 - SPI机制详解 | Java 全栈知识体系
不适用双亲委派的场景
Tomcat
Tomcat 作为一个 Servlet 容器,一个 Tomcat 实例(一个 JVM 进程)可以同时运行多个 Java Web Application(WAR 包),不同的 WAR 可能会依赖同一个第三方类库的不同版本, JDK 的 App ClassLoader 无法实现这个机制,所以 Tomcat 使用自己的 WebApp ClassLoader。[[../13.JavaEE-Framework/JavaEE.Tomcat#ClassLoader]]
WebApp ClassLoader 的加载顺序和双亲委派相反:
- 首先用 WebApp ClassLoader 加载应用自己
/WEB-INF
下的 class文件 - WebApp ClassLoader 无法加载的交给 App ClassLoader (CLASSPATH下的class)
- 再加载不到的,最后交给 Common ClassLoader(加载 CATALINA 下面的lib)
可以认为每个不同的 web 应用都有各自的 CLASS_PATH,如果默认使用 App ClassLoader 无法实现 CP 的隔离
SPI
SPI(Service Provider Interface),是 JDK 内置的一种服务提供发现机制,由此机制,JDK 的核心类可以加载并使用第三方厂商提供的类。
JNDI、 JDBC DriveManager 都使用了这种机制来加载三方厂商的类库,
二者各自被 Bootstrap 和 Ext ClassLoader 加载,但这两种 ClassLoader 都无法加载三方厂商的类库——因为它们是属于 App ClassLoader 的管辖范围,对 Bootstrap & Ext ClassLoader 来说不可见。
以 JDBC DriveManager 为例,它使用 java.util.ServiceLoader
完成 SPI 机制,ServiceLoader 实现的原理一个是延后加载,一个是使用 ConextClassLoader,具体见上面的「ConextClassLoader」一节。
OSGI
OSGi(Open Service Gateway Initiative)
OSGi 是 Java 技术栈下的动态模块系统。OSGi 中的每个模块(bundle)都包含 Java 包和类。
模块可以声明它所依赖的需要导入的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出自己的包和类,供其它模块使用(通过 Export-Package)。
这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 类。当它需要加载 Java 核心库的类时,它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载其他模块导入的 Java 类时,它会代理给「导出此 Java 类的模块」来完成加载。
假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。
- 在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。
- bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample 继承自 com.bundleA.Sample。
- 在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample 是导入的,classLoaderB 把加载类 com.bundleA.Sample 的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。
- classLoaderA 在其模块内部查找类 com.bundleA.Sample 并定义它,所得到的类 com.bundleA.Sample 实例就可以被所有声明导入了此类的模块使用。
OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。
不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:
- 如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath 中指明即可。
- 如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
- 如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError 异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。
Java 9 引入模块化之后
整个 JDK 都基于模块化进行构建,以前的 rt.jar, tool.jar 被拆分成数十个模块,
编译的时候只编译实际用到的模块,所以 App ClassLoader 中调用的 loadClass 逻辑已经发生改变(见下面的代码)
先尝试通过模块的 ClassLoader 进行加载,找不到模块 ClassLoader 才进行双亲委派。loadClass的工作模式不再总是“向上委托”了
protected Class<?> loadClassOrNull(String cn, boolean resolve) { |
JVM 如何判断两个类是否相同
OBJ09-J. Compare classes and not class names - SEI CERT Oracle Coding Standard for Java - Confluence
In a Java Virtual Machine (JVM), “Two classes are the same class (and consequently the same type) if they are loaded by the same class loader and they have the same fully qualified name” [JVMSpec 1999]. Two classes with the same name but different package names are distinct, as are two classes with the same fully qualified name loaded by different class loaders.
在 Java 虚拟机(JVM)中,“如果两个类由相同的类加载器加载,并且它们具有相同的完全限定名,则它们是相同的类(因此是相同的类型)”[JVMSpec 1999]
It could be necessary to check whether a given object has a specific class type or whether two objects have the same class type associated with them, for example, when implementing the equals() method. If the comparison is performed incorrectly, the code could assume that the two objects are of the same class when they are not. As a result, class names must not be compared.
例如,在实现equals()
方法时,可能需要检查给定对象是否具有特定的类类型,或者两个对象是否具有与其关联的相同类类型。如果比较执行得不正确,代码可能会假定这两个对象属于同一类,但实际上并不是。因此,不能比较类名
JVM 在判定两个 class 是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。就算两个 class 是同一份 class 字节码,如果被两个不同的 ClassLoader 实例所加载,JVM 也会认为它们是两个不同 class。
比如网络上的一个 Java 类 org.classloader.simple.NetClassLoaderSimple
,javac 编译之后生成字节码文件 NetClassLoaderSimple.class,
ClassLoaderA 和 ClassLoaderB 这两个类加载器并读取了 NetClassLoaderSimple.class 文件,并分别定义出了 java.lang.Class 实例来表示这个类,对于 JVM 来说,虽然都来自一份.class,但它们是两个不同的实例对象,如果试图将这个 Class 实例生成具体的对象进行转换时,就会抛运行时异常 java.lang.ClassCaseException
public class NewworkClassLoaderTest { |
setNetClassLoaderSimple() 实现也很简单,只有一个强制转换+赋值:
this.instance = (NetClassLoaderSimple)object; |
PS: 加载类(NetClassLoaderSimple)不要放在 IDE 的工程目录内,因为 IDE 在 run 的时候会先将工程中的所有.class 都用 AppClassLoader 加载,输出结果是 true
结论,ClassLoader 对判断类是否相同的影响:
- 使用不同的 ClassLoader,
loadClass()
创建出来的 class 对象不同(不 equals,说明不是一个 class对象) - 使用不同的 class 对象,
newInstance()
创建出来的 obj 对象也不同,反射调用失败
@ref::
ClassLoader 面试八股文
你确定你真的理解”双亲委派”了吗?! - HollisChuang - 博客园
1、什么是双亲委派? 2、为什么需要双亲委派,不委派有什么问题? 3、”父加载器”和”子加载器”之间的关系是继承的吗? 4、双亲委派是怎么实现的? 5、我能不能主动破坏这种双亲委派机制?怎么破坏? 6、为什么重写loadClass方法可以破坏双亲委派,这个方法和findClass()、defineClass()区别是什么? 7、说一说你知道的双亲委派被破坏的例子吧 8、为什么JNDI、JDBC等需要破坏双亲委派? 9、为什么TOMCAT要破坏双亲委派? 10、谈谈你对模块化技术的理解吧!