Java:类加载

java

Java 8

IDE Eclipse

---

目录

一、概述

二、开始试验

try1:获取各种类加载器

try2:Class.forName加载类

try3:Application ClassLoader加载类

try4:自定义类加载器&加载类

try5:自定义类加载器&热部署

参考文档

一、概述

类加载:使用 类加载器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>

try1:获取各种类加载器

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]

try2:Class.forName加载类

添加类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

try4:自定义类加载器&加载类

继承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

try5:自定义类加载器&热部署

热部署就是,在不重启应用(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查看内存中加载的类和类加载器

来自博客园

当然,还有 jmap命令,可以输出 dump文件 进行更进一步分析。来自博客园

参考文档

1、书《Java编程的逻辑》 by 马昌俊

2、Java内存区域(运行时数据区域)和内存模型(JMM)

3、【JVM】查看JVM加载的类及类加载器的方法

4、一篇文章吃透:为什么加载数据库驱动要用Class.forName()

5、

以上是 Java:类加载 的全部内容, 来源链接: utcz.com/z/392788.html

回到顶部