vlambda博客
学习文章列表

《实战java虚拟机》05-垃圾回收器和内存分配(二)内存分配篇

第五章 垃圾回收器和内存分配(二)内存分配篇

回眸:有关对象内存分配的一些细节问题

在TLAB上分配对象

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存。TLAB是一个线程专用的内存分配区域。

为什么需要TALB区域呢?这是为了加速对象分配,由于对象一般会分配在堆上,而堆是全局共享的。在同一时间可能会有多个线程在堆上申请空间。因此每一次对象分配都必须进行同步,而在竞争激烈的场合分配的效率又会进一步下降。考虑到对象分配几乎是Java最常用的操作,因此Java虚拟机就使用了TLAB这种线程专属的区域来避免多线程冲突,提高对象分配的效率。TLAB本身占用了eden区空间,在TLAB启用的情况下,虚拟机会为每一个Java线程分配一块TLAB区域。

示例:启用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 < 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区域进行分配,总会直接分配在堆上。

比如,一个100KB的空间,如果已经使用了80KB,当需要再分配30KB对象时,肯定就无能为力了。这次,虚拟机有两种选择:第一,废弃当前TLAB区域,这样会就浪费20KB。第二,将这30KB的对象直接分配在堆上,保留TLAB区域,后续如果有小于20MB的对象可以直接存入。

虚拟机内部维护了一个refill_waste的值,当请求对象大于refill_waste,会在堆中分配,小于则废弃TLAB区域,新建TLAB来存放该对象。

这个阈值通过TLABRefillWasteFraction来调整,表示TLAB中允许浪费的区域比例。默认为64。即表示使用约1/64的TLAB区域作为refill_waste

默认情况下,TLAB和refill_waste的大小都是在运行时不断动态调整的,如果想禁用自动调整TLAB大小,可以使用-XX:-ResizeTLAB参数,并使用-XX:TLABSize手工指定TLAB大小。

对象的分配流程:

  • 如果开启了栈上分配,系统就会先进行栈上分配;
  • 没有开启栈上分配或者不符合条件则会进行TLAB分配;
  • 如果TLAB分配不成功,再尝试在堆上分配;
  • 如果满足了直接进入老年代的条件,就在老年代分配;
  • 否则就在eden区分配,当然,如有必要,可能会进行一次新生代GC;
对象的分配流程

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

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

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

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

虽然不推荐使用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()函数可能会被作为一种补偿措施,在正常方法出现意外时进行补偿,尽可能确保系统稳定。当然,由于其调用时间的不确定性,这不能单独作为可靠的资源回收手段。

温故又知新:常用的GC参数

(1)与串行回收器相关的参数

  • -XX:UseSerialGC:在新生代和老年代使用串行回收器。
  • -XX:SurvivorRatio:设置eden区和survivor区比例。
  • -XX:PretenureSizeThreshold:设置大对象直接进入老年代的阈值。
  • -XX:MaxTenuringThreshold:设置进入老年代的最大年龄值。

(2)与并行回收器相关的参数

  • -XX:+UseParNewGC:新生代使用并行回收器(jdk9后删除)。
  • -XX:+UseParallelOldGC:老年代使用并行回收器。
  • -XX:ParallelGCThreads:设置用户垃圾回收的线程数,通常和CPU数量相等。
  • -XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。它的值是一个大于0的整数。回收器在工作时,会调整Java堆大小,尽可能把停顿时间控制在MaxGCPauseMillis之内。
  • -XX:GCTimeRatio:设置吞吐量大小。值是0-100的整数,如果值为n,则系统花费不超过1/(1+n)的时间用于回收。
  • -XX:+UseAdaptiveSizePolicy:打开自适应GC策略。自动调整新生代大小、eden区和survivor区比例、晋升老年代对象年龄等参数。

(3)与CMS回收器相关的参数(JDK9后废弃)

  • -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器。
  • -XX:ParallelCMSThreads:设定CMS的线程数量。
  • -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发,默认为68%。
  • -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理。
  • -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩。
  • -XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收。
  • -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收(前提是 -XX:+CMSClassUnloadingEnabled激活了)。
  • -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阈值的时候才进行CMS回收。
  • -XX:+CMSIncrementalMode:使用增量模式,比较适合单CPU。增量模式在JDK8中标记为废弃,并且将在JDK9中彻底移除。

(4)与G1回收器相关的参数

  • -XX:+UseG1GC:使用G1回收器。
  • -XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。
  • -XX:GCPauseIntervalMillis:设置停顿间隔时间。

(5)TLAB相关

  • -XX:+UseTLAB:开启TLAB分配。
  • -XX:+PrintTLAB:打印TLAB分配信息(jdk9后删除)。
  • -XX:TLABSize:设置TLAB区域大小。
  • -XX:+ResizeTLAB:自动调整TLAB区域大小。

(6)其他参数

  • -XX:+DisableExplicitGC:禁用显式GC
  • -XX:+ExplicitGCInvokesConcurrent:使用并发方式处理显式GC