深入浅出--类加载器和类加载机制
类加载器(ClassLoader)是负责读取 Java 字节码,并转换成 java.lang.Class 类的一个实例的代码模块。
类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。
1. 类加载器
类加载器层级关系如下图所示, Bootstrap、Extension、Application 三者并非是继承关系,而是子类加载器指派父类加载器为自己的 parent属性。由于启动类加载器是内嵌于 JVM 且无法被引用,因此 Extension Classloader 设置null为parent,即等同于指派启动类加载器为自己的父加载器。
类加载器
1.1 Bootstrap ClassLoader
由C++语言实现的,并不是继承自java.lang.ClassLoader,没有父类加载器。
加载Java核心类库,如:$JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path路径下的包,用于提供JVM运行所需的包。
它加载扩展类加载器和应用程序类加载器,并成为他们的父类加载器。
1.2 Extension ClassLoader
Java语言编写,继承自java.lang.ClassLoader,父类加载器为启动类加载器。
负责加载java平台中扩展功能的一些jar包。从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目。录:jre/lib/ext目录下加载类库。我们可以将我们自己的包放在以上目录下,就会自动加载进来了。
1.3 Application ClassLoader
Java语言编写,继承自java.lang.ClassLoader,父类加载器为启动类加载器
它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库
是程序中默认的类加载器,我们Java程序中的类,都是由它加载完成的。
我们可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器。
1.4 User ClassLoader
Java语言编写,根据自身需要实现ClassLoader自定义加载class,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。通过灵活定义classloader的加载机制,我们可以完成很多事情,例如解决类冲突问题,实现热加载以及热部署,甚至可以实现jar包的加密保护。实现自定义ClassLoader的示例参考章节3自定义类加载器
**
// App ClassLoader
System.out.println(this.getClass().getClassLoader());
// Ext ClassLoader
System.out.println(this.getClass().getClassLoader().getParent());
// Bootstrap ClassLoader
System.out.println(this.getClass().getClassLoader().getParent().getParent());
// Bootstrap ClassLoader
System.out.println(new String().getClass().getClassLoader());
输出结果如下,Bootstrap ClassLoader属于JVM的范畴,所以是null
**
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ac42916
null
null
3. 加载类的方式
1. 隐式加载:
通过new
隐式加载,创建对象时,如果类未加载也会尝试加载,例如 Student s = new Student();
会尝试加载Student
类
2. 显示加载:
- 通过
Class.forName(类全限定名)
加载
**
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
其中forName0()
方法调用中的参数 true 表示要初始化该类。包括:
- 执行静态代码块
- 初始化静态域
例如com.mysql.jdbc.Driver
就通过静态代码块想DriverManager中注册自己。当然forName(String name, boolean initialize,ClassLoader loader)
也支持指定初始化(initialize)和类加载器(loader)
- 通过
ClassLoader
的loadClass()
方法加载类
4. 类加载机制
4.1 全盘负责
当一个类加载器负责加载某个类时,该类所依赖的和引用的其他类也将由该类加载器负责加载,除非显示使用另外一个类加载器来加载。
4.2 双亲委派
双亲委派(Parent Delegation),是一个非常糟糕的翻译,但是因为使用较广所以一直沿用至今, 双亲委派也叫作“父类委托”,是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并加载目标类。
双亲委派的机制如ClassLoader中loadClass方法所示:
- findLoadedClass,内部调用native方法,在虚拟机内存中查找是否已经加载过此类
- 如果没有加载,则委派父类加载,对于Extension ClassLoader,parent是null,所以通过findBootstrapClassOrNull委派Bootstrap ClassLoader加载。
- 如果父类没有加载,则通过findClass(name)自己尝试加载。
**
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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.
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;
}
5. 自定义类加载器
“双亲委派”机制只是Java推荐的,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,只需要重写findClass(name)方法;如果想破坏双亲委派模型,则重写loadClass(name)方法。
如下MyClassLoader,通过重写findClass,从MyClassLoader.setRoot 指定的目录加载编译后的.class 文件。注意文件路径不能是classpath路径, 防止要加载的类被Application ClassLoader加载。
**
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
// fileName处理逻辑需要根据实际情况修改,保证能够找到文件
String[] name = className.split("\.");
String fileName = root + File.separatorChar + name[name.length - 1] + ".class";
try {
return Files.readAllBytes(Paths.get(fileName));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("指定目录");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("类的全限定名");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
System.out.println(object.getClass().getClassLoader().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
6. SPI机制是否打破双亲委派
关于SPI(Service Provider Interface)是否打破双亲委派,众说纷纭,这里先抛出结论:没有打破双亲委派。
双亲委派机制是指,子类加载器加载类之前,先去父类加载器中查找,一直查到最基础的启动类加载器,如果都没有加载,则尝试自行加载。根据双亲委派原理,父类加载器加载的类对子类可见,反之则不成立。
这里以JDBC(Java Database Connectivity,Java数据库连接池)为例进行说明。使用JDBC的示例代码如下:
**
String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "root", "1234");
1. DriverManager加载
根据包名可知java.sql.DriverManager
是由启动类加载器加载,在加载时,通过静态代码块调用loadInitialDrivers()
方法, loadInitialDrivers()
通过SPI的方式加载java.sql.Driver
的实现,这里是com.mysql.jdbc.Driver
和com.mysql.fabric.jdbc.FabricMySQLDriver
,加载过程如下,省略部分非核心代码:
**
private static void loadInitialDrivers() {
String drivers;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
2. java.mysql.jdbc.Driver 加载
SPI的机制这里不展开讲,核心原理是通过mysql-connector-java.jar中的META-INF/services/java.sql.Driver文件,找到java.sql.Driver
的具体实现,然后加载实现。loadInitialDrivers()
中的load()
实现如下所示,注意通过Thread.currentThread().getContextClassLoader()
获得应用类加载器,赋值给loader
成员变量。
**
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
SPI中,最终通过Thread.currentThread().getContextClassLoader()
获得的应用类加载器加载com.mysql.jdbc.Driver
,代码如下:
**
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
cn这里就是com.mysql.jdbc.Driver
和com.mysql.fabric.jdbc.FabricMySQLDriver
。调用c.newInstances时, 会执行com.mysql.jdbc.Driver中的静态代码块,即向DriverManager注册自己。
以上穿插的源码比较多,又夹杂着SPI的源码,所以比较混乱,这里做一下总结:
-
java.sql.DriverManager
是由启动类加载器加载,在加载时,通过SPI加载java.sql.Driver
的实现,即com.mysql.jdbc.Driver
。 -
com.mysql.jdbc.Driver
是由应用类加载器负责加载的 -
父类加载器(Bootstrap ClassLoader)通过线程上下文类加载器(ContextClassLoader)去请求子类加载器(Application ClassLoader)完成类加载的行为,看似打破了双亲委派模型来逆向使用类加载器,晚上所有的打破双亲委派也是指这一过程。但是子类加载器的加载也是走双亲委派流程,先委托给父类加载器,加载不到再尝试自行加载,因此完全没有破坏双亲委派。
推荐阅读
-
双亲委托模型和 Flink 的类加载策略--父类优先的类加载策略
-
01-JVM 学习记录-类加载器
-
Android 开发中 nodpi、xhdpi、hdpi、mdpi、ldpi 的概念 - 术语和概念 屏幕尺寸 屏幕的物理尺寸,基于屏幕的对角线长度(如 2.8 英寸、3.5 英寸)。 简而言之,安卓系统将所有屏幕尺寸简化为三大类:大、普通和小。 程序可以为这三种屏幕尺寸提供三种不同的布局选项,然后系统会以合适的方式将布局选项呈现到相应的屏幕上,这个过程不需要程序员用代码进行干预。 屏幕纵横比 屏幕的物理长度与物理宽度之比。程序只需使用系统提供的资源分类器 long(长)和 notlong(不长),就能为具有特定长宽比的屏幕提供配制材料。 分辨率 屏幕的像素总数。请注意,分辨率并不意味着长宽比,尽管在大多数情况下,分辨率表示为 "宽度 x 长度"。在安卓系统中,程序一般不直接处理分辨率。 密度 根据屏幕分辨率,沿屏幕宽度和长度排列的像素数量。 密度较低的屏幕在长度和宽度方向上的像素都相对较少,而密度较高的屏幕通常会在同一区域内排列很多甚至非常非常多的像素。屏幕的密度非常重要;例如,一个界面元素(如按钮)的长度和宽度以像素为单位,在低密度屏幕上会显得很大,但在高密度屏幕上就会显得很小。 独立于密度的像素(DIP)是指程序用来定义界面元素的抽象意义上的像素。它作为一个与实际密度无关的单位,帮助程序员构建布局方案(界面元素的宽度、高度和位置)。 与密度无关的像素在逻辑上与像素密度为 160 DPI 的屏幕上的像素大小相同,而 160 DPI 是安卓平台默认的显示设备。在运行时,平台会以目标屏幕的密度为基准,"透明 "地处理所有所需的 DIP 缩放操作。要将与密度无关的像素转换为屏幕像素,可以使用一个简单的公式:像素 = DIP * (密度 / 160)。例如,在 240 DPI 的屏幕上,1 个 DIP 等于 1.5 个物理像素。强烈建议使用 DIP 来定义程序界面的布局,因为这样可以确保用户界面在所有分辨率的屏幕上都能正常显示。 为了简化程序员在面对各种分辨率时的麻烦,也为了让各种分辨率的平台都能直接运行这些程序,Android 平台将所有屏幕以密度和分辨率作为分类方式,分别分为三类:- 三大尺寸:大、普通、小;- 三种不同密度:高(hdpi)、中(mdpi)和低(ldpi)。DPI 表示 "每英寸点数",即每英寸的像素数。如果需要,程序可以为不同的屏幕尺寸提供不同的资源(主要是布局),为不同的屏幕密度提供不同的资源(主要是位图)。除此之外,程序无需对屏幕尺寸或密度进行任何额外处理。执行时,平台会根据屏幕本身的尺寸和密度特性自动加载相应的资源,并将其从逻辑像素(DIP,用于定义界面布局)转换为屏幕上的物理像素。
-
JVM 类加载机制
-
Java 类加载机制详解
-
jvm 的 java 类加载机制和类加载器(ClassLoader)的详细信息
-
Java 类加载]自定义类加载器
-
JVM 注意事项 (I) Java 类加载过程?类加载器?
-
4.进一步了解类加载器
-
19.类加载器说明