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字段则指向实际的对象引用
- 由于对象在回收前被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);
}
}
}
运行参数:
-Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="d:/f.dump"
分析:
上述代码,使用一个sleep()方法模拟一个耗时操作,主函数则不断产生新的LF对象。结果发生了OOM错误并在D盘下得到了堆的Dump文件。
说明:
去掉LF类的finalize()方法,即注释本例代码中的重写方法,再次以相同的参数运行这段程序,程序会很快正常结束。
注意:
一个糟糕的finalize()可能会使对象长时间被Finalizer引用,而不得到释放,因此会进一步增加GC的压力
- 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()是一种补偿措施,在开发人员疏忽时,进行补救的一种方式。这种方式的调用时间依然不确定,不能单独作为可靠的资源回收手段;
- Java虚拟机--对象分配和回收的细节问题(九)
- java 对象的内存分配和回收
- Java虚拟机 - 对象内存分配与回收
- Java虚拟机堆的内存分配和回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机学习(7):对象内存分配与回收
- 9.《深入理解Java虚拟机》对象分配与回收策略
- Java虚拟机学习(7):对象内存分配与回收
- Java虚拟机学习 - 对象内存分配与回收
- Java虚拟机--对象回收
- Java虚拟机原理、内存分配和回收机制
- Java虚拟机原理、内存分配和回收机制
- Java虚拟机原理、内存分配和回收机制
- C++实现双向循环链表
- LeetCode (Roman to Integer)
- BF算法(Brute Force)
- openssl动态库生成以及交叉编译
- Android Telephony分析(二) ---- RegistrantList详解
- Java虚拟机--对象分配和回收的细节问题(九)
- 浅析TCP字节流与UDP数据报的区别
- 冒泡排序
- 2017 年你不能错过的 Java 类库
- Linux内核安装补丁
- [leetcode] 554. Brick Wall
- 如何卸载rpm包
- Centos 下搭建FTP上传下载服务器
- oracle过滤重复数据,只保留一条