概述
这篇文章基于最近在排查的一个问题,花了我们团队不少时间来排查这个问题,现象是有一些类加载器是作为key放到WeakHashMap里的,但是经历过多次full gc之后,依然坚挺地存在内存里,但是从代码上来说这些类加载器是应该被回收的,因为没有任何强引用可以到达这些类加载器了,于是我们做了内存dump,分析了下内存,发现除了一个WeakHashMap外并没有别的GC ROOT途径达到这些类加载器了,那这样一来经过多次FULL GC肯定是可以被回收的,但是事实却不是这样,为了让这个问题听起来更好理解,还是照例先上个Demo,完全模拟了这种场景。
Demo
首先我们创建两个类AAA和AAB,分别打包到两个不同jar里,比如AAA.jar和AAB.jar,这两个类之间是有关系的,AAA里有个属性是AAB类型的,注意这两个jar不要放到classpath里让appClassLoader加载到:
1
2
3
4
5
6
7
8
9
10
11
|
public class AAA {
private AAB aab;
public AAA(){
aab=new AAB();
}
public void clear(){
aab=null;
}
}
public class AAB {}
|
接着我们创建一个类加载TestLoader,里面存一个WeakHashMap,专门来存TestLoader的,并且复写loadClass方法,如果是加载AAB这个类,就创建一个新的TestLoader来从AAB.jar里加载这个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import java.net.URL;
import java.net.URLClassLoader;
import java.util.WeakHashMap;
public class TestLoader extends URLClassLoader {
public static WeakHashMap<TestLoader,Object> map=new WeakHashMap<TestLoader,Object>();
private static int count=0;
public TestLoader(URL[] urls){
super(urls);
map.put(this, new Object());
}
@SuppressWarnings("resource")
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.equals("AAB") && count==0){
try {
count=1;
URL[] urls = new URL[1];
urls[0] = new URL("file:///home/nijiaben/tmp/AAB.jar");
return new TestLoader(urls).loadClass("AAB");
}catch (Exception e){
e.printStackTrace();
}
}else{
return super.loadClass(name);
}
return null;
}
}
|
再看我们的主类TTest,一些说明都写在类里了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import java.lang.reflect.Method;
import java.net.URL;
/**
* Created by nijiaben on 4/22/16.
*/
public class TTest {
private Object aaa;
public static void main(String args[]){
try {
TTest tt = new TTest();
//将对象移到old,并置空aaa的aab属性
test(tt);
//清理掉aab对象
System.gc();
System.out.println("finished");
}catch (Exception e){
e.printStackTrace();
}
}
@SuppressWarnings("resource")
public static void test(TTest tt){
try {
//创建一个新的类加载器,从AAA.jar里加载AAA类
URL[] urls = new URL[1];
urls[0] = new URL("file:///home/nijiaben/tmp/AAA.jar");
tt.aaa=new TestLoader(urls).loadClass("AAA").newInstance();
//保证类加载器对象能进入到old里,因为ygc是不会对classLoader做清理的
for(int i=0;i<10;i++){
System.gc();
Thread.sleep(1000);
}
//将aaa里的aab属性清空掉,以便在后面gc的时候能清理掉aab对象,这样AAB的类加载器其实就没有什么地方有强引用了,在full gc的时候能被回收
Method[] methods=tt.aaa.getClass().getDeclaredMethods();
for(Method m:methods){
if(m.getName().equals("clear")){
m.invoke(tt.aaa);
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
|
运行的时候请跑在JDK8下,打个断点在System.out.println("finished")的地方,然后做一次内存dump。
从上面的例子中我们得知,TTest是类加载器AppClassLoader加载的,其属性aaa的对象类型是通过TestLoader从AAA.jar里加载的,而aaa里的aab属性是从一个全新的类加载器TestLoader从AAB.jar里加载的,当我们做了多次System GC之后,这些对象会移到old,在做最后一次GC之后,aab对象会从内存里移除,其类加载器此时已经是没有任何地方的强引用了,只有一个WeakHashMap引用它,理论上做GC的时候也应该被回收,但是事实时这个AAB的这个类加载器并没有被回收,从分析结果来看,GC ROOT路径是WeakHashMap,如图所示:

JDK8里的metaspace
这里不得不提的一个概念是JDK8里的metaspace,它是为了取代perm的,至于好处是什么,我个人觉得不是那么明显,有点费力不讨好的感觉,代码改了很多,但是实际收益并不明显,据说是oracle内部斗争的一个结果。
在JDK8里虽然没了perm,但是klass的信息还是要有地方存,jvm里为此分配了两块内存,一块是紧挨着heap来的,就和perm一样,专门用来存klass的信息,可以通过-XX:CompressedClassSpaceSize来设置大小,另外一块和它们不一定连着,主要是存非klass之外的其他信息,比如常量池什么的,可以通过-XX:InitialBootClassLoaderMetaspaceSize来设置,同时我们还可以通过-XX:MaxMetaspaceSize来设置触发metaspace回收的阈值。
每个类加载器都会从全局的metaspace空间里取一些metaChunk管理起来,当有类定义的时候,其实就是从这些内存里分配的,当不够的时候再去全局的metaspace里分配一块并管理起来。
这块具体的情况后面可以专门写一篇文章来介绍,包括内存结构,内存分配,GC等。
JDK8里的ClassLoaderDataGraph
每个类加载器都会对应一个ClassLoaderData的数据结构,里面会存譬如具体的类加载器对象,加载的klass,管理内存的metaspace等,它是一个链式结构,会链到下一个ClassLoaderData上,gc的时候通过ClassLoaderDataGraph来遍历这些ClassLoaderData,ClassLoaderDataGraph的第一个ClassLoaderData是bootstrapClassLoader的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
class ClassLoaderData : public CHeapObj<mtClass> {
...
static ClassLoaderData * _the_null_class_loader_data;
oop _class_loader; // oop used to uniquely identify a class loader
// class loader or a canonical class path
Dependencies _dependencies; // holds dependencies from this class loader
// data to others.
Metaspace * _metaspace; // Meta-space where meta-data defined by the
// classes in the class loader are allocated.
Mutex* _metaspace_lock; // Locks the metaspace for allocations and setup.
bool _unloading; // true if this class loader goes away
bool _keep_alive; // if this CLD is kept alive without a keep_alive_object().
bool _is_anonymous; // if this CLD is for an anonymous class
volatile int _claimed; // true if claimed, for example during GC traces.
// To avoid applying oop closure more than once.
// Has to be an int because we cas it.
Klass* _klasses; // The classes defined by the class loader.
JNIHandleBlock* _handles; // Handles to constant pool arrays
// These method IDs are created for the class loader and set to NULL when the
// class loader is unloaded. They are rarely freed, only for redefine classes
// and if they lose a data race in InstanceKlass.
JNIMethodBlock* _jmethod_ids;
// Metadata to be deallocated when it's safe at class unloading, when
// this class loader isn't unloaded itself.
GrowableArray<Metadata*>* _deallocate_list;
// Support for walking class loader data objects
ClassLoaderData* _next; /// Next loader_datas created
// ReadOnly and ReadWrite metaspaces (static because only on the null
// class loader for now).
static Metaspace* _ro_metaspace;
static Metaspace* _rw_metaspace;
...
}
|
这里提几个属性:
_class_loader : 就是对应的类加载器对象
_keep_alive : 如果这个值是true,那这个类加载器会认为是活的,会将其做为GC ROOT的一部分,gc的时候不会被回收
_unloading : 表示这个类加载是否需要卸载的
_is_anonymous : 是否匿名,这种ClassLoaderData主要是在lambda表达式里用的,这个我后面会详细说
_next : 指向下一个ClassLoaderData,在gc的时候方便遍历
_dependencies : 这个属性也是本文的重点,后面会细说
再来看下构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
ClassLoaderData::ClassLoaderData(Handle h_class_loader, bool is_anonymous, Dependencies dependencies) :
_class_loader(h_class_loader()),
_is_anonymous(is_anonymous),
// An anonymous class loader data doesn't have anything to keep
// it from being unloaded during parsing of the anonymous class.
// The null-class-loader should always be kept alive.
_keep_alive(is_anonymous || h_class_loader.is_null()),
_metaspace(NULL), _unloading(false), _klasses(NULL),
_claimed(0), _jmethod_ids(NULL), _handles(NULL), _deallocate_list(NULL),
_next(NULL), _dependencies(dependencies),
_metaspace_lock(new Mutex(Monitor::leaf+1, "Metaspace allocation lock", true)) {
// empty
}
|
可见,_keep_ailve属性的值是根据_is_anonymous以及当前类加载器是不是bootstrapClassLoader来的。
_keep_alive到底用在哪?其实是在GC的的时候,来决定要不要用Closure或者用什么Closure来扫描对应的ClassLoaderData。
1
2
3
4
5
6
7
8
9
10
|
void ClassLoaderDataGraph::roots_cld_do(CLDClosure* strong, CLDClosure* weak) {
//从最后一个创建的classloader到bootstrapClassloader
for (ClassLoaderData* cld = _head; cld != NULL; cld = cld->_next) {
//如果是ygc,那weak和strong是一样的,对所有的类加载器都做扫描,保证它们都是活的
//如果是cms initmark阶段,如果要unload_classes了(should_unload_classes()返回true),则weak为null,那就只遍历bootstrapclassloader以及正在做匿名类加载的类加载
CLDClosure* closure = cld->keep_alive() ? strong : weak;
if (closure != NULL) {
closure->do_cld(cld);
}
}
|
类加载器什么时候被回收
类加载器是否需要被回收,其实就是看这个类加载器对象是否是活的,所谓活的就是这个类加载器加载的任何一个类或者这些类的对象是强可达的,当然还包括这个类加载器本身就是GC ROOT一部分或者有GC ROOT可达的路径,那这个类加载器就肯定不会被回收。
从各种GC情况来看:
- 如果是YGC,类加载器是作为GC ROOT的,也就是都不会被回收
- 如果是Full GC,只要是死的就会被回收
- 如果是CMS GC,CMS GC过程也是会做标记的(这是默认情况,不过可以通过一些参数来改变),但是不会做真正的清理,真正的清理动作是发生在下次进入安全点的时候。
僵尸类加载器如何产生
如果类加载器是与GC ROOT的对象存在真正依赖的这种关系,这种类加载器对象是活的无可厚非,我们通过zprofiler或者mat都可以分析出来,可以将链路绘出来,但是有两种情况例外:
lambda匿名类加载
lambda匿名类加载走的是unsafe的defineAnonymousClass方法,这个方法在vm里对应的是下面的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
UNSAFE_ENTRY(jclass, Unsafe_DefineAnonymousClass(JNIEnv *env, jobject unsafe, jclass host_class, jbyteArray data, jobjectArray cp_patches_jh))
{
instanceKlassHandle anon_klass;
jobject res_jh = NULL;
UnsafeWrapper("Unsafe_DefineAnonymousClass");
ResourceMark rm(THREAD);
HeapWord* temp_alloc = NULL;
anon_klass = Unsafe_DefineAnonymousClass_impl(env, host_class, data,
cp_patches_jh,
&temp_alloc, THREAD);
if (anon_klass() != NULL)
res_jh = JNIHandles::make_local(env, anon_klass->java_mirror());
// try/finally clause:
if (temp_alloc != NULL) {
FREE_C_HEAP_ARRAY(HeapWord, temp_alloc, mtInternal);
}
// The anonymous class loader data has been artificially been kept alive to
// this point. The mirror and any instances of this class have to keep
// it alive afterwards.
if (anon_klass() != NULL) {
anon_klass->class_loader_data()->set_keep_alive(false);
}
// let caller initialize it as needed...
return (jclass) res_jh;
}
UNSAFE_END
}
|
可见,在创建成功匿名类之后,会将对应的ClassLoaderData的_keep_alive属性设置为false,那是不是意味着_keep_alive属性在这之前都是true呢?下面的parse_stream方法是从上面的方法最终会调下来的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
Klass* SystemDictionary::parse_stream(Symbol* class_name,
Handle class_loader,
Handle protection_domain,
ClassFileStream* st,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TRAPS) {
TempNewSymbol parsed_name = NULL;
Ticks class_load_start_time = Ticks::now();
ClassLoaderData* loader_data;
if (host_klass.not_null()) {
// Create a new CLD for anonymous class, that uses the same class loader
// as the host_klass
assert(EnableInvokeDynamic, "");
guarantee(host_klass->class_loader() == class_loader(), "should be the same");
guarantee(!DumpSharedSpaces, "must not create anonymous classes when dumping");
loader_data = ClassLoaderData::anonymous_class_loader_data(class_loader(), CHECK_NULL);
loader_data->record_dependency(host_klass(), CHECK_NULL);
} else {
loader_data = ClassLoaderData::class_loader_data(class_loader());
}
instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name,
loader_data,
protection_domain,
host_klass,
cp_patches,
parsed_name,
true,
THREAD);
...
}
ClassLoaderData* ClassLoaderData::anonymous_class_loader_data(oop loader, TRAPS) {
// Add a new class loader data to the graph.
return ClassLoaderDataGraph::add(loader, true, CHECK_NULL);
}
ClassLoaderData* ClassLoaderDataGraph::add(Handle loader, bool is_anonymous, TRAPS) {
// We need to allocate all the oops for the ClassLoaderData before allocating the
// actual ClassLoaderData object.
ClassLoaderData::Dependencies dependencies(CHECK_NULL);
No_Safepoint_Verifier no_safepoints; // we mustn't GC until we've installed the
// ClassLoaderData in the graph since the CLD
// contains unhandled oops
ClassLoaderData* cld = new ClassLoaderData(loader, is_anonymous, dependencies);
...
}
|
从上面的代码得知,只要走了unsafe的那个方法,都会为当前类加载器创建一个ClassLoaderData对象,并设置其_is_anonymous为true,也同时意味着_keep_alive的属性是true,并加入到ClassLoaderDataGraph中。
试想如果创建的这个匿名类没有成功,也就是anon_klass()==null,那这个_keep_alive属性就永远无法设置为false了,这意味着这个ClassLoaderData对应的ClassLoader对象将永远都是GC ROOT的一部分,无法被回收,这种情况就是真正的僵尸类加载器了,不过目前我还没模拟出这种情况来,有兴趣的同学可以试一试,如果真的能模拟出来,这绝对是JDK里的一个BUG,可以提交给社区。
类加载器依赖导致的
这里说的类加载器依赖,并不是说ClassLoader里的parent建立的那种依赖关系,如果是这种关系,那其实通过mat或者zprofiler这样的工具都是可以分析出来的,但是还存在一种情况,那些工具都是分析不出来的,这种关系就是通过ClassLoaderData里的_dependencies属性得出来的,比如说如果A类加载器的_dependencies属性里记录了B类加载器,那当GC遍历A类加载器的时候也会遍历B类加载器,并将其标活,哪怕B类加载器其实是可以被回收了的,可以看下下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
|
void ClassLoaderData::oops_do(OopClosure* f, KlassClosure* klass_closure, bool must_claim) {
if (must_claim && !claim()) {
return;
}
f->do_oop(&_class_loader);
_dependencies.oops_do(f);
_handles->oops_do(f);
if (klass_closure != NULL) {
classes_do(klass_closure);
}
}
|
那问题来了,这种依赖关系是怎么记录的呢?其实我们上面的demo就模拟了这种情况,可以仔细去看看,我也针对这个demo描述下,比如加载AAA的类加载器TestLoader加载AAA后,并创建AAA对象,此时会看到有个类型是AAB的属性,此时会对常量池里的类型做一个解析,我们看到TestLoader的loadClass方法的时候做了一个判断,如果是AAB类型的类加载,那就创建一个新的类加载器对象从AAB.jar里去加载,当加载返回的时候,在jvm里其实就会记录这么一层依赖关系,认为AAA的类加载器依赖AAB的类加载器,并记录下来,但是纵观所有的hotspot代码,并没有一个地方来清理这种依赖关系的,也就是说只要这种依赖关系建立起来,会一直持续到AAA的类加载器被回收的时候,AAB的类加载器才会被回收,所以说这算一种伪僵尸类加载器,虽然从依赖关系上其实并不依赖了(比如demo里将AAA的aab属性做clear清空动作),但是GC会一直认为他们是存在这种依赖关系的,会持续存在一段时间,具体持续多久就看AAA类加载器的情况了。
针对这种情况个人认为需要一个类似引用计数的GC策略,当某两个类加载器确实没有任何依赖的时候,将其清理掉这种依赖关系,估计要实现这种改动的地方也挺多,没那么简单,所以当时的设计者或许因为这样并没有这么做了,我觉得这算是偷懒妥协的结果吧,当然这只是我的一种猜测。
- JVM源码分析之Metaspace解密
概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...
- JVM源码分析-JVM源码编译与调试
要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...
- JVM源码分析之SystemGC完全解读
JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- JVM源码分析之堆外内存完全解读
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
- JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...
- JVM源码分析-类加载场景实例分析
A类调用B类的静态方法,除了加载B类,但是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为何在未调用时尝试加载?. 场景如下: public cl ...
- JVM源码分析之JVM启动流程
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...
- JVM源码分析之synchronized实现
“365篇原创计划”第十二篇. 今天呢!灯塔君跟大家讲: JVM源码分析之synchronized实现 java内部锁synchronized的出现,为多线程的并发执行提供了一个稳定的 ...
随机推荐
- Image Processing and Computer Vision_Review:HPatches A benchmark and evaluation of handcrafted and learned local descriptors——2017.04
翻译 HPatches:手工和学习本地描述符的基准和评估——http://tongtianta.site/paper/8979 摘要:在本文中,我们提出了一个评估本地图像描述符的新基准.我们证明现有数 ...
- 在cmd下import cv2报错——OpenCV实现BRISK
平台:win10 x64 +JetBrains PyCharm 2018.2.4 x64 +Anaconda3(python3.7.0+opencv3.4.5) Issue说明:同学发了个python ...
- mysql tinyint(1) 在java中被转化为boolean
数据库表字段类型为:tinyint 长度为1 在java中对应的类型是boolean 查询时直接在页面展示成true或false 如果是2,3,4 这样的也是默认成true,非常不友好. 解决方案: ...
- sysbench 数据库性能测试工具的使用
sysbench 数据库性能测试 Mac上安装sysbench测试工具 brew install sysbench 测试sysbench 是否安装成功 //执行这条指令 sysbench cpu -- ...
- Ubuntu系统---安装Caffe (+OpenCV+Python+CPU-only)
安装配置Ubuntu14.04+Caffe (+OpenCV+Python+CPU-only) 记录 [作者:Wu Ping.时间:20180428.] 本人已经安装很多次的Caffe了:从开始的初探 ...
- chrome上一些好用的插件
1. Super Auto Refresh Plus - 这个插件可以自动刷新网页 2. 屏蔽百度推广 - 这个插件可以屏蔽百度搜索的推广广告
- 关于C++编译时内链接和外链接
最近在阅读<大规模C++ 程序设计> 在第1部分,作者讨论了内链接和外链接问题(因为大规模的C++程序有繁多的类和单元.因此编译速度是个大问题) 这里记录一下关于内链接和外链接的理解. ...
- python dict list 遍历的几种常见方法
list 遍历index,value list = ['one', 'two', 'three'] for i in list: print(list.index(i),i) #rangefor i ...
- 下划线文字,鼠标hover小样式
CSS样式 //不只是a标签,其他有下划线的字体也可以 a:hover{ color: #ff3100; //这里的颜色是指字体颜色不是波浪下划线效果的svg图颜色 text-decorati ...
- Vue学习日记(三)——Vue路由管理vue-router
前言 为了给读者更好的体验,去理解vue-router和下一篇介绍vuex,决定还是来一个实战教程来带大家更加的去深入理解vue-router,在这之前,读者先自行了解和去官网下载npm和node,附 ...