深入浅出 JVM ClassLoader
# 前言
在 JVM 综述里面,我们说,JVM 做了三件事情,Java 程序的内存管理,
Java Class 二进制字节流的加载(ClassLoader),Java 程序的执行(执行引擎)。我们也说,我们大部分情况下只关注前2个。在前面的文章中,我们已经分析了内存关系相关的,包括运行时数据区,GC 相关。今天我们要讲的就是类加载器。
在 JVM 综述 里,我们已经大致分析了一些概念。而今天的文章将详细的阐述类加载器。
首先,我们要了解类加载器,当然,了解的目的是为了更好的开发,通过对类加载器的解读,看看我们能不能做些什么,比如修改类加载器的加载逻辑,比如加入自定义的类加载器等等功能。
让我们开始吧!
# 1. 类加载器介绍
对于 Java 虚拟机来说,Class 文件是一个重要的接口,无论使用何种语言进行软件开发,只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上运行。可以说,Class 文件就是虚拟机的基石。
如图所示:
从上图可以看出,虚拟机不拘泥于 Java 语言,任何一个源文件只要能编译成 Class 文件的格式,就可以在JVM 上运行!Class 文件格式就像是一个接口,只要遵守这个接口,就能够在 JVM 上运行。
# 2. 类加载器的工作流程
Class 文件通常是以文件的方式存在(任何二进制流都可以是 Class 类型),但只有能被 JVM 加载后才能被使用,才能运行编译后的代码。系统装在 Class 类型可以分为加载,链接和初始化三个步骤。其中,链接也可分为验证,准备和解析3步骤。如图所示:
其中,只有加载过程是程序员能够控制的,后面的几个步骤都是有虚拟机自动运行的。因此,我们的关注点主要放在加载阶段。
# 3. 类加载流程中的 “加载”
上面说了,类加载器3个流程中,唯一能让程序员 “做手脚” 的就是加载过程,上面是加载过程呢?其主要作用就是从系统外部获得 Class 二进制数据流。
JVM 不会无故装载 Class 文件,只有在必要的时候才装载,哪几个时候呢?
- 当创建一个类的实例是,比如使用 new 关键字,或者通过反射,克隆,反序列化。
- 当调用类的静态方法时,即当使用字节码 invokstatic 指令。
- 当使用类或接口的静态字段时(final 常量除外),比如,使用 getstatic 或者 pustatic 指令。
- 当时用 Java.lang.reflect 包中的方法反射类的方法时。
- 当初始化子类,要求先初始化父类。
- 作为启动虚拟机,含有 main()方法的那个类。
以上6种情况属于主动调用,主动调用会触发初始化,还有一种情况是被动调用,则不会引起初始化。
# 3.1 ClassLoader 抽象类介绍
Java 类加载器的具体实现就在 java.lang.ClassLoader,该类是一个抽象类,并且提供了一些重要的接口,用于自定义Class 的加载流程和加载方式。主要方法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
给定一个类名,加载一个雷,返回代表这个类的 Class 实例,如果找不到类,则返回异常。protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
根据给定的字节码流 b 定义一个类,off 表示位置,len 表示长度。该方法只有子类可以使用。protected Class<?> findClass(String name) throws ClassNotFoundException
查找一个类,也是只能子类使用,这是重载 ClassLoader 时,最重要的系统扩展点。这个方法会被 loadClass 调用,用于自定义查找类的逻辑,如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。protected final Class<?> findLoadedClass(String name)
同样的,这个方法也只有子类能够使用,他会去寻找已经加载的类,这个方法是 final 方法,无法被修改。
同时,在该类中,还有一个字段非常重要:parent,他也是一个 ClassLoader 的实例,这个字段所表示的 ClassLoader 也称为这个 ClassLoader 的双亲,在类加载的过程中,ClassLoader 可能会将某些请求交给自己的双亲处理。
# 3.2 类加载器的双亲委派模型
在标准的 Java 程序中,从虚拟机的角度讲,只有2种类加载器:
- 启动类加载器(BootStrap ClassLoader),C++ 语言实现,虚拟机自身的一部分
- 另一种就是所有其他的类加载器,由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽身类 java.lang.ClassLoader。
从程序员的角度讲,虚拟机会创建 3 中类加载器,分别是:Bootstrap ClassLoader(启动类加载器),Extension ClassLoader(扩展类加载器)和 APPClassLoader(应用类加载器,也称为系统类加载器)。此外,每一个应用程序还可以拥有自定义的 ClassLoader,扩展 Java 虚拟机获取 Class 数据的能力。
而这 3 个类加载器有着层次关系。
先来看一个著名的图:
如图所示:从 ClassLoader 的层次自顶向下为启动类加载器,扩展类加载器,应用类加载器和自定义类加载器,当系统需要适用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断,但系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。
注意,我们无法访问启动类加载器,当试图获取启动类加载器的时候,返回 null,因此,如果返回的是 null,并不意味没有类加载器为它服务,而是指哪个类为启动类加载器。
那么这些类加载路径是哪些呢?
BootStrap 类加载器负责加载 <JAVA_HOME>/lib 目录中的,或者别-Xbootclasspath 参数指定的路径。并且是被虚拟机识别的,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会加载。
扩展类加载器有 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>/lib/ext 目录中的。或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
应用类加载器由 sun.misc.Launcher$AppClassLoader 实现,由于这个类是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也称为系统类加载器,负载加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。一般情况下,这个就是程序中默认的类加载器。
自定义类加载器用于加载一些特殊途径的类,一般也是用户程序类。
系统中的 ClassLoader 在协同工作时,默认会使用双亲委托模式,即在类加载的时候,系统会判断当前类是否已经被加载,如果已经加载,则直接返回,否则就尝试加载,在尝试加载时,会先请求双亲处理,如果双亲查找事变,则自己加载。代码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
代码中,如果双亲是 null,则使用启动类加载器加载,如果事变,则使用当前类加载器加载。
双亲为 null 一般有2种情况,1. 双亲是启动类加载器。2. 自己就是启动类加载器。
其中加载类的逻辑有2个注意的地方。
判断是否已经加载?当判断类是否需要加载时,是从底层开始判断,如果底层已经加载了,则不再请求双亲。
当系统准备加载一个类时。会先从双亲加载,也就是最顶层的启动类加载器,逐层向下,直到找到该类。和上面的是相反的。
# 3.3 类加载器的双亲委派模型缺陷和补充
双亲模型固然有着优点,能够让整个系统保持了类的唯一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码无法访问到底层的类加载器。如 rt.jar 无法中代码无法访问到应用类加载器。
你肯定要问,为什么需要访问呢?
在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 Service Provider Interface,即 SPI。
在 rt.jar 中的抽象类需要加载继承他们的在应用层的子类实现,但是以目前的双亲机制是无法实现的。
因此 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是讲类加载放在线程上下文变量中。通过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就可以获取到底层的类加载了。
# 3.4 突破双亲模式
双亲模式是虚拟机的默认行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,很多框架和软件都修改了,比如 Tomcat,OSGI。具体实现则是通过重写 loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载。
# 4. 类加载的扩展---热替换
我们知道:由不同的 ClassLoader 加载的同名类属于不同的类型,不能相互转化和兼容。
而这个特性就是我们实现热替换的关键。过程如图所示:
# 总结
好了,到这里,基本的类加载器就介绍结束了。我们总结了类加载的工作流程,包括加载,连接,初始化。然后我们重点介绍了加载,因为加载阶段是我们程序员唯一有所作为的地方。然后介绍了加载阶段的一些细节,比如双亲委派,然后说了双亲委派的缺点和补充,然后探讨了如何修改默认的类加载方式,最后通过类加载的特性实现了热替换。当然也看了核心类 ClassLoader 的源码。不过,这肯定不是类加载器的全部。我们将在后面的文章中将类加载的其他特性一一解开。
good luck!!!!
深入浅出 JVM ClassLoader的更多相关文章
- jvm classLoader architecture :
jvm classLoader architecture : a.Bootstrap ClassLoader/启动类加载器 主要负责jdk_home/lib目录下的核心 api 或 - ...
- 深入浅出 JVM GC(3)
# 前言 在 深入浅出 JVM GC(2) 中,我们介绍了一些 GC 算法,GC 名词,同时也留下了一个问题,就是每个 GC 收集器的具体作用.有哪些 GC 收集器呢? Serial 串行收集器(只适 ...
- 深入浅出 JVM GC(2)
# 前言 在 深入浅出 JVM GC(1) 中,限于上篇文章的篇幅,我们留下了一个问题 : 如何回收? 这篇文章将重点讲述这个问题. 在上篇文章中,我们也列出了一些大纲,今天我们就按照那个大纲来逐个讲 ...
- 【深入浅出-JVM】(76):classloader
方法 public Class<?> loadClass(String name) throws ClassNotFoundException 通过类名发挥这个类的Class实例 prot ...
- JVM ClassLoader加载过程
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java虚拟机的类加载机制. 1)三个类加载器: bootstra ...
- 阿里架构师带你深入浅出jvm
本文跟大家聊聊JVM的内部结构,从组件中的多线程处理,JVM系统线程,局部变量数组等方面进行解析 JVM JVM = 类加载器(classloader) + 执行引擎(execution engine ...
- 对JVM还有什么不懂的?一文章带你深入浅出JVM!
本文跟大家聊聊JVM的内部结构,从组件中的多线程处理,JVM系统线程,局部变量数组等方面进行解析 JVM JVM = 类加载器(classloader) + 执行引擎(execution engine ...
- 深入浅出 JVM GC(4)常用 GC 参数介绍
# 前言 从前面的3篇文章中,我们分析了5个垃圾收集器,还有一些 GC 的算法,那么,在 GC 调优中,我们肯定会先判断哪里出现的问题,然后再根据出现的问题进行调优,而调优的手段就是 JVM 提供给我 ...
- 深入浅出 JVM GC(1)
# 前言 初级 Java 程序员步入中级程序员的有一个无法绕过的阶段------GC(Garbage Collection).作为 Java 程序员,说实话,很幸福,不用像 C 程序员那样,时刻关心着 ...
随机推荐
- fly插件飞向购物车
首先载入jQuery库文件和jquery.fly.min.js插件. 插件官方: https://github.com/amibug/fly, 官方例子: http://codepen.io/hzxs ...
- 【慕课网实战】Spark Streaming实时流处理项目实战笔记十五之铭文升级版
铭文一级:[木有笔记] 铭文二级: 第12章 Spark Streaming项目实战 行为日志分析: 1.访问量的统计 2.网站黏性 3.推荐 Python实时产生数据 访问URL->IP信息- ...
- SpringMVC源码解读 - HandlerMapping - RequestMappingHandlerMapping请求分发
AbstractHandlerMethodMapping实现接口getHandlerInternal,定义查找流程 RequestMappingInfoHandlerMapping根据RequestM ...
- HDU 1171 01背包
http://acm.hdu.edu.cn/showproblem.php?pid=1171 基础的01背包,求出总值sum,背包体积即为sum/2 #include<stdio.h> # ...
- Vuejs——(13)组件——杂项
版权声明:出处http://blog.csdn.net/qq20004604 目录(?)[+] 本篇资料来于官方文档: http://cn.vuejs.org/guide/components ...
- bash编程-sed
sed(Stream Editor)是Linux系统下的一个文本流编辑器,它将文本文件内容逐行读取到标准输出,并将此行内容写入模式空间(pattern space),然后按照给定的地址定界和命令处理匹 ...
- git 删除本地分支、远程分支、本地回滚、远程回滚
一. git 删除分支 1. git 删除本地分支 git branch -D branchname 2. git 删除远程分支 git push origin :branchname (origin ...
- C# Autofac集成之Framework WebAPI
Web API 2集成需要Autofac.WebApi2 NuGet包. Web API集成需要Autofac.WebApi NuGet包. Web API集成为控制器,模型绑定器和操作过滤器提供了依 ...
- 47_并发编程-线程python实现
一.Threading模块 1.线程的创建 - 方式一 from threading import Thread import time def sayhi(name): time.sleep(2 ...
- nginx代理tomcat做负载
先对三台服务器统一环境. 对两台tomcat服务器的操作 查看jdk环境 # java -version openjdk version "1.8.0_65" OpenJDK Ru ...