java垃圾收集器中内存分配和回收的一些细节是怎样的

这篇文章将为大家详细讲解有关java垃圾收集器中内存分配和回收的一些细节是怎样的,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

1. 禁用 System.gc()

jvm提供了一个参数 DisableExplicitGC 来控制是否手工触发GC,如果需要禁用,可以使用以下配置:

-XX:+DisableExplicitGC

2. System.gc() 使用并发回收

在默认情况下,即使 System.gc 生效,会使用传统的 Full GC 方式回收整个堆,而忽略参数中的 UseG1GCUseConcMarkSweepGC,即CMS和G1是没有并发执行的。

示例代码

public class Demo01 {

    public static void main(String[] args) {
        System.gc();
    }

}

使用-XX:+PrintGCDetails -XX:+UseConcMarkSweepGC,日志如下:

[Full GC (System.gc()) [CMS: 0K->372K(174784K), 0.0246450 secs] 2798K->372K(253440K), [Metaspace: 2906K->2906K(1056768K)], 0.0247414 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]

使用-XX:+PrintGCDetails -XX:+UseG1GC,日志如下:

[Full GC (System.gc())  1517K->368K(8192K), 0.0089949 secs]
   [Eden: 2048.0K(12.0M)->0.0B(3072.0K) Survivors: 0.0B->0.0B Heap: 1517.6K(256.0M)->368.7K(8192.0K)], [Metaspace: 2906K->2906K(1056768K)]
 [Times: user=0.01 sys=0.00, real=0.01 secs]

显然,此时CMS和G1是没有并发执行的,因为在日志中没有任何并发相关的信息。可以使用以下参数来改变这种默认行为:

-XX:+ExplicitGCInvokesConcurrent

使用-XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -XX:+ExplicitGCInvokesConcurrent,日志如下:

[GC (System.gc()) [ParNew: 2798K->398K(78656K), 0.0010206 secs] 2798K->398K(253440K), 0.0010476 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(174784K)] 1797K(253440K), 0.0001720 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Final Remark) [YG occupancy: 1797 K (78656 K)][Rescan (parallel) , 0.0007200 secs][weak refs processing, 0.0000066 secs][class unloading, 0.0001855 secs][scrub symbol table, 0.0003697 secs][scrub string table, 0.0001424 secs][1 CMS-remark: 0K(174784K)] 1797K(253440K), 0.0014769 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-reset-start]

使用-XX:+PrintGCDetails -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent,日志如下:

[GC pause (System.gc()) (young) (initial-mark), 0.0024614 secs]
   [Parallel Time: 1.5 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 100.6, Avg: 100.7, Max: 100.8, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 1.6]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.4, Avg: 0.6, Max: 1.0, Diff: 0.6, Sum: 5.1]
      [Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.5, Diff: 0.5, Sum: 3.0]
         [Termination Attempts: Min: 1, Avg: 3.1, Max: 8, Diff: 7, Sum: 25]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 1.1, Avg: 1.2, Max: 1.4, Diff: 0.3, Sum: 9.8]
      [GC Worker End (ms): Min: 101.9, Avg: 101.9, Max: 102.0, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.8 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.6 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 2048.0K(24.0M)->0.0B(23.0M) Survivors: 0.0B->1024.0K Heap: 2009.1K(256.0M)->568.1K(256.0M)]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0003924 secs]
[GC concurrent-mark-start]
[GC concurrent-mark-end, 0.0002255 secs]
[GC remark [Finalize Marking, 0.0001208 secs] [GC ref-proc, 0.0000284 secs] [Unloading, 0.0002767 secs], 0.0005331 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC cleanup 1039K->1039K(256M), 0.0003741 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs]

使用ExplicitGCInvokesConcurrent参数后,System.gc() 这种显式GC才会使用并发的方式进行回收,否则无论是否启用了CMS或G1,都不会进行并发回收。

3. 并行GC前额外触发的新生代GC

对于并行回收器(使用UseParallelOldGC或者UseParallelGC),在每一次FullGC之前都会伴随一次新生代GC,这和串行回收器相比,有很大的不同,示例如下:

public class Demo01 {
    public static void main(String[] args) {
        System.gc();
    }
}

使用-XX:+PrintGCDetails -XX:+UseSerialGC,gc日志如下:

[Full GC (System.gc()) [Tenured: 0K->367K(174784K), 0.0017465 secs] 2798K->367K(253440K), [Metaspace: 2903K->2903K(1056768K)], 0.0017770 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

可以看到,System.gc() 触发了一次Full GC.

使用-XX:+PrintGCDetails -XX:+UseParallelOldGC,gc日志如下:

[GC (System.gc()) [PSYoungGen: 2621K->528K(76288K)] 2621K->536K(251392K), 0.0008817 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 528K->0K(76288K)] [ParOldGen: 8K->368K(175104K)] 536K->368K(251392K), [Metaspace: 2905K->2905K(1056768K)], 0.0036679 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

可以看到,触发FullGC前,进行了一次新生代GC。因此,这里的Sytem.gc()实际上触发了两次GC,这样做的目的是先将新生代进行一次回收,避免将所有回收工作同时交给一次Full GC进行,从而尽可能地缩短一次停顿时间。

如果不需要这个特性,可以使用参数-XX:-ScavengeBeforeFullGC去除发生在FullGC之前的那次新生代GC. 使用-XX:+PrintGCDetails -XX:+UseParallelOldGC -XX:-ScavengeBeforeFullGC运行,gc日志如下:

[Full GC (System.gc()) [PSYoungGen: 2621K->0K(76288K)] [ParOldGen: 0K->368K(175104K)] 2621K->368K(251392K), [Metaspace: 2906K->2906K(1056768K)], 0.0032836 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

可以看到,Full GC 前已经没有了新生代gc。

4. 对象何时进入老年代

  • 初创对象在eden区产生

  • 老年对象进行老年代:虚拟机提供一个参数来控制新生代对象的最大年龄:MaxTenuringThreshold,这个值默认是15,即新生代对象最多经历15次GC,就可以晋升到老年代。实际情况中,对象的实际晋升年龄是根据survivor区的使用情况动态计算得来的,而 MaxTenuringThreshold 只是表示这个年龄的最大值。可以使用参数TargetSurvivorRatio设置survivor的目标使用率,默认为50,即如果survivor区在GC后使用率超过50%,那么就很可能会使用较小的age作为晋升年龄。

  • 大对象进入老年代:如果对象很大,新生代无论是eden区还是survivor区都无法容纳这个对象,自然这个对象无法存放在新生代。jvm提供了参数PretenureSizeThreshold参数来设置对象直接晋升到老年代的阈值,单位是字节。只要对象大于该指定值,就会直接在老年代分配。这个参数只对串行回收器和ParNew有效,对于ParallelGC无效。默认下该值为0,也就是不指定最大的晋升大小,一切由运行情况决定。

大对象直接进入老年代的示例:

public class Demo04 {
    public static final int _1K = 1024;
    public static void main(String[] args) {
        Map<Integer, byte[]> map = new HashMap<>();
        for(int i = 0; i < 5 * _1K; i++) {
            byte[] b = new byte[_1K];
            map.put(i, b);
        }
    }
}

使用-Xmx32m -Xms32m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB参数运行,结果如下:

Heap
 def new generation   total 9792K, used 963K [0x00000007be000000, 0x00000007beaa0000, 0x00000007beaa0000)
  eden space 8704K,  11% used [0x00000007be000000, 0x00000007be0f0f88, 0x00000007be880000)
  from space 1088K,   0% used [0x00000007be880000, 0x00000007be880000, 0x00000007be990000)
  to   space 1088K,   0% used [0x00000007be990000, 0x00000007be990000, 0x00000007beaa0000)
 tenured generation   total 21888K, used 5953K [0x00000007beaa0000, 0x00000007c0000000, 0x00000007c0000000)
   the space 21888K,  27% used [0x00000007beaa0000, 0x00000007bf070408, 0x00000007bf070600, 0x00000007c0000000)
 Metaspace       used 3054K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 333K, capacity 388K, committed 512K, reserved 1048576K

可以看到,无任何gc日志输出,最终使用的空间中,老年代使用了大约5m空间。

5. 在TLAB上分配对象

TLAB 全称是Thread Local Allocation Buffer,即线程本地分配缓存。从名字上看,TLAB是一个线程专用的内存分配区域。由于对象一般分配在堆上,而堆是全局共享的,在同一时间内,可能有多个线程在堆上申请空间。因此,每一次对象分配都必须进行同步,而在竞争激烈的场合分配的效率又会进一步下降。考虑到对象分配几乎是java最常用的操作,因此java虚拟机就使用了TLAB这种线程专属的区域来避免多线程冲突,提高对象分配的效率。TLAB本身占用了eden区空间,在TLAB启用的情况下,虚拟机会为每一个java线程分配一块TLAB区域。

默认情况下,TLAB的大小是会在运行时不断调整的,使系统的运行状态达到最优。如果想禁用自动调整TLAB的大小,可以使用-XX:ResizeTLAB禁用ResizeTLAB并使用-XX:TLABSize手工指定TLAB的大小。

示例:启用TLAB与关闭TLAB时的性能差异

public class Demo05 {
    public static void alloc() {
        byte[] b = new byte[2];
        b[0] = 1;
    }

    public static void main(String[] args) {
        long b = System.currentTimeMillis();
        for(int i = 0; i < 1000_0000; i++) {
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e - b);
    }
}
  • 使用参数-XX:+UseTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis -server运行,结果为71;

  • 使用参数-XX:-UseTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis -server运行,结果为135;

从结果来看,TLAB是否启用对于对象分配的影响还是很大的。

对象的分配流程:

  • 如果开启了栈上分配,系统就会先进行栈上分配;

  • 没有开启栈上分配或者不符合条件则会进行TLAB分配;

  • 如果TLAB分配不成功,再尝试在堆上分配;

  • 如果满足了直接进入老年代的条件,就在老年代分配;

  • 否则就在eden区分配,当然,如有必要,可能会进行一次新生代GC.

6. finalize() 函数对垃圾回收的影响

java中提供了一个类似于C++析构函数的机制——finalize()函数,该函数允许在子类中被重载,用于在对象被回收时进行资源释放。目前普遍的认识是尽量不要使用finalize()函数进行资源释放,原因主要有以下几点:

  • finalize()函数时,可能会导致对象复活;

  • finalize()函数的执行时间是没有保障的,它完全由GC线程决定,在极端情况下,若不发生GC,finalize()将没有机会执行;

  • 一个糟糕的finalize()函数会严重影响GC性能。

虽然不推荐使用finalize()函数,但是在某些场合,使用finalize()函数可以起到双保险的作用。比如,在mysql的jdbc驱动中,com.mysql.jdbc.ConnectionImpl就实现了finalize()函数,实现代码如下:

protected void finalize() throws Throwable {
    this.cleanup((Throwable)null);
    super.finalize();
}

也就是,当一个jdbc connection被回收时,需要进行连接的关闭,即这里的cleanup()方法。事实上,在回收前,开发人员如果正常调用了Connection.close()方法,连接就会被显式关闭,那样的话,在cleanup()方法中将什么也不做。而如果开发人员忘记显式关闭连接,而Connection对象又被回收了,则会隐式地进行连接的关闭,确保没有数据库连接泄露。此时,finalize()函数可能会被作为一种补偿措施,在正常方法出现意外时进行补偿,尽可能确保系统稳定。当然,由于其调用时间的不确定性,这不能单独作为可靠的资源回收手段。


关于java垃圾收集器中内存分配和回收的一些细节是怎样的就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。