· 最近在研究弱引用的时候,注意到了Java在内存管理时的细节,在这里给大家分享一下。
· 垃圾处理的算法非常多,本博客只介绍使用最广的Hotspot虚拟机的垃圾回收算法。简单说来,其使用了可达性分析算法 ,通过选出一系列称为GC Roots的对象作为起点,当一个对象没有任何一条强引用链指向GC Roots时,则这个对象会被判定为可回收对象。弱引用不是本章讨论的重点,有兴趣的自行百度。
· 在上图中,object5、6、7会被回收。
· 显然,此算法最核心的部分在于GC Roots的选取,所以我们要关注在什么位置的对象才能成为GC Roots,在《深入理解JVM虚拟机》中给出了答案:
·这里不普及JVM的内存结构。
· 介绍可达性算法主要是为了给我下面的想法做铺垫。
· 注意:以下的内容我没有从任何书籍和博客上看到明确的解答,所以仅是我个人的一些理解。
public class B {
byte[] b = new byte[1024 * 1024];
}
public class A {
public B b = new B();
byte[] a = new byte[1024 * 1024];
}
public class GCTest {
public static void main(String[] args) {
System.gc();
System.out.println("--------------");
A a = new A();
System.gc();
System.out.println("--------------");
a = null;
System.gc();
System.out.println("--------------");
while(true){
try {
Thread.sleep(5000);
System.gc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
· 结果猜测:显然,引用a
是存在于栈中的,而a指向的实例A,以及实例A中创建的实例B存在于堆中。现在不确定的点在于:指向实例B的a.b
这个引用是存在于栈中,还是堆中呢?众所周知,栈中的数据会随着方法的结束而被释放,如果方法不结束,其数据就不会释放,因此我用while(true){...}
使main方法永远不会结束,且每5秒进行一次垃圾回收,看堆中的储存情况。
a.b
保存在栈中,其就可作为GC Root存在,那么根据可达性算法,实例B永远不会被回收。a.b
保存在堆中,一旦引用a
被释放了,那么实例A和实例B就会如上图中的object5、6、7
一样,被GC回收a.b
保存在栈中,即使实例A被释放了,实例B也不会被释放,堆中至少有1MB的数据。而如果a.b
保存在堆中,实例A和实例B都会被释放,堆中的数据至少会小于1MB。[GC (System.gc()) [PSYoungGen: 3333K->744K(38400K)] 3333K->752K(125952K), 0.0042393 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->673K(87552K)] 752K->673K(125952K), [Metaspace: 3479K->3479K(1056768K)], 0.0078916 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
--------------
[GC (System.gc()) [PSYoungGen: 2713K->2176K(38400K)] 3386K->2849K(125952K), 0.0010460 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 2176K->0K(38400K)] [ParOldGen: 673K->2718K(87552K)] 2849K->2718K(125952K), [Metaspace: 3485K->3485K(1056768K)], 0.0079493 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
--------------
[GC (System.gc()) [PSYoungGen: 665K->64K(38400K)] 3384K->2782K(125952K), 0.0003627 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 2718K->670K(87552K)] 2782K->670K(125952K), [Metaspace: 3485K->3485K(1056768K)], 0.0039402 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
--------------
[GC (System.gc()) [PSYoungGen: 0K->0K(38400K)] 670K->670K(125952K), 0.0003178 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 0K->0K(38400K)] [ParOldGen: 670K->669K(87552K)] 670K->669K(125952K), [Metaspace: 3485K->3485K(1056768K)], 0.0066122 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
[ParOldGen: 8K->673K(87552K)]
。[PSYoungGen: 2713K->2176K(38400K)]
([ParOldGen: 673K->2718K(87552K)]
[ParOldGen: 2718K->670K(87552K)]
· 有了上述问题的铺垫,我们可以正式聊一下HashMap中内存泄漏的问题, 因为之前看ThreadLocal的源码才意识到了这个问题,实在是惭愧,话不多说,我先解释一下内存泄漏的原因:
map
在put(key,value)
时,就会生成key,value引用的副本,我们姑且称为key'
和value'
。根据上面的理论,这个副本应该存在堆内存中。key引用
被释放时,原来指向的实例KEY
(姑且这么称呼),与栈中的map引用
依然存在一条可达链:map ---- HashMap实例 ---- key’ ---- KEY实例 这条强引用链。因此实例KEY不会被释放,虽然HashMap可以保存键为null的Entry,但是实例KEY我们就不会再用到了,而它迟迟不释放,就会造成内存泄漏的问题。我们将类A稍作修改,让其不关联类B:
public class A {
// public B b = new B();
byte[] a = new byte[1024 * 1024];
}
public class GCTest {
public static void main(String[] args) {
System.out.println("0.--------------");
System.gc();
System.out.println("1.--------------");
Map<A, B> map = new HashMap<>();
A a = new A();
B b = new B();
System.gc();
System.out.println("2.--------------");
map.put(a,b);
System.gc();
System.out.println("3.--------------");
a = null;
System.gc();
System.out.println("4.--------------");
while(true){
try {
Thread.sleep(5000);
System.gc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
0.--------------
[GC (System.gc()) [PSYoungGen: 3333K->712K(38400K)] 3333K->720K(125952K), 0.0016227 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 712K->0K(38400K)] [ParOldGen: 8K->629K(87552K)] 720K->629K(125952K), [Metaspace: 3206K->3206K(1056768K)], 0.0074770 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
1.--------------
[GC (System.gc()) [PSYoungGen: 3379K->2176K(38400K)] 4008K->2805K(125952K), 0.0012001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 2176K->0K(38400K)] [ParOldGen: 629K->2676K(87552K)] 2805K->2676K(125952K), [Metaspace: 3226K->3226K(1056768K)], 0.0076292 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2.--------------
[GC (System.gc()) [PSYoungGen: 1996K->32K(38400K)] 4672K->2708K(125952K), 0.0005201 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 2676K->2675K(87552K)] 2708K->2675K(125952K), [Metaspace: 3226K->3226K(1056768K)], 0.0076530 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
3.--------------
[GC (System.gc()) [PSYoungGen: 1331K->96K(38400K)] 4007K->2771K(125952K), 0.0003721 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 96K->0K(38400K)] [ParOldGen: 2675K->2677K(87552K)] 2771K->2677K(125952K), [Metaspace: 3229K->3229K(1056768K)], 0.0027659 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
4.--------------
[GC (System.gc()) [PSYoungGen: 1331K->160K(38400K)] 4009K->2837K(125952K), 0.0005946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 160K->0K(38400K)] [ParOldGen: 2677K->2718K(87552K)] 2837K->2718K(125952K), [Metaspace: 3488K->3488K(1056768K)], 0.0065556 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[ParOldGen: 8K->629K(87552K)]
。[ParOldGen: 629K->2676K(87552K)]
。map.put(a,b)
,put操作会在堆内存中会有额外的开销(会新建一些对象用来进行put操作),因此新生代中会有新内存的使用(但很快就被清理掉了):[PSYoungGen: 1996K->32K(38400K)]
。我们通过观察老年代:[ParOldGen: 2676K->2675K(87552K)]
,发现实际上这个put操作并没有为HashMap开辟新的内存空间,因为map仅仅只是把内部的引用指向了实例A, B而已。a = null;
,我们希望看到实例A被回收,但是GC并没有这样做,老年代中的值几乎没有变化:[ParOldGen: 2675K->2677K(87552K)]
[ParOldGen: 2677K->2718K(87552K)]
,因此可以判断这造成了一定的内存泄漏。· 笔者不太会手动去真正释放HashMap的key,因此,我做了一个实验,先把类A关联上类B,但是不实例化这个B。
public class A {
public B b;
byte[] a = new byte[1024 * 1024];
}
引用a,b
,并把a.b = b
b = null
, 再令a.b = null
,比较内存变化情况a = null
,查看内存public class GCTest {
public static void main(String[] args) {
System.out.println("0.--------------");
System.gc();
System.out.println("1.--------------");
A a = new A();
B b = new B();
System.gc();
System.out.println("2.--------------");
a.b = b;
System.gc();
System.out.println("3.--------------");
b = null;
System.gc();
System.out.println("4.--------------");
a.b = null;
System.gc();
System.out.println("5.--------------");
a = null;
System.gc();
System.out.println("--------------");
while(true){
try {
Thread.sleep(5000);
System.gc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
0.--------------
[GC (System.gc()) [PSYoungGen: 3333K->776K(38400K)] 3333K->784K(125952K), 0.0013798 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 776K->0K(38400K)] [ParOldGen: 8K->673K(87552K)] 784K->673K(125952K), [Metaspace: 3482K->3482K(1056768K)], 0.0198551 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
1.--------------
[GC (System.gc()) [PSYoungGen: 2713K->2144K(38400K)] 3387K->2817K(125952K), 0.0047306 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 2144K->0K(38400K)] [ParOldGen: 673K->2718K(87552K)] 2817K->2718K(125952K), [Metaspace: 3487K->3487K(1056768K)], 0.0091880 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2.--------------
[GC (System.gc()) [PSYoungGen: 1331K->32K(38400K)] 4050K->2750K(125952K), 0.0003588 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 2718K->2718K(87552K)] 2750K->2718K(125952K), [Metaspace: 3487K->3487K(1056768K)], 0.0080175 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
3.--------------
[GC (System.gc()) [PSYoungGen: 665K->32K(38400K)] 3383K->2750K(125952K), 0.0004018 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 2718K->2718K(87552K)] 2750K->2718K(125952K), [Metaspace: 3487K->3487K(1056768K)], 0.0037897 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4.--------------
[GC (System.gc()) [PSYoungGen: 665K->32K(38400K)] 3383K->2750K(125952K), 0.0005433 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 2718K->1694K(87552K)] 2750K->1694K(125952K), [Metaspace: 3487K->3487K(1056768K)], 0.0051309 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
5.--------------
[GC (System.gc()) [PSYoungGen: 665K->32K(38400K)] 2359K->1726K(125952K), 0.0045968 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 1694K->670K(87552K)] 1726K->670K(125952K), [Metaspace: 3488K->3488K(1056768K)], 0.0044260 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
--------------
[GC (System.gc()) [PSYoungGen: 665K->32K(38400K)] 1335K->702K(125952K), 0.0003409 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 32K->0K(38400K)] [ParOldGen: 670K->670K(87552K)] 702K->670K(125952K), [Metaspace: 3488K->3488K(1056768K)], 0.0030132 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
[ParOldGen: 8K->673K(87552K)]
。[ParOldGen: 673K->2718K(87552K)]
a.b = b
后,堆内存无变化:[ParOldGen: 2718K->2718K(87552K)]
引用b=null
,发现内存不释放:[ParOldGen: 2718K->2718K(87552K)]
,这与HashMap的内存泄漏完全一致a.b
对实例B的引用a.b = null
,发现实例B被清理了:[ParOldGen: 2718K->1694K(87552K)]
,但是实例A仍然占用了1MB的堆内存。引用a = null
,发现此时实例A也被清理了:[ParOldGen: 1694K->670K(87552K)]
· Java的内存管理机制还是很巧妙的,相比C++方便了很多,越往后面学发现这些底层的东西才是java的灵魂的所在啊。