Java虚拟机--对象分配和回收的细节问题(九)

来源:互联网 发布:php 存储json 编辑:程序博客网 时间:2024/06/16 22:27
  • 禁用System.gc()
    • System.gc()会直接触发Full GC,同时对老年代和新生代进行回收;
    • 一般情况下垃圾回收应是自动进行的,无需手工触发;过于频繁地触发垃圾回收对系统性能没有好处;
    • 虚拟机提供了DisableExplicitGC来控制是否手工触发GC;
    • System.gc()的实现如下:

Runtime.getRuntime().gc();

  • Runtime.gc()是一个native方法,最终实现在jvm.cpp中,如下所示:

  • 如果设置了-XX:-+DisableExpblicitGC,条件判断就无法成立,那么就会禁用显示GC,使System.gc()等价于一个空函数调用;
  • System.gc()使用并发回收
    • System.gc()默认使用Full GC回收整个堆,会忽略参数中的UseG1GC和UseConcMarkSweepGC;
      • -XX:+ExplicitGCInvokesConeurrent:该参数会使System.gc()使用并发的方式进行回收;
  • 并行GC前额外触发的新生的GC
    • 并行回收器在每一次Full GC之前都会伴随一次新生代GC;
    • 示例:下面的代码只是进行了一次简单的Full GC

 

public class ScavengeBeforeFullGC {

public static void main(String[]args) {

System.gc();

}

}

使用参数:-XX:+PrintGCDetails -XX:+UseSerialGC运行程序,
 

效果:System.gc()触发了一个Full GC操作
[Full GC[Tenured: 0K->461K(87424K), 0.0101706 secs] 698K->461K(126720K), [Perm : 2562K->2562K(21248K)], 0.0103377 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

使用参数:-XX:+PrintGCDetails -XX:+UseParallelOldGC,运行程序
效果:使用并行回收器,触发Full GC之前,进行了一次新生代GC。

[GC [PSYoungGen: 675K->536K(38912K)] 675K->536K(125952K), 0.0051475 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

[Full GC [PSYoungGen: 536K->0K(38912K)] [ParOldGen: 0K->461K(87040K)] 536K->461K(125952K) [PSPermGen: 2562K->2561K(21504K)], 0.0208193 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]


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

  • -XX:-ScavengeBeforeFullGC:该参数会去除发生在Full GC之前的那次新生代GC,默认为true;
  • 对象何时进入老年代
    • 对象首次创建时,会被放置在新生代的eden区。没有GC的介入,这些对象不会离开eden;
      • 初创的对象在eden区:下面的代码申请了大约5MB内存

 

public class AllocEden {

public static final int_1K=1024;

public static void main(String[]args) {

for (inti = 0;i < 5*_1K;i++) {

byte[]b =new byte[_1K];

}

}

}

使用参数:-Xmx64M -Xms64M -XX:+PrintGCDetails
部分结果:

Heap

PSYoungGen total 19456K, used 6436K [0x00000000fea80000, 0x0000000100000000, 0x0000000100000000)

整个过程没有GC发生,一共分配的5MB数据都应该在堆中;

  • 老年对象进入老年代
    • 当对象的年龄达到一定的大小,自然会进入老年代。这种过程,被称为"晋升";
    • 对象的年龄由该对象经历的GC次数决定。每经历一次GC,没被回收,该次数加1。
      • MaxTenuringThreshold:控制新生代对象的最大年龄;默认上限为15;也就是说,新生代对象最多经历15次GC,即可晋升到老年代;
        • 注意:达到该条件,新生代对象必然晋升,但未达到该对象也有可能晋升!对象的晋升年龄是由虚拟机自行判断的!
    • 示例:

public class MaxTenuringThreshold {

public static final int _1M=1024*1024;

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);

}

  

for (int k = 0; k < 17; k++) {

for (int i = 0; i < 270; i++) {

byte[] g = new byte[_1M];

}

}

}

}

说明:
该代码申请了大约5M空间,在第一个for循环中将byte数组进行保存,防止它们在GC时被回收。
后面的循环在新生代不停的分配内存,已触发新生代GC
运行参数:
-Xmx1024M -Xms1024M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+PrintHeapAtGC
 


分析:
第一次GC开始前,eden使用了99%,这也是触发新生分代GC的原因。该区不能容纳更多对象,后面又要产生新的对象,自然要对eden进行清理,清理的结果是将存活对象移入了from。from区占用了13%。第一次GC后eden被清空;

  • 大对象进入老年代
    • 除了年龄外,对象的体积也会影响对象的晋升;如果对象体积过大,新生代无论eden或者survivor区无法容纳这个对象,就会直接晋升到老年代,如下图:
    • PretenureSizeThreshold:用来设置对象直接晋升到老年代的阈值,单位是字节。只要对象大于指定值,就会绕过新生代,直接分配到老年代。该参数只对串行回收器和ParNew有效,对ParallelGC无效。默认为0
  • 在TLAB上分配对象(Thread Local Allocation Buffer,线程本地分配缓存)
    • 存在的意义:加速对象分配;由于对象一般会分配在堆上,而堆是全局共享的。所以存在多个线程在堆上申请空间。这些分配的对象都必须进行同步,会降低效率;所以Java使用了TLAB这种线程专属的区间来避免多线程冲突;
      • 该区域占用eden空间;
      • 启用时,虚拟机会为每一个Java线程分配一块TLAB空间
    • 示例:启用与关闭TLAB的性能对比

 

public class UseTLAB {

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 < 10000000; i++) {

alloc();

}

long e = System.currentTimeMillis();

System.out.println(e-b);

}

}

启用TLAB,运行参数为:
-XX:+UseTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis -server

禁用TLAB,运行参数为:
-XX:-UseTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis -server

观察TLAB的使用情况,打开跟踪参数 -XX:+PrintTLAB,运行参数为:
-XX:+UseTLAB -XX:+PrintTLAB -XX:+PrintGC -XX:TLABSize=102400 -XX:-ResizeTLAB -XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis -server

跟踪TLAB的使用情况,结果如下图:

该日志分为两部分,首先是每一个线程的TLAB的使用情况,其次是以TLAB totals为首的整体TLAB的统计情况
desired_size:TLAB的大小;
slow allocs:从上次新生代GC到现在为止慢分配次数。慢分配是指由于TLAB空闲空间太小,不能满足较大对象的分配,而将该对象直接分配到堆上的次数;
refill waste:refill_waste的值;
alloc:表示当前的TLAB分配比例和使用评估量;
refills:表示该线程的TLAB空间被重新分配到填充的次数;
waste:表示空间的浪费比例;浪费的空间分为:gc,slow,fast;
gc:表示在当前新生代GC发生时,尚空闲的TLAB空间;
slow:当TLAB被废弃时没有被使用的TLAB空间;
fast:同slow作用,不同的是,fast表示这个refill操作是通过JIT编译优化的;
TLAB totals:显示了所有线程的使用情况;
thrds:显示了相关线程总数;
refills:表示所有线程refills的总数;
max:表示refills次数最多的线程的refills次数;

  • TLAB空间一般不大,大对象无法被分配到这里,而是直接分配到堆上。
    • 当空间快要被装满时,虚拟机有两种选择:
      • 废弃当前的TLAB,这样会浪费为被分配的TLAB空间;
      • 将大于TLAB剩余空间的对象直接分配到堆上;
    • 虚拟机的选择:
      • 虚拟机内部有一个refill_waste的值,当请求对象大于该值,会放入堆中,小于该值,会废弃当前TLAB,新建TLAB类分配新对象;
      • TLABRefillWasteFraction:用来调整refill_waste,它表示TLAB中允许产生这种浪费的比例;默认为64;
      • -XX:-ResizeTLAB:禁用ResizeTLAB;
      • -XX:TLABSize手工指定一个TLAB的大小;
  • 对象分配简要流程:
  • 方法finalize()对垃圾回收的影响
    • 该函数允许被子类重载,用于在对象被回收时进行资源的释放;尽量不要使用此函数,原因如下:
      • 可能会导致对象复活;
      • 该函数的执行完全由GC线程决定,若不发生GC,则该函数没有机会执行;
      • 影响性能;
    • 函数finalize()由FinalizerThread线程处理。每个即将被回收的并且包含finalize()的对象都会在回收前加入FinalizerThread的执行队列,该队列为java.lang.ref.ReferenceQueue引用队列,内部实现为链表结构。列队中每一项为java.lang.ref.Finalizer引用对象,它本质为一个引用。这和虚引用,弱引用如出一辙:
    • Finalizer内部封装了实际的回收对象,如下图:next,prev为实现链表所需,分别指向队列中的下一个元素和上一个元素,而referent字段则指向实际的对象引用

      • 由于对象在回收前被Finalizerreferent字段进行"强引用",并加入了FinalizerThread的执行队列,这意味着对象又变为可达对象,因此阻止了对象的正常的回收。由于在引用队列中的元素,排列执行finalize()方法,一旦出现性能问题,将导致这些垃圾对象长时间堆积在内存中,导致OOM异常;
    • FinalizerThread的工作过程和FinalizerThread执行队列中Finalizer的引用关系

    • 示例:finalize()的糟糕回收过程

public class LongFinalize {

public static class LF {

private byte[] content = new byte[512];

}

@Override

protected void finalize() throws Throwable {

try{

System.out.println(Thread.currentThread().getId());

Thread.sleep(1000);

}catch(Exception e){

e.printStackTrace();

}

}

public static void main(String[] args) {

long b = System.currentTimeMillis();

for (int i = 0; i < 50000; i++) {

LF f = new LF();

long e =System.currentTimeMillis();

System.out.println(e-b);

}

}

}

运行参数:
-Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="d:/f.dump"

分析:
上述代码,使用一个
sleep()方法模拟一个耗时操作,主函数则不断产生新的LF对象。结果发生了OOM错误并在D盘下得到了堆的Dump文件。

说明:
去掉LF类的finalize()方法,即注释本例代码中的重写方法,再次以相同的参数运行这段程序,程序会很快正常结束。

注意:
一个糟糕的finalize()可能会使对象长时间被Finalizer引用,而不得到释放,因此会进一步增加GC的压力
 

  • finalize()在特殊场合的应用
    • MySqlJDBC驱动中,com.mysql.jdbc.ConnectionImpl就实现了finalize()方法,实现如下:

protected void finalize() throws Throwable{

cleanup(null);

super.finalize();

}

  • 说明:当一个JDBC Connection被回收时,需要进行连接的关闭,即cleanup方法。在回收前,开发人员如果正常调用了Connection.close()方法,那么连接就会被显示关闭,cleanup()方法就什么都不做。如果开发人员没有关闭连接,而Connection对象又被回收了,则隐式进行连接的关闭,确保没有数据库连接的泄漏。
    • 在这里,finalize()是一种补偿措施,在开发人员疏忽时,进行补救的一种方式。这种方式的调用时间依然不确定,不能单独作为可靠的资源回收手段;
0 0