gc日志

http://www.importnew.com/20129.html

  在用代码分析之前,我们对内存(堆)的分配策略明确以下三点:

  • 对象优先在Eden分配。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。一般情况下接受过15次Minor GC后晋升老年代
    对垃圾回收策略说明以下两点:
  • 新生代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”没有意义。 


你可能感兴趣的:(jvm)