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 FilesJavajdk1.8.0_202jrelib
t.jar]
[Loaded java.lang.Shutdown$Lock from D:Program FilesJavajdk1.8.0_202jrelib
t.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 FilesJavajdk1.8.0_202jrelib
t.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 FilesJavajdk1.8.0_202jrelib
t.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 FilesJavajdk1.8.0_202jrelib
t.jar]
[Loaded java.lang.Shutdown$Lock from D:Program FilesJavajdk1.8.0_202jrelib
t.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("执行类加载:
file.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
", fb[0], fb[1], fb[2], fb[3], fb[4]);
        	System.out.printf("0x%02x 0x%02x 0x%02x 0x%02x 0x%02x
", 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查看内存中加载的类和类加载器

除了 jvisualvm.exe,jconsole.exe 命令也可以看到一些信息:来自博客园

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

参考文档

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

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

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

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

5、

原文地址:https://www.cnblogs.com/luo630/p/15451829.html