对象内存分配与回收细节问题
标签:JVM

对象内存分配与回收细节问题

1. 禁用System.gc()

System.gc():会显示直接触发Full GC,同时触发老年代和新生代进行回收。而一般情况是我们认为,垃圾回收时自动进行的,无需手动触发。频繁的垃圾回收对系统性能造成较大影响。可以使用-XX:+DisableExplicitGC,则禁用显示GC,使得System.gc()等价于一个空函数。

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

默认情况下,即时System.gc()生效,它会使用传统的Full GC的方式回收整个堆,而会忽略参数中断的UseG1GCUseConcMarkSweepGC

使用-XX:+ExplicitGCInvokesConcurrent后,System.gc()这种显示GC才会使用并发发的方式进行回收。否者无论是否启用CMS和G1,都不会进行并发回收。

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

对于并回收器(使用UseParallelGC或者UseParallelOldGC),在每一次Full GC之前都会伴随一次新生代的GC

故使用并发回收器时,使用System.gc()会触发两次GC,这样做的目的是先将新生代进行一次收集,避免将所有回收工作同时交给一次Full GC进行,这样可以缩短一次停顿时间。

-XX:-ScavengeBeforeFullGC 可以关闭上面这个特性,默认的情况下,这个是true的。

4. 对象何时进入老年代

4.1 老年对象进入老年代

初创的对象在eden区域中存放


当对象每经过一次GC,对象的年龄就会加一,当对象的年龄达到一定的值时,对象就可以晋升为老年代的对象。这个值可以通过MaxTenuringThreshold设定,默认值是15。

package com.liuyao;

import java.util.HashMap;
import java.util.Map;

/**
 * Created By liuyao on 2018/4/18 15:56.
 */
public class MaxTenuringThreshold {
    public final static int _1M=1024*1024;
    public final static int _1K=1024;

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

        for (int i = 0; i < 17; i++) {
            for (int j = 0; j < 270; j++) {
                byte[] bytes=new byte[_1M];
            }
        }
    }
}

运行参数:

-Xmx1024M -Xms1024M -XX:MaxTenuringThreshold=10 -XX:+UseSerialGC -XX:+PrintCommandLineFlags  -XX:+PrintGCDetails

修改运行参数为:

-Xmx1024M -Xms1024M -XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=15 -XX:+UseSerialGC  -XX:+PrintCommandLineFlags  -XX:+PrintGCDetails

MaxTenuringThreshold是对象晋升的充分非必要条件,即达到这个值,对象必然晋升,而未达到该年龄,对象也可能晋升。事实上,对象的实际晋升年龄,是由虚拟机在运行时自行判断的。

确定对象晋升的另外一个重要参数是:TargetSurvivorRatio,它用于设置survivor区的目标使用率,默认是50,即如果survivor区在GC超过50%的使用率,就可能会使用一个较小的age作为晋升年龄

综上:对象的实际晋升年龄是根据survivor区的使用情况动态计算得来的,而MaxTenuringThreshold只是表示这个年龄的最大值

4.2 大对象进入老年代

除了年龄以外,对象的体积也会影响对象的晋升。如果一个对象很大,新生代无论eden区还是survivor区都没办法容乃对象,那么对象将直接分配在老年代。

PretenureSizeThreshold:它用来设置对象直接晋升到老年代的阈值,单位值字节。只要对象大于该值,就会绕过新生代,直接分配到老年代中。该参数只对ParNew收集器有效,对ParallelGC无效,该值的默认值是0,表示不设定该值的大小,一切由运行情况而定。

5. 在TLAB上分配对象

TLAB 全称叫做Thread Local Allocation Buffer 线程本地分配缓存,是一个线程专用的内存分配区域。为加速对象分配而生的。

TLAB本身占用了eden区的空间,在TLAB启用的情况下,虚拟机会为每一个Java线程分配一块TLAB空间。

由于TLAB空间一般不会太大,因此大对象无法在TLAB上面分配,总是会直接分配在堆上,TLAB由于空间小,总是容易装满,假如一个100KB的空间,如果已经使用80KB,现在要分配一个30KB的对象,有两种办法:

  1. 废弃当前的TLAB,将会浪费20KB空间
  2. 将这30KB的对象直接分配在堆上,希望以后有小于20KB的对象分配在TLAB上

虚拟机内部会维护一个refill_waste的值,当需求大于refill_waste时,会选择在堆上分配,若小于该值,将废弃当前TLAB,新建TLAB来分配对象。这个值可以通过-XX:TLABRefillWasteFraction来设置,它表示TLAB中允许产生这种浪费的比例,默认值是64,即表示可以使用约1/64的TLAB空间作为refill_waste。

默认情况下,TLAB和refill_waste都是会在运行时不断调整的,使系统运行达到最优。

-XX:+PrintTLAB:查看TLAB使用情况
-XX:-ResizeTLAB:禁用自动调整TLAB大小
-XX:TLABSize:手工指定TLAB大小

6. 方法finalize()对垃圾回收的影响

Java提供了类似C++中析构函数的机制——finalize()函数

该函数允许在子类中被重载,用于在对象回收时进行资源的释放。但尽量不要使用它,主要原因有:

  1. finalize()可能导致对象复活
  2. finalize()执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finaliz()将没有机会执行。
  3. 一个糟糕的finalize()会严重影响GC的性能。
  • 6 min read

CONTRIBUTORS


  • 6 min read