文章目录



在冯.诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与 CPU 进行交流

字节码 ​​.class​​ 文件同样需要加载到内存中,才可以实例化类。

在加载类时, 使用的是 ​​Parents Delegation Model​​ ,译为 双亲委派模型


一、类加载器作用


Java 的类加载器是一个运行时核心基础设施模块。

主要是在启动之初进行类的 加载(Load)、链接(Link)、初始化(Init)

具体如图:

【JVM】之 类加载(ClassLoader)_jvm



(1)加载 Load

Load 阶段读取类文件产生二进制流, 并转化为特定的数据结构,初步校验 ​​cafe babe​​​ 魔法数、常量池、文件长度、是否有父类等, 然后创建对应类的 ​​java.Jang.Class​​ 实例

【JVM】之 类加载(ClassLoader)_加载_02



(2)链接 Link

Link 阶段包括验证、准备、解析三个步骤。

  1. 验证是更详细的校验,比如​​final​​ 是否合规、类型是否正确、静态变量是否合理等
  2. 准备阶段是为静态变量分配内存,并设定默认值。
  3. 解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。


(3)初始化 Init

Init 阶段执行类构造器​​<clinit>​​ 方法,如果赋值运算是通过其他类的静态方法来完成的, 那么会马上解析另外个类,在虚拟机枪中执行完毕后通过返回值进行赋值。


二、类加载详情


类加载是一个将 ​​.class​​​ 字节码文件实例化成 ​​Class​​ 对象并进行相关初始化的过程。

在这个过程中, JVM 会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语旬等。



(1)双委托模型

如图:

【JVM】之 类加载(ClassLoader)_classloader_03

作用:为了避免重复加载,由下到上逐级委托,由上到下逐级查找

  1. 首先不会自己去加载类,而是把这个请求委托给父加载器去完成
  2. 只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

【JVM】之 类加载(ClassLoader)_虚拟机_04



1)启动类加载器

​Bootstarp ClassLoader​

负责加载机器上安装的​​Java​​目录下的核心类

​JVM​​​启动后,首先依托启动类加载器,去加载环境下​​lib​​目录中的核心类库



2)扩展类加载器

​Extension ClassLoader​​​ 加载 ​​lib/ext​​目录下



3)应用程序类加载器

​Application ClassLoader​​​ 去加载​​ClassPath​​环境变量所指定的路径中的类

其实,就是去加载自己写的​​Java​​代码,这个类加载负载将那些类加载到内存中。



4)自定义类加载器

自定义类加载器,根据需求去加载你的类。

【JVM】之 类加载(ClassLoader)_jvm_05

【JVM】之 类加载(ClassLoader)_classloader_06



(2)举个栗子

public class ClassLoaderDemo { 



public static void main(String[] args) throws ClassNotFoundException {



// 加载核心类库的 Bootstrap ClassLoader

System.out.println("核心类库加载器: " + ClassLoaderDemo.class.getClassLoader()

.loadClass("java.lang.String").getClassLoader());



// 加载拓展库的 Extension ClassLoader

System.out.println("扩展类库加载器: " + ClassLoaderDemo.class.getClassLoader()

.loadClass("com.sun.nio.zipfs.ZipCoder").getClassLoader());



// 加载应用程序

System.out.println("应用程序库加载器: " + ClassLoaderDemo.class.getClassLoader());



// 双亲委派模型 Parents Delegation Model

System.out.println("应用程序库加载器的父类: " + ClassLoaderDemo.class.getClassLoader().getParent());

System.out.println("应用程序库加载器的父类的父类: " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());

}

}

输出

$ExtClassLoader@63947c6b 

应用程序库加载器: sun.misc.Launcher$AppClassLoader@18b4aac2

应用程序库加载器的父类: sun.misc.Launcher$ExtClassLoader@63947c6b


三、类卸载


类什么时候会被卸载?

满足以下两个条件:

  1. 该​​Class​​​所有的实例都已经被​​GC​
  2. 加载该类的​​ClassLoader​​​实例已经被​​GC​

可以通过​​jvm -verbose:class​​ 输出类加载和卸载的日志信息




四、类热加载


​ClassLoader​​​ 和 ​​Class​​ 确定一个 类

【JVM】之 类加载(ClassLoader)_类加载器_07

当​​.java​​​重新编译为​​.class​​文件

通过创建新的类加载器来实现热加载。

= new URL("file:/"); 



URLClassLoader loader = new URLClassLoader(new URL[] {classURL});



Class clazz = loader.loadClass("HelloService");




五、问题


(1)如何突破双亲模式?

可以通过重载 ​​ClassLoader​​ 来修改双亲委托模式。

即,重写​​loadClass​​​方法,来改变类的加载次序。
比如:先使用自定义类加载器加载,如果加载不到,则交给双亲加载。

实现方案:

  1. 使用线程上下文类加载器实现,让父类加载器请求子类加载器去完成类加载的动作。
.currentThread().setContextClassLoader(loader);
  1. 直接创建,然后去加载
// 注意 parent 

URLClassLoader loader = new URLClassLoader(new URL[] {classURL});



(2)​​JVM​​ 如何知道类在哪?

可以查看​​openjdk​​​源码:​​sun.misc.Launcher.AppClassLoader​

即,读取​​java.class.path​​配置,指定去哪些地址加载类资源。

验证下:

  1. ​jps​​​查看本机 ​​Java​​ 进程
  2. 查看运行时配置:​​jcmd 进程号 VM.system_properties​

Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接。(Tips:例如AOP(动态代理),因为Java是静态,不像Ruby Python 运行时修改源码,Java不行,Java只能修改字节码来实现运行时动态。即根据Class,读取字节码,进行修改,再形成字节数组,写入内存或文件,从而实现。)

其他资料:Javasisit,ASM。