• 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、

  • 相关阅读:
    开始写游戏 --- 第十一篇
    开始写游戏 --- 第十篇
    开始写游戏 --- 第九篇
    CDN的原理以及其中的一些技术
    深入理解Redis主键失效原理及实现机制
    使用 Redis 实现分布式系统轻量级协调技术
    Redis实现分布式锁
    进程线程协程
    类加载机制
    消息队列
  • 原文地址:https://www.cnblogs.com/luo630/p/15451829.html
Copyright © 2020-2023  润新知