【摘要】
前面系统讨论过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 MyURLClassLoader() {
super(getMyURLs());
}
private static URL[] getMyURLs() {
try {
return new URL[]{new File ("D:/classes/").toURL()};
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
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来观察垃圾收集的情况,对应输出如下:
开始GC
[Full GC[Unloading class MyClass]
207K->131K(1984K), 0.0126452 secs]
GC完成
【测试场景二】使用系统类加载器加载,但是无法将其设置为unreachable的状态
说明:将场景一中的MyClass类型字节码文件放置到工程的输出目录下,以便系统类加载器可以加载
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
开始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类型并没有被卸载.
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来观察垃圾收集的情况,对应输出如下:
开始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类型对应的字节码
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类简要修改如下:重新运行测试场景四中的测试代码
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.
【测试场景六】同一个加载器类型的不同实例重复加载同一类型
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的分析文档。
Java Rebel
转载 http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html
相关推荐
《深入Java虚拟机(原书第2版)》,原书名《Inside the Java Virtual Machine,Second Edition》,作者:【美】Bill Venners,翻译:曹晓钢、蒋靖,出版社:机械工业出版社,ISBN:7111128052,出版日期:2003 年 9 ...
由于无法查看实际的图片和OCR扫描文字内容,我将基于标题和描述提供关于Java虚拟机(JVM)规范的详细知识点。 ### Java虚拟机规范知识点 #### 1. JVM概述 Java虚拟机是运行所有Java程序的抽象计算机,它遵循一定的...
Java虚拟机工作原理详解 Java虚拟机工作原理详解是 Java 程序执行的核心组件之一。了解 Java 虚拟机的工作原理对 Java 开发人员来说非常重要。本文将详细介绍 Java 虚拟机工作原理的详细过程和类加载器的工作机理。...
1. **平台无关性**:Java虚拟机能够屏蔽底层硬件和操作系统差异,使得Java程序可以在任意支持JVM的平台上运行。 2. **安全性**:通过沙箱模型、类型检查等方式,JVM能够有效防止恶意代码的执行。 3. **自动垃圾回收*...
Java虚拟机(JVM)是...通过对这些Java虚拟机的关键知识点的理解和实践,开发者能更好地编写高性能、可扩展的Java应用,并解决内存管理和并发等问题。通过阅读经典书籍,你可以深入学习JVM的内部机制,提升编程技能。
Java虚拟机(JVM,Java Virtual Machine)是Java平台的核心组成部分,它负责执行Java程序,为应用程序提供了一个抽象的硬件和操作系统环境。JVM使得Java具有“一次编写,到处运行”的特性,因为它的目标是实现跨平台...
1.3.1 Java虚拟机 1.3.2 类装载器的体系结构 1.3.3 Java class文件 1.3.4 Java API 1.3.5 Java程序设计语言 1.4 Java体系结构的代价 1.5 结论 1.6 资源页 第2章 平台无关 2.1 为什么要平台...
Java虚拟机(JVM)是Java编程语言的核心组成部分,它为Java程序提供了跨平台的运行环境。深入理解JVM对于优化代码性能、排查问题以及...通过持续学习和实践,开发者能够更好地驾驭Java虚拟机,提高软件系统的整体性能。
Java 虚拟机(JVM)是 Java 语言的运行环境,它负责解释和执行 Java 字节码。下面是 Java 虚拟机相关的知识点: 虚拟机内存结构 Java 虚拟机的内存结构主要包括以下几个部分: * 方法区(Method Area):用于存储...
本篇文章将深入探讨嵌入式Linux环境下Java虚拟机的分析与设计,旨在为读者提供对这一领域的全面理解。 首先,我们来了解嵌入式系统的基本概念。嵌入式系统是指用于特定功能的计算机系统,它们通常被集成到更大的...
Java虚拟机(JVM)的类加载过程是Java程序运行的基础,它涉及到类加载器、类的生命周期和一系列复杂的步骤。在这个过程中,类加载器主要任务是根据类的全限定名加载二进制字节流并转化为`java.lang.Class`对象。整个...
2. 动态添加和删除功能:Java虚拟机支持动态加载和卸载类文件(Class files),这意味着可以根据需要在程序运行时加载新的类库,或者卸载不再需要的类库,从而提供了极大的灵活性和动态性。 3. 高保密性和低风险性...
### Java虚拟机技术分析 Java虚拟机(JVM)作为Java平台的核心,承载着Java语言“一次编写,到处运行”的承诺。本文将深入探讨JVM的组成、运行机制及其在不同平台上的应用,旨在为编程实现JVM或向各种平台移植JVM...
### Java虚拟机(JVM)详解 #### 一、Java技术与Java平台 Java不仅仅是一种编程语言,更是一种全面的技术体系。Java技术体系主要包括以下几个组成部分: 1. **Java编程语言**:这是一种面向对象的编程语言,提供了...
Java虚拟机(JVM)是运行Java程序的核心环境,它负责解释执行Java字节码,管理和分配内存,以及进行垃圾收集等任务。本文详细探讨了JVM中的垃圾收集器和垃圾收集算法,以帮助开发者深入理解Java虚拟机的内部运作机制...
《Java虚拟机规范(Java SE 7)》不仅是Java开发者不可或缺的参考资料,也为理解和分析Java程序的行为提供了理论基础。通过学习这本书,开发者不仅可以更好地理解Java语言本身,还能掌握如何优化程序性能、解决复杂...
10. **JVM参数调整**:讲解如何通过设置JVM参数来影响其行为,如-Xms、-Xmx、-XX:MaxHeapFreeRatio等,以及如何分析和诊断JVM性能问题。 通过阅读《自己动手写Java虚拟机》,读者不仅能够掌握JVM的核心概念,还能...
Java虚拟机还定义了一系列的数据类型,包括引用类型和基本类型。引用类型包括类类型、接口类型和数组类型。基本类型则包括boolean、byte、char、short、int、long、float和double。 在JVM中,数据类型决定了变量的...
Java虚拟机(JVM)是一种能够运行Java字节码的虚拟机。它不仅可以运行Java语言编写的程序,还能够运行任何编译后符合JVM规范的其他语言的字节码。JVM由三个主要子系统构成:类加载子系统、运行时数据区、执行引擎。...