-
禁用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
| ||
效果:System.gc()触发了一个Full GC操作 | ||
使用参数:-XX:+PrintGCDetails -XX:+UseParallelOldGC,运行程序 [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]
|
- -XX:-ScavengeBeforeFullGC:该参数会去除发生在Full GC之前的那次新生代GC,默认为true;
-
对象何时进入老年代
-
对象首次创建时,会被放置在新生代的eden区。没有GC的介入,这些对象不会离开eden;
- 初创的对象在eden区:下面的代码申请了大约5MB内存
-
| ||
整个过程没有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]; } } } } | 说明:
|
-
大对象进入老年代
-
除了年龄外,对象的体积也会影响对象的晋升;如果对象体积过大,新生代无论eden或者survivor区无法容纳这个对象,就会直接晋升到老年代,如下图:
- PretenureSizeThreshold:用来设置对象直接晋升到老年代的阈值,单位是字节。只要对象大于指定值,就会绕过新生代,直接分配到老年代。该参数只对串行回收器和ParNew有效,对ParallelGC无效。默认为0
-
-
在TLAB上分配对象(Thread Local Allocation Buffer,线程本地分配缓存)
-
存在的意义:加速对象分配;由于对象一般会分配在堆上,而堆是全局共享的。所以存在多个线程在堆上申请空间。这些分配的对象都必须进行同步,会降低效率;所以Java使用了TLAB这种线程专属的区间来避免多线程冲突;
- 该区域占用eden空间;
- 启用时,虚拟机会为每一个Java线程分配一块TLAB空间
-
示例:启用与关闭TLAB的性能对比
-
跟踪TLAB的使用情况,结果如下图:
|
-
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字段则指向实际的对象引用
- 由于对象在回收前被Finalizer的referent字段进行"强引用",并加入了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); } } } | 运行参数: |
-
finalize()在特殊场合的应用
- 在MySql的JDBC驱动中,com.mysql.jdbc.ConnectionImpl就实现了finalize()方法,实现如下:
protected void finalize() throws Throwable{ cleanup(null); super.finalize(); } |
-
说明:当一个JDBC Connection被回收时,需要进行连接的关闭,即cleanup方法。在回收前,开发人员如果正常调用了Connection.close()方法,那么连接就会被显示关闭,cleanup()方法就什么都不做。如果开发人员没有关闭连接,而Connection对象又被回收了,则隐式进行连接的关闭,确保没有数据库连接的泄漏。
- 在这里,finalize()是一种补偿措施,在开发人员疏忽时,进行补救的一种方式。这种方式的调用时间依然不确定,不能单独作为可靠的资源回收手段;