【转】Java虚拟机类型卸载和类型更新解析
【摘要】
前面系统讨论过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;
}
}
}
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的分析文档。
【转】Java虚拟机类型卸载和类型更新解析的更多相关文章
- Java虚拟机类型卸载和类型更新解析(转)
转自:http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html [摘要] 前面系统讨论过java 类型加载(loa ...
- 深入理解Java虚拟机--个人总结(持续更新)
深入理解Java虚拟机--个人总结(持续更新) 每天按照书本学一点,会把自己的总结思考写下来,形成输出,持续更新,立帖为证 -- 2020年7月7日 开始第一次学习 -- 2020年7月8日 今天在百 ...
- Java 虚拟机 最易理解的 全面解析
先上一个最容易理解的类实例化的内存模型案例截图: 转载自:https://www.zybuluo.com/Yano/note/321063 周志明著的<深入理解 Java 虚拟机>的干货~ ...
- 深入理解java虚拟机(十一) 方法调用-解析调用与分派调用
方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程.我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是 ...
- 深入Java虚拟机——类型装载、连接(转)
来自http://hi.baidu.com/holder/item/c38abf02de14c7d31ff046e0 Java虚拟机通过装载.连接和初始化一个Java类型,使该类型可以被正在运行的Ja ...
- 深入Java虚拟机读书笔记第五章Java虚拟机
Java虚拟机 Java虚拟机之所以被称之为是虚拟的,就是因为它仅仅是由一个规范来定义的抽象计算机.因此,要运行某个Java程序,首先需要一个符合该规范的具体实现. Java虚拟机的生命周期 一个运行 ...
- <<深入Java虚拟机>>-第二章-Java内存区域-学习笔记
Java运行时内存区域 Java虚拟机在运行Java程序的时候会将它所管理的内存区域划分为多个不同的区域.每个区域都有自己的用途,创建以及销毁的时间.有的随着虚拟机的启动而存在,有的则是依赖用户线程来 ...
- 深入理解java虚拟机系列初篇(一):为什么要学习JVM?
前言 本来想着关于写JVM这个专栏,直接写知识点干货的,但是想着还是有必要开篇讲一下为什么要学习JVM,这样的话让一些学习者心里有点底的感觉比较好... 原因一:面试 不得不说,随着互联网门槛越来越高 ...
- 深入Java虚拟机
第一章:Java体系结构介绍 1.Java为什么重要? Java是为网络而设计的,而Java这种适合网络环境的能力又是由其体系结构决定的,可以保证安全健壮和平台无关的程序通过网络传播. 2 ...
随机推荐
- zabbix 报警程序
一,报警程序 本次使用的为onealert http://c.onealert.com/console/ucid/login.jsp 二,服务端安转 下面为他教的怎么安装这个东西 第一步: 找到脚本目 ...
- Dubbo解析及原理浅析
原文链接:https://blog.csdn.net/chao_19/article/details/51764150 一.Duboo基本概念解释 Dubbo是一种分布式服务框架. Webservic ...
- 《HTTP权威指南》之HTTP连接管理及对TCP性能的考虑
在上一篇博客中(<HTTP权威指南>之HTTP相关概念详解)我们简单对HTTP相关的基本概念做了一些简单的了解,但未对HTTP连接管理的内容做一些详细的介绍.本篇博客我们就一起来看一下HT ...
- adb调试安卓
http://blog.csdn.net/liushida00/article/details/49797239
- Unity3d Attribute 总结
举两个例子,在变量上使用[SerializeFiled]属性,可以强制让变量进行序列化,可以在Unity的Editor上进行赋值. 在Class上使用[RequireComponent]属性,就会在C ...
- BNU4299——God Save the i-th Queen——————【皇后攻击,找到对应关系压缩空间】
God Save the i-th Queen Time Limit: 5000ms Memory Limit: 65536KB 64-bit integer IO format: %lld ...
- Node.js 框架对比之 Express VS Koa
背景 上图是一个典型的采用 Node.js 开发 web 应用的前后端结构,下面介绍一下 Node 服务层在其中的作用以及使用 Node.js 的一些优劣. Node 服务层作用: 请求代理 传统做法 ...
- [转].NET Core之Entity Framework Core 你如何创建 DbContext
本文转自:http://www.cnblogs.com/tdws/p/5874212.html 本文版权归博客园和作者吴双共同所有,欢迎转载,转载和爬虫请注明博客园蜗牛原文地址 http://www. ...
- [译]理解 Windows UI 动画引擎
本文译自 Nick Waggoner 的 "Understand what’s possible with the Windows UI Animation Engine",已获原 ...
- bootstrap fileinput+MVC 上传多文件,保存
新增用户资料,需要用户上传多笔附件,所以就尝试用了fileinput控件,显示效果如图: 首先,先在model中定义数据模型: public partial class create { [Requi ...