【转载】Java虚拟机类型卸载和类型更新解析
连接:http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html
【摘要】
前面系统讨论过java类型加载(loading)的问题,在这篇文章中简要分析一下java类型卸载(unloading)的问题,并简要分析一下如何解决如何运行时加载newly compiled version的问题。
【相关规范摘要】
首先看一下,关于java虚拟机规范中时如何阐述类型卸载(unloading)的:
A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
Java虚拟机规范中关于类型卸载的内容就这么简单两句话,大致意思就是:只有当加载该类型的类加载器实例(非类加载器类型)为unreachable状态时,当前被加载的类型才被卸载.启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载.
我们再看一下Java语言规范提供的关于类型卸载的更详细的信息(部分摘录):
//摘自JLS 12.7 Unloading of Classes and Interfaces
1、An implementation of the Java programming language may unload classes.
2、Class unloading is an optimization that helps reduce memory use. Obviously,the semantics of a program should not depend on whether and how a system chooses to implement an optimization such as class unloading.
3、Consequently,whether a class or interface has been unloaded or not should be transparent to a program
通过以上我们可以得出结论: 类型卸载(unloading)仅仅是作为一种减少内存使用的性能优化措施存在的,具体和虚拟机实现有关,对开发者来说是透明的.
纵观java语言规范及其相关的API规范,找不到显示类型卸载(unloading)的接口, 换句话说:
1、一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的
2、一个被特定类加载器实例加载的类型运行时可以认为是无法被更新的
【类型卸载进一步分析】
前面提到过,如果想卸载某类型,必须保证加载该类型的类加载器处于unreachable状态,现在我们再看看有 关unreachable状态的解释:
1、A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
2、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means.
某种程度上讲,在一个稍微复杂的java应用中,我们很难准确判断出一个实例是否处于unreachable状态,所 以为了更加准确的逼近这个所谓的unreachable状态,我们下面的测试代码尽量简单一点.
【测试场景一】使用自定义类加载器加载, 然后测试将其设置为unreachable的状态
说明:
1、自定义类加载器(为了简单起见, 这里就假设加载当前工程以外D盘某文件夹的class)
2、假设目前有一个简单自定义类型MyClass对应的字节码存在于D:/classes目录下
public class MyURLClassLoader extends URLClassLoader {
public MyURLClassLoader() {
super(getMyURLs());
}
private static URL[] getMyURLs() {
try {
return new URL[]{new File ("D:/classes/").toURL()};
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
1 public class Main {
2 public static void main(String[] args) {
3 try {
4 MyURLClassLoader classLoader = new MyURLClassLoader();
5 Class classLoaded = classLoader.loadClass("MyClass");
6 System.out.println(classLoaded.getName());
7
8 classLoaded = null;
9 classLoader = null;
10
11 System.out.println("开始GC");
12 System.gc();
13 System.out.println("GC完成");
14 } catch (Exception e) {
15 e.printStackTrace();
16 }
17 }
18 }
我们增加虚拟机参数-verbose:gc来观察垃圾收集的情况,对应输出如下:
MyClass
开始GC
[Full GC[Unloading class MyClass]
207K->131K(1984K), 0.0126452 secs]
GC完成
【测试场景二】使用系统类加载器加载,但是无法将其设置为unreachable的状态
说明:将场景一中的MyClass类型字节码文件放置到工程的输出目录下,以便系统类加载器可以加载
1 public class Main {
2 public static void main(String[] args) {
3 try {
4 Class classLoaded = ClassLoader.getSystemClassLoader().loadClass(
5 "MyClass");
6
7
8 System.out.printl(sun.misc.Launcher.getLauncher().getClassLoader());
9 System.out.println(classLoaded.getClassLoader());
10 System.out.println(Main.class.getClassLoader());
11
12 classLoaded = null;
13
14 System.out.println("开始GC");
15 System.gc();
16 System.out.println("GC完成");
17
18 //判断当前系统类加载器是否有被引用(是否是unreachable状态)
19 System.out.println(Main.class.getClassLoader());
20 } catch (Exception e) {
21 e.printStackTrace();
22 }
23 }
24 }
我们增加虚拟机参数-verbose:gc来观察垃圾收集的情况, 对应输出如下:
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$AppClassLoader@197d257
开始GC
[Full GC 196K->131K(1984K), 0.0130748 secs]
GC完成
sun.misc.Launcher$AppClassLoader@197d257
由于系统ClassLoader实例(AppClassLoader@197d257">sun.misc.Launcher$AppClassLoader@197d257)加载了很多类型,而且又没有明确的接口将其设置为null,所以我们无法将加载MyClass类型的系统类加载器实例设置为unreachable状态,所以通过测试结果我们可以看出,MyClass类型并没有被卸载.(说明: 像类加载器实例这种较为特殊的对象一般在很多地方被引用, 会在虚拟机中呆比较长的时间)
【测试场景三】使用扩展类加载器加载, 但是无法将其设置为unreachable的状态
说明:将测试场景二中的MyClass类型字节码文件打包成jar放置到JRE扩展目录下,以便扩展类加载器可以加载的到。由于标志扩展ClassLoader实例(ExtClassLoader@7259da">sun.misc.Launcher$ExtClassLoader@7259da)加载了很多类型,而且又没有明确的接口将其设置为null,所以我们无法将加载MyClass类型的系统类加载器实例设置为unreachable状态,所以通过测试结果我们可以看出,MyClass类型并没有被卸载.
1 public class Main {
2 public static void main(String[] args) {
3 try {
4 Class classLoaded = ClassLoader.getSystemClassLoader().getParent()
5 .loadClass("MyClass");
6
7 System.out.println(classLoaded.getClassLoader());
8
9 classLoaded = null;
10
11 System.out.println("开始GC");
12 System.gc();
13 System.out.println("GC完成");
14 //判断当前标准扩展类加载器是否有被引用(是否是unreachable状态)
15 System.out.println(Main.class.getClassLoader().getParent());
16 } catch (Exception e) {
17 e.printStackTrace();
18 }
19 }
20 }
我们增加虚拟机参数-verbose:gc来观察垃圾收集的情况,对应输出如下:
sun.misc.Launcher$ExtClassLoader@7259da
开始GC
[Full GC 199K->133K(1984K), 0.0139811 secs]
GC完成
sun.misc.Launcher$ExtClassLoader@7259da
关于启动类加载器我们就不需再做相关的测试了,jvm规范和JLS中已经有明确的说明了.
【类型卸载总结】
通过以上的相关测试(虽然测试的场景较为简单)我们可以大致这样概括:
1、有启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范).
2、被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小.(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则).
3、被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的).
综合以上三点,我们可以默认前面的结论1, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的.同时,我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能.
【类型更新进一步分析】
前面已经明确说过,被一个特定类加载器实例加载的特定类型在运行时是无法被更新的.注意这里说的
是一个特定的类加载器实例,而非一个特定的类加载器类型.
【测试场景四】
说明:现在要删除前面已经放在工程输出目录下和扩展目录下的对应的MyClass类型对应的字节码
1 public class Main {
2 public static void main(String[] args) {
3 try {
4 MyURLClassLoader classLoader = new MyURLClassLoader();
5 Class classLoaded1 = classLoader.loadClass("MyClass");
6 Class classLoaded2 = classLoader.loadClass("MyClass");
7 //判断两次加载classloader实例是否相同
8 System.out.println(classLoaded1.getClassLoader() == classLoaded2.getClassLoader());
9
10 //判断两个Class实例是否相同
11 System.out.println(classLoaded1 == classLoaded2);
12 } catch (Exception e) {
13 e.printStackTrace();
14 }
15 }
16 }
输出如下:
true
true
通过结果我们可以看出来,两次加载获取到的两个Class类型实例是相同的.那是不是确实是我们的自定义
类加载器真正意义上加载了两次呢(即从获取class字节码到定义class类型…整个过程呢)?
通过对java.lang.ClassLoader的loadClass(String name,boolean resolve)方法进行调试,我们可以看出来,第二
次 加载并不是真正意义上的加载,而是直接返回了上次加载的结果.
说明:为了调试方便, 在Class classLoaded2 = classLoader.loadClass("MyClass");行设置断点,然后单步跳入, 可以看到第二次加载请求返回的结果直接是上次加载的Class实例. 调试过程中的截图 最好能自己调试一下).
【测试场景五】同一个类加载器实例重复加载同一类型
说明:首先要对已有的用户自定义类加载器做一定的修改,要覆盖已有的类加载逻辑, MyURLClassLoader.java类简要修改如下:重新运行测试场景四中的测试代码
1 public class MyURLClassLoader extends URLClassLoader {
2 //省略部分的代码和前面相同,只是新增如下覆盖方法
3 /*
4 * 覆盖默认的加载逻辑,如果是D:/classes/下的类型每次强制重新完整加载
5 *
6 * @see java.lang.ClassLoader#loadClass(java.lang.String)
7 */
8 @Override
9 public Class<?> loadClass(String name) throws ClassNotFoundException {
10 try {
11 //首先调用系统类加载器加载
12 Class c = ClassLoader.getSystemClassLoader().loadClass(name);
13 return c;
14 } catch (ClassNotFoundException e) {
15 // 如果系统类加载器及其父类加载器加载不上,则调用自身逻辑来加载D:/classes/下的类型
16 return this.findClass(name);
17 }
18 }
19 }
说明: this.findClass(name)会进一步调用父类URLClassLoader中的对应方法,其中涉及到了defineClass(String name)的调用,所以说现在类加载器MyURLClassLoader会针对D:/classes/目录下的类型进行真正意义上的强制加载并定义对应的类型信息.
测试输出如下:
Exception in thread "main" java.lang.LinkageError: duplicate class definition: MyClass
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)
at java.net.URLClassLoader.access$100(URLClassLoader.java:56)
at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at MyURLClassLoader.loadClass(MyURLClassLoader.java:51)
at Main.main(Main.java:27)
结论:如果同一个类加载器实例重复强制加载(含有定义类型defineClass动作)相同类型,会引起java.lang.LinkageError: duplicate class definition.
【测试场景六】同一个加载器类型的不同实例重复加载同一类型
1 public class Main {
2 public static void main(String[] args) {
3 try {
4 MyURLClassLoader classLoader1 = new MyURLClassLoader();
5 Class classLoaded1 = classLoader1.loadClass("MyClass");
6 MyURLClassLoader classLoader2 = new MyURLClassLoader();
7 Class classLoaded2 = classLoader2.loadClass("MyClass");
8
9 //判断两个Class实例是否相同
10 System.out.println(classLoaded1 == classLoaded2);
11 } catch (Exception e) {
12 e.printStackTrace();
13 }
14 }
15 }
测试对应的输出如下:
false
【类型更新总结】
由不同类加载器实例重复强制加载(含有定义类型defineClass动作)同一类型不会引起java.lang.LinkageError错误, 但是加载结果对应的Class类型实例是不同的,即实际上是不同的类型(虽然包名+类名相同). 如果强制转化使用,会引起ClassCastException.(说明: 头一段时间那篇文章中解释过,为什么不同类加载器加载同名类型实际得到的结果其实是不同类型, 在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间).
应用场景:我们在开发的时候可能会遇到这样的需求,就是要动态加载某指定类型class文件的不同版本,以便能动态更新对应功能.
建议:
1. 不要寄希望于等待指定类型的以前版本被卸载,卸载行为对java开发人员透明的.
2. 比较可靠的做法是,每次创建特定类加载器的新实例来加载指定类型的不同版本,这种使用场景下,一般就要牺牲缓存特定类型的类加载器实例以带来性能优化的策略了.对于指定类型已经被加载的版本, 会在适当时机达到unreachable状态,被unload并垃圾回收.每次使用完类加载器特定实例后(确定不需要再使用时), 将其显示赋为null, 这样可能会比较快的达到jvm 规范中所说的类加载器实例unreachable状态, 增大已经不再使用的类型版本被尽快卸载的机会.
3. 不得不提的是,每次用新的类加载器实例去加载指定类型的指定版本,确实会带来一定的内存消耗,一般类加载器实例会在内存中保留比较长的时间. 在bea开发者网站上找到一篇相关的文章(有专门分析ClassLoader的部分):http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html
写的过程中参考了jvm规范和jls, 并参考了sun公司官方网站上的一些bug的分析文档。
欢迎大家批评指正!
本博客中的所有文章、随笔除了标题中含有引用或者转载字样的,其他均为原创。转载请注明出处,谢谢!
相关推荐
由于无法查看实际的图片和OCR扫描文字内容,我将基于标题和描述提供关于Java虚拟机(JVM)规范的详细知识点。 ### Java虚拟机规范知识点 #### 1. JVM概述 Java虚拟机是运行所有Java程序的抽象计算机,它遵循一定的...
Java虚拟机工作原理详解 Java虚拟机工作原理详解是 Java 程序执行的核心组件之一。了解 Java 虚拟机的工作原理对 Java 开发人员来说非常重要。本文将详细介绍 Java 虚拟机工作原理的详细过程和类加载器的工作机理。...
类加载过程包括加载、验证、准备、解析和初始化等步骤。 2. 堆内存管理:堆是Java程序中对象的出生地,JVM使用垃圾收集器自动管理内存。新生代、老年代和持久代是堆内存的划分,不同的对象根据其生命周期被分配在...
Java虚拟机(JVM,Java Virtual Machine)是Java平台的核心组成部分,它负责执行Java程序,为应用程序提供了一个抽象的硬件和操作系统环境。JVM使得Java具有“一次编写,到处运行”的特性,因为它的目标是实现跨平台...
8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info 入口 S.1.6 解析CONSTANT_Methodref_info 入口 ...
类加载分为加载、验证、准备、解析和初始化五个阶段。加载找到并加载类文件,验证确保字节码安全,准备为静态变量分配内存,解析将符号引用转换为直接引用,初始化执行类的初始化方法。 3. **运行时数据区** - **...
Java 虚拟机(JVM)是 Java 语言的运行环境,它负责解释和执行 Java 字节码。下面是 Java 虚拟机相关的知识点: 虚拟机内存结构 Java 虚拟机的内存结构主要包括以下几个部分: * 方法区(Method Area):用于存储...
Java虚拟机(JVM)的类加载过程是Java程序运行的基础,它涉及到类加载器、类的生命周期和一系列复杂的步骤。在这个过程中,类加载器主要任务是根据类的全限定名加载二进制字节流并转化为`java.lang.Class`对象。整个...
### Java虚拟机(JVM)详解 #### 一、Java技术与Java平台 Java不仅仅是一种编程语言,更是一种全面的技术体系。Java技术体系主要包括以下几个组成部分: 1. **Java编程语言**:这是一种面向对象的编程语言,提供了...
### Java虚拟机规范(Java SE 7)关键知识点解析 #### 一、概述 《Java虚拟机规范(Java SE 7)》是一本详细介绍Java虚拟机(JVM)工作原理的标准文档,由Tim Lindholm、Frank Yellin、Gilad Bracha、Alex Buckley等...
Java虚拟机还定义了一系列的数据类型,包括引用类型和基本类型。引用类型包括类类型、接口类型和数组类型。基本类型则包括boolean、byte、char、short、int、long、float和double。 在JVM中,数据类型决定了变量的...
8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info 入口 S.1.6 解析CONSTANT_Methodref_info 入口 8.1.7 ...
Java虚拟机(JVM)是一种能够运行Java字节码的虚拟机。它不仅可以运行Java语言编写的程序,还能够运行任何编译后符合JVM规范的其他语言的字节码。JVM由三个主要子系统构成:类加载子系统、运行时数据区、执行引擎。...
- 解释了类的加载、验证、准备、解析和初始化的过程。 - **第9章:垃圾收集** - 介绍了JVM中垃圾回收的工作原理以及不同的垃圾回收算法。 - **第10章:栈和局部变量操作** - 讲解了栈帧的结构、局部变量表的作用...
Java虚拟机(JVM)是Java程序运行的核心组件,它为开发者提供了跨平台的执行环境。深入理解JVM内核的原理、诊断与优化对于提升Java应用的性能至关重要。本教程将带你探索JVM的奥秘,从内存管理到垃圾回收,从编译...
8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info入口 S.1.6 解析CONSTANT_Methodref_info入口 ...
本文将详细介绍Java虚拟机(JVM)的内部机理和实现原理,从类型的生命周期、方法区、常量池、类加载器、垃圾收集器、栈和局部变量等方面对JVM进行深入解析。 类型的生命周期 类型的生命周期是JVM中最重要的部分,...