概述
文章目录
- 1 简单介绍
- 1.1 跨平台运行
- 1.2 编译机制
- 2 类加载机制
- 2.1 加载方式
- 2.2 加载过程
- 2.2.1 加载
- 2.2.2 链接
- 2.2.2.1 验证
- 2.2.2.2 准备
- 2.2.2.3 解析
- 2.2.3 初始化
- 2.2.4 类加载总结
- 2.3 类加载时机
- 3 类加载器
- 3.1 了解类加载器
- 3.2 类加载器分类
- 3.3 类加载机制
- 3.3.1 类加载机制分类
- 3.3.2 双亲委派机制
- 3.3.2.1 双亲原理
- 3.3.2.2 验证双亲原理
- 3.3.2.3 双亲优点
- 3.3.2.4 JVM在搜索类的时候,又是如何判定两个class是相同的
- 3.3.3 深度分析Java的ClassLoader机制(源码级别)
- 3.4 自定义类加载器
- 3.5 其他加载器
- 3.5.1 线程上下文类加载器
- 3.5.2 类加载器与Web容器
- 4 类加载器卸载机制
- 4.1 classLoader的卸载机制
- 4.2 类的生命周期和引用
- 4.2.1 生命周期
- 4.2.2 引用关系
- 4.2.3 类的卸载
1 简单介绍
1.1 跨平台运行
Java
的编译和平台独立性
首先Java
是平台独立性语言(C/C++
就不是,java
一次编译在各个平台上都能执行),这关键就在它的字节码
和JVM
机制。Java
程序编译后不是直接生成硬件平台的可执行代码,而是生成.class
的字节码文件,再交由JVM
翻译成对应硬件平台可执行的代码。(也就是说.java
文件被javac
指令编译为.class
的字节码文件,再由JVM
执行)。
1.2 编译机制
Java
字节码的执行分为:即时编译
和解释执行
,通常采用解释执行
方式
解释执行
:是指解释器通过每次解释并执行一小段代码来完成.class
程序的所有操作
解释执行中有几种优化方式:- 栈顶缓存
将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令而言,就无需再入栈,可以直接在寄存器上进行计算,结果压入操作数站。这样便减少了寄存器和内存的交换开销。 - 部分栈帧共享
被调用方法可将调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了复制参数的开销。 - 执行机器指令
在一些特殊情况下,JVM会执行机器指令以提高速度。
- 栈顶缓存
即时编译
:则是以方法
为单位,将字节码.class
文件一次性翻译为机器码后执行
HotSpot
采用了惰性评估(Lazy Evaluation
)的做法,根据二八定律
(即:自适应优化执行),消耗大部分系统资源的只有那一小部分的代码(热点代码
),而这也就是JIT
所需要编译的部分。JVM
会根据代码每次被执行的情况收集信息并相应地做出一些优化静态提前编译
(Ahead Of Time
,AOT
编译)程序运行前,直接把Java
源码文件(.java
)编译成本地机器码的过程;
优点
: 编译不占用运行时间,可以做一些较耗时的优化,并可加快程序启动; 把编译的本地机器码保存磁盘,不占用内存,并可多次使用;
缺点
:因为Java
语言的动态性(如反射)带来了额外的复杂性,影响了静态编译代码的质量; 一般静态编译不如JIT
编译的质量,这种方式用得比较少;
2 类加载机制
Java
语言是一种具有动态性的解释性语言,类(Class
)只有被加载到JVM
中才能运行。
JVM
会将编译生成的.class
文件加载到内存中,并组织成为一个完整的Java
程序。 这个加载过程则是由类加载器(ClassLoader
和它的子类)来完成的,其实质是把类文件从硬盘读到内存中。
2.1 加载方式
在Java
中类的加载是动态的,它不会一次性加载所有类然后运行,而是先把保证程序能运行的基类先加载到JVM
中,其他类则是在需要时再加载,这样就加快了加载速度,而且节约了程序运行过程中内存的开销
类的加载方式分为:
隐式加载
:程序使用new
等方式创建对象,会隐式的调用类加载器。显式加载
:直接调用class.forName()
方法
2.2 加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM
会通过加载
、连接
、初始化
3
个步骤来对该类进行初始化。如果没有意外,JVM
将会连续完成3
个步骤,所以有时也把这个3
个步骤统称为类加载或类初始化
2.2.1 加载
加载指的是将类的class
文件读入到内存,并为之创建一个java.lang.Class
对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class
对象。
类的加载由类加载器完成,类加载器通常由JVM
提供,这些类加载器也是前面所有程序运行的基础,JVM
提供的这些类加载器通常被称为系统类加载器
。除此之外,开发者可以通过继承ClassLoader
基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载
class
文件,这是前面绝大部分示例程序的类加载方式。 - 从
JAR
包加载class
文件,这种方式也是很常见的,JDBC
编程时用到的数据库驱动类就放在JAR
文件中,JVM
可以从JAR
文件中直接加载该class
文件。 - 通过网络加载
class
文件。 - 把一个
Java
源文件动态编译,并执行加载。
2.2.2 链接
当类被加载之后,系统为之生成一个对应的Class
对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE
中。类连接又可分为如下3
个阶段:验证
,准备
,解析
2.2.2.1 验证
验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
Java
是相对C++
语言是安全的语言,例如它有C++
不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java
非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全
。
验证的目的在于确保Class文件
的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证
,元数据验证
,字节码验证
,符号引用验证
验证需要四步验证:
class
文件校验器需要四趟独立扫描
来完成验证工作,其中:
第一趟扫描在装载时进行,会对class
文件进行结构检查,如
(1) 对魔数
进行检查,以判断该文件是否是一个正常的class
文件
(2) 对主次版本号
进行检查,以判断class
文件是否与java
虚拟机兼容
(3) 对class
文件的长度和类型进行检查,避免class
文件部分缺失或被附加内容
第二趟扫描在连接过程中进行,会对类型数据进行语义检查,主要检查各个类的二进制兼容性(主要是查看超类和子类的关系)和类本身是否符合特定的语义条件
(1) final
类不能拥有子类
(2) final
方法不能被重写(覆盖)
(3) 子类和超类之间没有不兼容的方法声明
(4) 检查常量池入口类型是否一致(如CONSTANT_Class
常量池的内容是否指向一个CONSTANT_Utf8
字符串常量池)
(5) 检查常量池的所有特殊字符串,以确定它们是否是其所属类型的实例,以及是否符合特定的上下文无关语法、格式
第三趟扫描为字节码验证
,其验证内容和实现较为复杂,主要检验字节码是否可以被java
虚拟机安全地执行,分析数据流和控制
,确定语义是合法的,符合逻辑的,主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现
第四趟扫描在解析过程中进行,为符号引用验证
。在动态连接过程中,通过保存在常量池的符号引用查找被引用的类、接口、字段、方法时,在把符号引用替换成直接引用时,首先需要确认查找的元素真正存在,然后需要检查访问权限、查找的元素是否是静态类成员而非实例成员,主要是要保证引用一定会被访问到
,不会出现类等无法访问的问题。
2.2.2.2 准备
类准备阶段负责为类的静态变量
分配内存,并设置默认初始值
2.2.2.3 解析
将类的二进制数据中的符号引用
替换成直接引用
。
Java
之所以是符号引用而不是像c语言
那样,编译时直接指定其他类型,是因为java
是动态绑定
的,只有在运行时根据某些规则才能确定具体依赖的类型实例,这正是java
实现多态
的基础
说明一下符号引用和直接引用区别:
符号引用
:是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关直接引用
:是指向目标的指针,偏移量或者能够直接定位的句柄
。该引用是和内存中的布局有关的,并且一定加载进来的。
2.2.3 初始化
初始化是为类的静态变量赋予正确的初始值
,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的
如果类中有语句:private static int a = 10
,它的执行过程是这样的:
首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a
分配内存,因为变量a
是static
的,所以此时a
等于int
类型的默认初始值0
,即a=0
,然后到解析,到初始化这一步骤时,才把a
的真正的值10
赋给a
,此时a=10
2.2.4 类加载总结
类的加载主要分为3步:
装载
:根据查找路径找到相应的class
文件,然后倒入。链接
:
- 检查:检查待记载的
class
文件的正确性。- 准备:给类中的静态变量分配存储空间。(这里用到了
static
关键字的知识)- 解析:将符号引用转换成直接引用(此步是可选的)
初始化
:对静态变量和静态代码块执行初始化工作。这个阶段才是真正开始执行类中的字节码
2.3 类加载时机
点击此处了解类初始化分析
3 类加载器
当我们写好一个Java
程序之后,不是管是CS
还是BS
应用,都是由若干个.class
文件组织而成的一个完整的Java
应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class
文件当中,所以经常要从这个class
文件中要调用另外一个class
文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class
文件,而是根据程序的需要,通过Java
的类加载机制(ClassLoader
)来动态加载某个class
文件到内存当中的,从而只有class
文件被载入到了内存之后,才能被其它class
所引用。所以ClassLoader
就是用来动态加载class
文件到内存当中用的
3.1 了解类加载器
类加载器
负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class
实例对象。一旦一个类被加载到JVM
中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM
的类也有一个唯一的标识。
在Java
中,一个类用其全限定类名
(包括包名和类名)作为标识
;但在JVM
中,一个类用其全限定类名和其类加载器
作为其唯一标识。
例如,如果在pg
的包中有一个名为Person
的类,被类加载器ClassLoader
的实例kl
负责加载,则该Person
类对应的Class
对象在JVM
中表示为(Person.pg.kl
)。这意味着两个类加载器加载的同名类:(Person.pg.kl
)和(Person.pg.kl2
)是不同的、它们所加载的类也是完全不同、互不兼容的。
3.2 类加载器分类
类加载器的图示:
JVM
预定义有三种类加载器,当一个JVM
启动的时候,Java
开始使用如下三种类加载器:
-
根类加载器(
bootstrap classloader
):它用来加载Java
的核心类,是用原生代码来实现的,并且不继承自java.lang.ClassLoader
(负责加载$JAVA_HOME
中jre/lib/rt.jar
里所有的class
,由C++
实现,不是ClassLoader子类
)。
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
通过查找sun.boot.class.path
这个系统属性即可得知
System.out.println(System.getProperty("sun.boot.class.path"));
-
扩展类加载器(
extensions classloader
):它负责加载JRE
的扩展目录,lib/ext
或者由java.ext.dirs
系统属性指定的目录中的JAR
包的类。由Java
语言实现,父类加载器为null
假如当我们使用根加载器加载的对象使用此方法获取到的ClassLoader
是null
,为什么是这样呢?前面已经说了,根类加载器是
使用C++
编写的,JVM
不能够也不允许程序员获取该类,所以返回的是null
,还有一点,如果此对象表示的是一个基本类型或void
,则返回null
,其实进一步的含义就是:Java
中所有的基本数据类型
都是由根加载器加载的 -
系统类加载器(
app classloader
):被称为系统(也称为应用
)类加载器,它负责在JVM
启动时加载来自Java
命令的-classpath
选项、java.class.path
系统属性,或者CLASSPATH
换将变量所指定的JAR包
和类路径
。程序可以通过ClassLoader
的静态方法getSystemClassLoader()
来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java
语言实现,父类加载器为ExtClassLoader
-
自定义类类加载器:这些自定义的
ClassLoader
都必须继承自java.lang.ClassLoader
类,也包括Java
提供的另外二个ClassLoader
(Extension ClassLoader
和App ClassLoader
)在内,但是Bootstrap ClassLoader
不继承自ClassLoader
,因为它不是一个普通的Java
类,底层由C++
编写,已嵌入到了JVM
内核当中,当JVM
启动后,Bootstrap ClassLoader
也随着启动,负责加载完核心类库后,并构造Extension ClassLoader
和App ClassLoader
类加载器
3.3 类加载机制
3.3.1 类加载机制分类
JVM
的类加载机制主要有如下3
种:
全盘负责
:是指当一个类加载器负责加载某个Class
时,该Class
所依赖和引用其他Class
也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入双亲委派
:所谓的双亲委派
,则是先让父类加载器试图加载该Class
,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才给子类去加载缓存机制
:会保证所有加载过的Class
都会被缓存,当程序中需要使用某个Class
时,类加载器先从缓存区中搜寻该Class
,只有当缓存区中不存在该Class
对象时,系统才会读取该类对应的二进制数据,并将其转换成Class
对象,存入缓冲区中。这就是为什么修改了Class
后,必须重新启动JVM
,程序所做的修改才会生效的原因。
或者这样类加载器三个机制:委托
、单一性
、可见性
委托
:指加载一个类的请求交给父类加载器,若父类加载器不可以找到或者加载到,再加载这个类单一性
:指子类加载器不会再次加载父类加载器已经加载过的类可见性
:子类加载器可以看见父类加载器加载的所有类,而父类加载器不可以看到子类加载器加载的类
3.3.2 双亲委派机制
3.3.2.1 双亲原理
双亲委派机制
,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
注意一点:加载器之间不是继承关系,而是组合关系
3.3.2.2 验证双亲原理
- 验证双亲原理一:
ClassLoader loader = MapDemo.class.getClassLoader();//获得加载ClassLoaderTest.class这个类的类加载器
while(loader != null) {
System.out.println(loader);
loader = loader.getParent(); //获得父类加载器的引用
}
System.out.println(loader);
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@27716f4
null
运行结果分析:
第一行结果说明:MapDemo的类加载器是AppClassLoader。
第二行结果说明:AppClassLoader的类加载器是ExtClassLoader,即parent=ExtClassLoader。
第三行结果说明:ExtClassLoader的类加器是Bootstrap ClassLoader,
因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因
- 验证双亲原理二:
将MapDemo.class
打包成MapDemo.jar
,放到Extension ClassLoader
的加载目录下(JAVA_HOME/jre/lib/ext
),然后重新运行这个程序,得到的结果会是什么样呢
运行结果:
sun.misc.Launcher$ExtClassLoader@27716f4
null
运行结果分析:
为什么第一行的结果是ExtClassLoader
呢?
因为ClassLoader
的委托模型机制,当我们要用MapDemo.class
这个类的时候,AppClassLoader
在试图加载之前,先委托给Bootstrcp ClassLoader
,Bootstracp ClassLoader
发现自己没找到,它就告诉ExtClassLoader
,兄弟,我这里没有这个类,你去加载看看,然后Extension ClassLoader
拿着这个类去它指定的类路径(JAVA_HOME/jre/lib/ext
)试图加载,唉,它发现在MapDemo.jar
这样一个文件中包含MapDemo.class
这样的一个文件,然后它把找到的这个类加载到内存当中,并生成这个类的Class
实例对象,最后把这个实例返回。所以MapDemo.class
的类加载器是ExtClassLoader
。
第二行的结果为null
,是因为ExtClassLoader
的父类加载器是Bootstrap ClassLoader
- 验证双亲原理三:
用Bootstrcp ClassLoader
来加载MapDemo.class
,有两种方式:
方式一:在jvm
中添加-Xbootclasspath
参数,指定Bootstrcp ClassLoader
加载类的路径,并追加我们自已的jar
(MapDemo.jar)
方式二:将class
文件放到JAVA_HOME/jre/classes/
目录下
将MapDemo.jar
解压后,放到JAVA_HOME/jre/classes
目录下,如下图所示:
提示:jre
目录下默认没有classes
目录,需要自己手动创建一个
3.3.2.3 双亲优点
采用双亲委派模式的是好处是Java
类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要让子ClassLoader
再加载一次
其次是考虑到安全因素,java
核心api
中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API
发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的Integer.class
,这样便可以防止核心API
库被随意篡改。
3.3.2.4 JVM在搜索类的时候,又是如何判定两个class是相同的
JVM
在判定两个class
是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM
才认为这两个class
是相同的。就算两个class
是同一份class
字节码,如果被两个不同的ClassLoader
实例所加载,JVM
也会认为它们是两个不同class
。比如网络上的一个Java
类org.classloader.simple.NetClassLoaderSimple
,javac
编译之后生成字节码文件NetClassLoaderSimple.class
,ClassLoaderA
和ClassLoaderB
这两个类加载器并读取了NetClassLoaderSimple.class
文件,并分别定义出了java.lang.Class
实例来表示这个类,对于JVM
来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class
实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException
,提示这是两个不同的类型
3.3.3 深度分析Java的ClassLoader机制(源码级别)
class loader
是一个负责加载classes
的对象,ClassLoader
类是一个抽象类,需要给出类的二进制名称,class loader
尝试定位或者产生一个class
的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。
接下来我们看loadClass方法的实现方式:
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;
}
}
使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String)
方法检查这个类是否被加载过使用父加载器调用loadClass(String)
方法,如果父加载器为Null
,类加载器装载虚拟机内置的加载器调用findClass(String)
方法装载类,如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve
参数的值为true
,那么就调用resolveClass(Class)
方法来处理类。ClassLoader
的子类最好覆盖findClass(String)
而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)
接下来,我们开始分析该方法。
protected Class> loadClass(String name, boolean resolve)
该方法的访问控制符是protected
,也就是说该方法 同包内和派生类中可用 返回值类型Class
首先,在ClassLoader
类中有一个静态内部类ParallelLoaders
,会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么就给parallelLockMap
定义,就是new一个ConcurrentHashMap()
,那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么parallelLockMap
就不是Null
,这个时候,我们判断parallelLockMap
是不是Null
,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock
方法直接返回this,就是当前的加载器的一个实例。如果这个parallelLockMap
不是null
,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。
然后就是创建一个新的Object对象,调用parallelLockMap
的putIfAbsent(className, newLock)
方法,这个方法的作用是:首先根据传进来的className
,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的Object
对象作为value值,className
作为Key
值组成一个map
返回。然后无论putIfAbsent
方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object
对象。
简单说明一下getClassLoadingLock(String className)
的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader
对象注册了并行能力,方法返回一个与指定的名字className
相关联的特定对象,否则,直接返回当前的ClassLoader
对象。
Class c = findLoadedClass(name);
在这里,在加载类之前先调用findLoadedClass
方法检查该类是否已经被加载过,findLoadedClass
会返回一个Class
类型的对象,如果该类已经被加载过,那么就可以直接返回该对象(在返回之前会根据resolve的值来决定是否处理该对象)。 如果,该类没有被加载过,那么执行以下的加载过程
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
}
3.4 自定义类加载器
既然JVM
已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java
中提供的默认ClassLoader
,只加载指定目录下的jar
和class
,如果我们想加载其它位置的类或jar
时,比如:要加载网络上的一个class
文件,通过动态加载到内存之后,要调用这个类中的方法实现业务逻辑。在这样的情况下,默认的ClassLoader
就不能满足我们的需求了,所以需要定义自己的ClassLoader
。
定义自已的类加载器分为两步:
- 继承
java.lang.ClassLoader
- 重写父类的
findClass
方法
那么父类有那么多方法,为什么偏偏只重写findClass
方法?
因为JDK
已经在loadClass
方法中帮我们实现了ClassLoader
搜索类的算法,当在loadClass
方法中搜索不到类时,loadClass
方法就会调用findClass
方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass
搜索类的算法
示例:自定义一个NetworkClassLoader
,用于加载网络上的class文件
package classloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
/**
* 加载网络class的ClassLoader
*/
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;//this.findLoadedClass(name); // 父类已加载
byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
if (classData == null) {
throw new ClassNotFoundException();
}
clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
return clazz;
}
private byte[] getClassData(String name) {
InputStream is = null;
try {
String path = classNameToPath(name);
URL url = new URL(path);
byte[] buff = new byte[1024*4];
int len = -1;
is = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((len = is.read(buff)) != -1) {
baos.write(buff,0,len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return rootUrl + "/" + name.replace(".", "/") + ".class";
}
}
3.5 其他加载器
3.5.1 线程上下文类加载器
线程上下文类加载器(context class loader
)是从 JDK 1.2 开始引入的。类 java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java
应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源
前面提到的类加载器的代理模式并不能解决 Java
应用开发中会遇到的类加载器的全部问题。Java
提供了很多服务提供者接口(Service Provider Interface,SPI
),允许第三方为这些接口提供实现。常见的 SPI
有 JDBC、JCE、JNDI、JAXP 和 JBI
等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH
)来找到,如实现了 JAXP SPI
的 Apache Xerces
所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而问题在于,SPI
的接口是 Java
核心库的一部分,是由引导类加载器来加载的;SPI
实现的 Java
类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI
的实现类的,因为它只加载 Java
的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java
应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI
接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI
实现的类。线程上下文类加载器在很多 SPI
的实现中都会用到
3.5.2 类加载器与Web容器
对于运行在 Java EE
容器中的 Web
应用来说,类加载器的实现方式与一般的 Java
应用有所不同。不同的 Web
容器的实现方式也会有所不同。以 Apache Tomcat
来说,每个 Web
应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是 首先尝试去加载某个类,如果找不到再代理给父类加载器
。这与一般类加载器的顺序是相反的。这是 Java Servlet
规范中的推荐做法,其目的是使得 Web
应用自己的类的优先级高于 Web
容器提供的类。这种代理模式的一个例外是:Java
核心库的类是不在查找范围之内的。这也是为了保证 Java
核心库的类型安全。
绝大多数情况下,Web
应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
- 每个
Web
应用自己的Java
类文件和使用的库的jar
包,分别放在WEB-INF/classes
和WEB-INF/lib
目录下面。 - 多个应用共享的
Java
类文件和jar
包,分别放在Web
容器指定的由所有Web
应用共享的目录下面。 - 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确
4 类加载器卸载机制
4.1 classLoader的卸载机制
jvm
中没有提供class
及classloader
的unload
方法,那热部署及osgi中是通过什么机制来实现的呢?实现思路主要是通过更换classLoader
进行重新加载。之前的classloader
及加载的class
类在没有实例引用的情况下,在perm区gc的情况下会被回收掉.
perm区gc时回收掉没有引用的class是一个怎样的过程呢?
perm区达到回收条件后,对class进行引用计算,对于没有引用的class进行回收
如果有实例类有对classloader的引用,perm区class将无法卸载,导致perm区内存一直增加,进而导致perm space error
public static Map pool = new HashMap();
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
for (int i=0;i<10000000;i++){
test(args);
}
}
public static void test(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader cl = new MyLoader(Main.class.getClassLoader());
String className = "RealPerson";
@SuppressWarnings("unchecked")
Class<Person> clazz = (Class<Person>) cl.loadClass(className);
Person p = clazz.newInstance();
p.setName("qiang");
pool.put(System.nanoTime(), p);
cl = p.getClass().getClassLoader();
}
推测:
osgi的bundle进行热部署时有个条件:export class 必须是兼容的
.否则需要重启整个应用才会生效,为什么呢?
osgi
的export class
是被bundle
的parent classloader
加载的,bundle内部其他类是bundle
的classloader
加载的,bundle更换后,重新创建classloader,并对bundle进行加载,之前的加载靠jmv gc回收掉.
那osgi中explort class
如果有实例引用的话,是否会导致class无法被gc掉?
如果osgi中没有做过处理,应该会出现此问题
4.2 类的生命周期和引用
4.2.1 生命周期
当Sample类被加载、连接和初始化后,它的生命周期就开始了。
当代表Sample类的Class对象不再被引用,即不可触及时(点击了解GC Root 相关知识 ),Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
4.2.2 引用关系
加载器和Class对象:
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。
另一方面,一个Class对象总是会引用它的类加载器。调用Class对象的getClassLoader()
方法,就能获得它的类加载器。
由此可见,Class实例和加载它的加载器之间为双向关联关系。
类、类的Class对象、类的实例对象:
一个类的实例总是引用代表这个类的Class对象。
在Object
类中定义了getClass()
方法,这个方法返回代表对象所属类的Class
对象的引用。
此外,所有的Java类都有一个静态属性class
,它引用代表这个类的Class对象
4.2.3 类的卸载
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。
Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class
对象,因此这些Class对象始终是可触及的。
由用户自定义的类加载器加载的类是可以被卸载的。
loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。
当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)
最后
以上就是爱笑早晨为你收集整理的JVM加载class文件原理1 简单介绍2 类加载机制3 类加载器4 类加载器卸载机制的全部内容,希望文章能够帮你解决JVM加载class文件原理1 简单介绍2 类加载机制3 类加载器4 类加载器卸载机制所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复