Java虚拟机范例卸载和范例更新理会
副标题#e#
首先看一下,关于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状态,我们下面的测试代码只管简朴一点.
#p#副标题#e#
【测试场景一】利用自界说类加载器加载,然后测试将其配置为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来调查垃圾收集的环境,对应输出如下:
#p#分页标题#e#
[email protected]
[email protected]
[email protected]
开始GC...
[FullGC196K->131K(1984K),0.0130748 secs]
GC完成...
[email protected]
由于系统ClassLoader实例([email protected]">[email protected])加载了许多范例,并且又没有明晰的接口将其配置为null,所以我们无法将加载MyClass范例的系统类加载器实例配置为unreachable状态,所以通过测试功效我们可以看出,MyClass范例并没有被卸载.(说明: 像类加载器实例这种较为非凡的工具一般在许多处所被引用,会在虚拟机中呆较量长的时间)
【测试场景三】利用扩展类加载器加载,可是无法将其配置为unreachable的状态
说明:将测试场景二中的MyClass范例字节码文件打包成jar安排到JRE扩展目次下,以便扩展类加载器可以加载的到。由于符号扩展ClassLoader实例([email protected]">[email protected])加载了许多范例,并且又没有明晰的接口将其配置为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来调查垃圾收集的环境,对应输出如下:
[email protected]
开始GC...
[FullGC199K->133K(1984K),0.0139811 secs]
GC完成...
[email protected]
关于启动类加载器我们就不需再做相关的测试了,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)要领举办调试,我们可以看出来,第二
次 加载并不是真正意义上的加载,而是直接返回了上次加载的功效.
#p#分页标题#e#
说明:为了调试利便,在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开拓人员透明的.
#p#分页标题#e#
2.较量靠得住的做法是,每次建设特定类加载器的新实例来加载指定范例的差异版本,这种利用场景下,一般就要牺牲缓存特定范例的类加载器实例以带来机能优化的计策了.对付指定范例已经被加载的版本,会在适其机缘到达unreachable状态,被unload并垃圾接纳.每次利用完类加载器特定实例后(确定不需要再利用时),将其显示赋为null,这样大概会较量快的到达jvm 类型中所说的类加载器实例unreachable状态,增大已经不再利用的范例版本被尽快卸载的时机.
3.不得不提的是,每次用新的类加载器实例去加载指定范例的指定版本,确实会带来必然的内存耗损,一般类加载器实例会在内存中保存较量长的时间. 在bea开拓者网站上找到一篇相关的文章(有专门阐明ClassLoader的部门):http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html
写的进程中参考了jvm类型和jls,并参考了sun公司官方网站上的一些bug的阐明文档。
接待各人品评指正!