http://www.importnew.com/20129.html
在用代码分析之前,我们对内存(堆)的分配策略明确以下三点:
- 对象优先在Eden分配。
- 大对象直接进入老年代。
- 长期存活的对象将进入老年代。一般情况下接受过15次Minor GC后晋升老年代
- survivor 的“to”区满了之后(并非survivor2片都满,survivor永远有1个-from区是空的),对象进到old区
- 新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。
下面我们来看如下代码:
1
2
3
4
5
6
|
public class SlotGc{ public static void main(String[] args){ byte [] holder = new byte [ 32 * 1024 * 1024 ]; System.gc(); } } |
代码很简单,就是向内存中填充了32MB的数据,然后通过虚拟机进行垃圾收集。在Javac编译后,我们执行如下指令:java -XX:+PrintGC来查看垃圾收集的结果,得到如下输出信息:
[GC 208K->134K(5056K), 0.0017306 secs]
[Full GC 134K->134K(5056K), 0.0121194 secs]
[Full GC 32902K->32902K(37828K), 0.0094149 sec
注意第三行,“->”之前的数据表示垃圾回收前堆中存活对象所占用的内存大小,“->”之后的数据表示垃圾回收堆中存活对象所占用的内存大小,括号中的数据表示堆内存的总容量,0.0094149 sec 表示垃圾回收所用的时间。
从结果中可以看出,System.gc(()运行后并没有回收掉这32MB的内存,这应该是意料之中的结果,因为变量holder还处在作用域内,虚拟机自然不会回收掉holder引用的对象所占用的内存。
我们把代码修改如下:
1
2
3
4
5
6
7
8
|
public class SlotGc{ public static void main(String[] args){ { byte [] holder = new byte [ 32 * 1024 * 1024 ]; } System.gc(); } } |
加入花括号后,holder的作用域被限制在了花括号之内,因此,在执行System.gc()时,holder引用已经不能再被访问,逻辑上来讲,这次应该会回收掉holder引用的对象所占的内存。但查看垃圾回收情况时,输出信息如下:
[GC 208K->134K(5056K), 0.0017100 secs]
[Full GC 134K->134K(5056K), 0.0125887 secs]
[Full GC 32902K->32902K(37828K), 0.0089226 secs]
很明显,这32MB的数据并没有被回收。下面我们再做如下修改:
1
2
3
4
5
6
7
8
9
|
public class SlotGc{ public static void main(String[] args){ { byte [] holder = new byte [ 32 * 1024 * 1024 ]; holder = null ; } System.gc(); } } |
这次得到的垃圾回收信息如下:
[GC 208K->134K(5056K), 0.0017194 secs]
[Full GC 134K->134K(5056K), 0.0124656 secs]
[Full GC 32902K->134K(37828K), 0.0091637 secs]
说明这次holder引用的对象所占的内存被回收了。我们慢慢来分析。
首先明确一点:holder能否被回收的根本原因是局部变量表中的Slot是否还存有关于holder数组对象的引用。
在第一次修改中,虽然在holder作用域之外进行回收,但是在此之后,没有对局部变量表的读写操作,holder所占用的Slot还没有被其他变量所复用(回忆Java内存区域与内存溢出一文中关于Slot的讲解),所以作为GC Roots一部分的局部变量表仍保持者对它的关联。这种关联没有被及时打断,因此GC收集器不会将holder引用的对象内存回收掉。 在第二次修改中,在GC收集器工作前,手动将holder设置为null值,就把holder所占用的局部变量表中的Slot清空了,因此,这次GC收集器工作时将holder之前引用的对象内存回收掉了。
当然,我们也可以用其他方法来将holder引用的对象内存回收掉,只要复用holder所占用的slot即可,比如在holder作用域之外执行一次读写操作。如:
public static void main(String[] args){ { byte[] holder = new byte[32*1024*1024]; // holder = null; 导致内存回收 } // 词句在holder作用域之外执行一次读写操作。也可导致内存回收 // int a= 0; System.gc(); }
为对象赋null值并不是控制变量回收的最好方法,以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。另外,赋null值的操作在经过虚拟机JIT编译器优化后会被消除掉,经过JIT编译后,System.gc()执行时就可以正确地回收掉内存,而无需赋null值。在下一个帖子有解说:https://blog.csdn.net/zero__007/article/details/52517712
就这个例子来说,可以理解为直到System.gc()执行的那一刻,局部变量表中还有对placeholder的引用,因此在GC前的”=null”操作,实际是移除掉局部变量表中的placeholder引用,所以有”=null”的版本成功回收掉64M内存。
那实验看起来不是证明”=null”某些情况下是有用的吗?实际上前面说了,”=null”作用仅仅是打断局部变量表中的引用。而做到这点并不一定非得placeholder = null,把这句替换成“int a = 1”也能达到效果,反正就把那个坑占了,不在乎扔进去的是“null”还是“1”。
此前的ThreadLocal静态对象=null则不同,static修饰变量,生存期无限,除了显式=null打断GC Root外,无他法(https://blog.csdn.net/silyvin/article/details/79551635)
最关键的是,上面实验建立在“未JIT的前提下”,在JIT编译器进行控制流和数据流分析后,生成的OopMap就提供比较精确的信息,不需要通过”=null”来告知对象使命已经完成。退一步说,这时即使有”=null”操作,也会被优化掉,生成出来的本地代码与没有”=null”操作的版本是一模一样的。对于在意性能的代码,必定是执行频率高,会被JIT的,而不会被JIT的,也不需要在意效率,因此”=null”没有意义。