Java:类加载
Java 8
IDE Eclipse
---
目录
try3:Application ClassLoader加载类
类加载:使用 类加载器ClassLoader 将字节码加载到内存,创建Class对象。
ClassLoader一般是由系统提供的,在Java 8中,有以下3个类加载器:
- 启动类加载器(Bootstrap ClassLoader)
C++实现,加载Java基础类,主要是<JRE_HOME>/lib/rt.jar中的。
- 扩展类加载器(Extension ClassLoader)
static class sun.misc.Launcher$ExtClassLoader extends java.net.URLClassLoader,
加载一些扩展类,主要是<JRE_HOME>/lib/ext目录中的jar包。
- 应用程序类加载器(Application ClassLoader)
static class sun.misc.Launcher$AppClassLoader extends java.net.URLClassLoader,
加载自己写的 和 引入的第三方类库,即所有类路径中指定的类。
程序运行时,会创建一个 Application ClassLoader,如无特别说明,一般都是用它加载类,也因此被称为 系统类加载器(System ClassLoader),可以使用 ClassLoader.getSystemClassLoader() 获取。
---
三个类加载器存在一定的关系:父子委派关系。这个设计的目的是实现 双亲委派模型,即优先让父ClassLoader去加载,这样可以避免 Java类库被覆盖的问题。
说明,
1、在eclipse中,三个类加载器的源码没看到,或许要去官网下载源码才行;
2、上面的JRE_HOME,本来是 JAVA_HOME的,但在我电脑安装的 jdk1.8.0_202 中没有找到,但在 JRE_HOME 中存在;
3、扩展和应用程序类加载器都 继承了 java.net.URLClassLoader,有源码,其下也有很多子类;但它继承了 SecureClassLoader,其上还有ClassLoader抽象类;
4、本文针对Java 8的类加载做介绍,对于Java 9+的类加载器系统,另一种 体系,尚未研究。
5、双亲委派模型 虽然是 一般模型,但也有一些其它例外:1)自定义加载顺序、2)网状加载顺序、3)父加载器委派给子加载器加载。
除了系统提供的类加载器,还可以 创建自定义类加载器,通过继承ClassLoader抽象类即可。
通过自定义类加载器,可以实现一些强大的功能,比如:
1、热部署
2、应用的模块化和相互隔离
3、从不同地方灵活加载
加载类的几种方式:
1、Class.forName静态方法
两个静态方法:
public static Class<?> forName(String className) throws ClassNotFoundException;
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException;
其中,前者是后者的简单版本,底层都是调用 forName0 函数,而前者调用时,initialize设置为 true——执行类初始化(包括执行 static代码块)。
2、使用程序的Application ClassLoader对象的 实例方法 loadClass
public Class<?> loadClass(String name) throws ClassNotFoundException;
3、使用自定义ClassLoader的 实例方法 loadClass
先创建自定义ClassLoader,再生成其对象,再调用loadClass方法。
---
更多知识点:Class类、Java运行时数据区域
试验中使用了 lombok:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
public class LoadMain {
private static Consumer<Object> cs = System.out::println;
public static void main(String[] args) {
ClassLoader scl = ClassLoader.getSystemClassLoader();
cs.accept("scl=" + scl);
ClassLoader cl = LoadMain.class.getClassLoader();
cs.accept("cl =" + cl);
// 返回true:是同一个对象
cs.accept("scl == cl? = " + (scl == cl));
// 扩展类加载器
ClassLoader p1 = cl.getParent();
cs.accept("p1=" + p1);
// 启动类加载器,值为null——因为使用C++实现
ClassLoader p2 = p1.getParent();
cs.accept("p2=" + p2);
}
}
测试结果:
scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
启动时,可以使用 java命令的 -verbose:class 参数 来查看(监视)JVM加载了哪些类。
在上面的程序添加后,可以看到下面的内容:
执行结果(部分):可以看到,其中加载了 LoadMain类
...省略...
[Loaded fanshe.load.LoadMain$$Lambda$1/1418481495 from fanshe.load.LoadMain]
[Loaded java.lang.invoke.LambdaForm$MH/303563356 from java.lang.invoke.LambdaForm]
scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
....程序结束...
[Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
添加类LoadedOne 用于动态加载:
LoadedOne.java
package fanshe.load;
import lombok.Data;
@Data
public class LoadedOne {
private String name = "1726";
private final float pi = 3.14f;
private static Integer count;
private static final int MAX = 100;
static {
System.out.println("set count");
count = 99999;
System.out.println("count=" + count);
}
}
继续改造LoadMain:启动后,休眠20秒,然后再调用 Class.forName价值 LoadedOne类。
public class LoadMain {
private static Consumer<Object> cs = System.out::println;
private static Class<?> loadedOneCls;
private static String clsPath = "fanshe.load.LoadedOne";
public static void main(String[] args) {
...
try {
cs.accept("sleep...20秒后 加载 LoadedOne类...now=" + new Date());
TimeUnit.SECONDS.sleep(20);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 方式1:会执行类初始化
Class<?> loadedOneCls = Class.forName(clsPath);
// 方式2:不会执行类初始化
// loadedOneCls = Class.forName(clsPath, false, cl);
cs.accept("loadedOneCls=" + loadedOneCls);
cs.accept("loadedOneCls=" + loadedOneCls.getSimpleName());
cs.accept("loadedOneCls=" + loadedOneCls.getName());
cs.accept("loadedOneCls=" + loadedOneCls.getCanonicalName());
cs.accept("loadedOneCls=" + loadedOneCls.getTypeName());
try {
Object nobj = loadedOneCls.newInstance();
cs.accept("new obj=" + nobj);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
if (true) {
cs.accept("....程序结束...now=" + new Date());
return;
}
}
}
继续使用 -verbose:class,可以监控 休眠后加载的过程。
执行结果(部分):
[Loaded fanshe.load.LoadMain$$Lambda$1/1418481495 from fanshe.load.LoadMain]
[Loaded java.lang.invoke.LambdaForm$MH/303563356 from java.lang.invoke.LambdaForm]
scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
[Loaded java.util.Date from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
......
sleep...20秒后 加载 LoadedOne类...now=Sun Oct 24 17:37:19 CST 2021
......
set count
count=99999
loadedOneCls=class fanshe.load.LoadedOne
loadedOneCls=LoadedOne
loadedOneCls=fanshe.load.LoadedOne
loadedOneCls=fanshe.load.LoadedOne
loadedOneCls=fanshe.load.LoadedOne
......
[Loaded sun.misc.FDBigInteger from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
new obj=LoadedOne(name=1726, pi=3.14)
....程序结束...now=Sun Oct 24 17:37:39 CST 2021
[Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
上面代码有 2个 Class.forName函数, 试验使用的是第一个——需要执行类初始化,在加载时就会执行静态代码块。
选择第2个时,则会再 创建对象 时才会执行 静态代码块。
注意,在Eclipse中,LoadedOne类的class文件 已经自动编译到了classes 类路径中了,否则,请使用javac编译。
try3:Application ClassLoader加载类
loadClass方法:底层调用了另一个 protected的 loadClass方法,多一个resolve参数。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
示例程序:
private static String clsPath = "fanshe.load.LoadedOne";
/**
* 使用系统类加载器加载 LoadedOne
* @author ben
* @date 2021-10-24 17:54:38 CST
*/
public static void loadBySystemCl() {
ClassLoader scl = ClassLoader.getSystemClassLoader();
try {
Class<?> newcls = scl.loadClass(clsPath);
cs.accept("newcls=" + newcls);
cs.accept("newcls#1=" + newcls.getSimpleName());
cs.accept("newcls#2=" + newcls.getName());
cs.accept("newcls#3=" + newcls.getCanonicalName());
cs.accept("newcls#4=" + newcls.getTypeName());
try {
Object obj = newcls.newInstance();
cs.accept("obj=" + obj);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
执行结果:注意,这种加载方式,没有执行类初始化——static块没有在加载时执行,而是在创建类对象前执行的。
scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
sleep...20秒后 加载 LoadedOne类...now=Sun Oct 24 18:01:16 CST 2021
newcls=class fanshe.load.LoadedOne
newcls#1=LoadedOne
newcls#2=fanshe.load.LoadedOne
newcls#3=fanshe.load.LoadedOne
newcls#4=fanshe.load.LoadedOne
set count
count=99999
obj=LoadedOne(name=1726, pi=3.14)
....程序结束...now=Sun Oct 24 18:01:21 CST 2021
继承ClassLoader类,实现findClass函数就可以了——实现从 不同来源 获取class文件并执行加载。
不同来源包括:文件系统、数据库系统、Web服务器等。
ClassLoader类的findClass函数:直接抛出了异常!
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
接下来实现 自定义类加载器,并D盘下的LoadedOne类(默认package)。来自博客园
LoadedOne.java
public class LoadedOne {
private String name = "D:\\class";
private final float pi = 3.14f;
private static Integer count;
private static final int MAX = 1000;
static {
System.out.println("set count");
count = 99999;
System.out.println("count=" + count);
}
public String toString() {
return name + ", " + pi;
}
}
MyLoader.java:可以加载 D盘下 任何 默认package下的类
package fanshe.load;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyLoader extends ClassLoader {
// 加载D盘下的类文件LoadedOne.class
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filename = "d:/" + name + ".class";
File file = new File(filename);
System.out.println("执行类加载:\nfile.length=" + file.length() + ", lastModified=" + file.lastModified());
byte[] fb = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file);) {
fis.read(fb);
System.out.println("fb:[0-9]");
System.out.printf("0x%02x 0x%02x 0x%02x 0x%02x 0x%02x\n", fb[0], fb[1], fb[2], fb[3], fb[4]);
System.out.printf("0x%02x 0x%02x 0x%02x 0x%02x 0x%02x\n", fb[5], fb[6], fb[7], fb[8], fb[9]);
return defineClass(name, fb, 0, fb.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
}
使用MyLoader:
// LoadMain.java 中建立下面的方法
/**
* 加载类测试
* @author ben
* @date 2021-10-24 19:58:50 CST
*/
private static void myLoader1() {
MyLoader ml = new MyLoader();
cs.accept("MyLoader ml=" + ml);
cs.accept("MyLoader ml=" + ml.getParent());
// 确保 D:\\LoadedOne.class 文件存在
final String cls = "LoadedOne";
try {
Class<?> newcls = ml.loadClass(cls);
cs.accept("newcls=" + newcls);
cs.accept("newcls#1=" + newcls.getSimpleName());
cs.accept("newcls#2=" + newcls.getName());
cs.accept("newcls#3=" + newcls.getCanonicalName());
cs.accept("newcls#4=" + newcls.getTypeName());
Object obj;
try {
// 新建对象
obj = newcls.newInstance();
cs.accept("new obj=" + obj);
} catch (InstantiationException | IllegalAccessException e) {
cs.accept("newInstance()异常:" + e);
}
} catch (ClassNotFoundException e) {
cs.accept("加载失败:" + cls);
e.printStackTrace();
return;
}
}
执行结果:加载成功。但在加载时没有执行类初始化。可以看到,自定义类加载器的父类是 系统类加载器。
MyLoader ml=fanshe.load.MyLoader@65ab7765
MyLoader ml=sun.misc.Launcher$AppClassLoader@73d16e93
file.length=1061, lastModified=1634743643344
fb:[0-4]
0xca 0xfe 0xba 0xbe 0x00
0x00 0x00 0x34 0x00 0x4b
newcls=class LoadedOne
newcls#1=LoadedOne
newcls#2=LoadedOne
newcls#3=LoadedOne
newcls#4=LoadedOne
set count
count=99999
new obj=D:\class, 3.14
....程序结束...now=Sun Oct 24 20:03:25 CST 2021
另外,输出了class文件的前4个字节——cafebabe!
开启 -verbose:class 检查加载的类,显示如下:和之前 使用Class.forName 加载的不同
[Loaded LoadedOne from __JVM_DefineClass__]
dynamicLoadClass2: loadedOneCls=class LoadedOne
set count
count=99999
实现加载类时执行初始化:
重写两个参数的 loadClass(String name, boolean resolve)失败了,TODO
热部署就是,在不重启应用(JVM)的情况下,把 被加载类 改了,然后,程序检测到更新,再次执行 类加载,使用新的类。
踩坑:同一个类加载器对象执行热部署,失败!
同一个ClassLoader,类只会被加载一次,加载后,即使class文件已经变了,再次加载得到的还是原来的Class对象。来自博客园
示例程序:
// LoadMain.java 文件中 // 自定义类加载器
public static void main(String[] args) {
MyLoader myl = new MyLoader();
while (true) {
try {
cs.accept("1秒后执行类加载...now=" + new Date());
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
dynamicLoadClass2(myl);
cs.accept("加载完毕,修改类文件,并编译新的class文件...");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
private static void dynamicLoadClass2(MyLoader cl) throws ClassNotFoundException {
// 入参 无法实现 热部署——重新加载类文件
// 同一个类加载器
// 新建类加载器 才可以 动态加载
// 怎么使用ClassLoader卸载 已加载的类呢?
// cl = new MyLoader();
loadedOneCls = cl.loadClass("LoadedOne");
cs.accept("dynamicLoadClass2: loadedOneCls=" + loadedOneCls);
try {
Object nobj = loadedOneCls.newInstance();
cs.accept("2 new obj=" + nobj);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
执行结果:使用同一个类加载器,不能实现热部署
更改上面的示例程序,每次加载使用新的ClassLoader对象:来自博客园
// 打开上面的这句注释
cl = new MyLoader();
执行结果:动态加载成功。
上面实现了热部署的功能,但是,存在下面的问题:
1、会创建很多ClassLoader对象;
2、每次创建ClassLoader对象去加载类,但是,类不一定变化了,需要判断——最后修改时间等;
3、类加载器可以卸载已加载的类吗?
使用jvisualvm.exe查看内存中加载的类和类加载器
除了 jvisualvm.exe,jconsole.exe 命令也可以看到一些信息:来自博客园
当然,还有 jmap命令,可以输出 dump文件 进行更进一步分析。来自博客园
1、书《Java编程的逻辑》 by 马昌俊
4、一篇文章吃透:为什么加载数据库驱动要用Class.forName()
5、