《实战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