对象内存分配与回收细节问题
1. 禁用System.gc()
System.gc()
:会显示直接触发Full GC,同时触发老年代和新生代进行回收。而一般情况是我们认为,垃圾回收时自动进行的,无需手动触发。频繁的垃圾回收对系统性能造成较大影响。可以使用-XX:+DisableExplicitGC
,则禁用显示GC,使得System.gc()
等价于一个空函数。
2. System.gc()使用并发回收
默认情况下,即时System.gc()生效,它会使用传统的Full GC的方式回收整个堆,而会忽略参数中断的UseG1GC
和UseConcMarkSweepGC
使用-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()函数
该函数允许在子类中被重载,用于在对象回收时进行资源的释放。但尽量不要使用它,主要原因有:
- finalize()可能导致对象复活
- finalize()执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finaliz()将没有机会执行。
- 一个糟糕的finalize()会严重影响GC的性能。