1 判断对象是否可以回收
1.1 引用计数法
如果两个对象互相引用,计数器都为1,即使他们都没有被使用,都不会被清理。
1.2 可达性分析算法
Java虚拟机中的垃圾回收器采用可达性分析来探索所有的对象。
扫描堆中的对象,判断是否能根据GC Root对象的引用链找到该对象,找不到则回收。
1.2.1 哪些对象可以作为GC Root对象?
- 使用Eclipse开发的一款分析内存的软件MAT.
- jps获取进程号
- jmap -dump:format=b,live,file=a.bin 进程ID ;
- MAT打开b.bin(文件名)
public class rootSearch {
public static void main(String[] args) throws InterruptedException , IOException {
List list = new ArrayList<>();
list.add("a");
list.add("b");
System.out.println(1);
//jmap -dump:format=b,live,file=a.bin 12436
System.in.read();
list = null;
System.out.println(2);
//jmap -dump:format=b,live,file=b.bin 12436
System.in.read();
System.out.println("end");
}
}
1.3 五种引用
强引用 软引用 弱引用 虚引用 终结器引用
1.3.1引用关系回收判断:
- 强引用,指向某一对象的所有强引用都断开,该对象才能被回收。
(比如User user = new User(),就把堆中的user对象引用给了栈帧中的user,只有断开之后才会被回收) - 软、弱引用,指向某一被软、弱引用的对象的所有 强引用 都断开,该对象可能被回收。
- 软引用:如果垃圾回收之后,内存依然不足,被软引用的对象会被回收。
- 弱引用:只要发生垃圾回收,只被弱引用的对象就会被回收。
- 对象回收后,软、弱引用本身转移到引用队列中。
- 遍历引用队列,释放引用。
- 虚引用
- 虚引用的ByteBuffer,没有被强引用,被回收掉,分配的直接内存尚未回收
- 虚引用进入引用队列中,RefferenceHandler在队列中寻找到虚引用Cleaner
- 调用Unsafe.freeMemory()方法释放直接内存;
- 释放引用。
- 终结器引用
- 终结器对象引用的对象没有被强引用,在被回收前,终结器引用转移到引用队列,一个优先级较低的线程finallize在引用队列中寻找终结器引用;
- 并找到终结器引用的对象,调用finalize()方法(不建议使用,判断机制复杂,效率太低);
- 下次垃圾回收时,回收该对象。
1.3.2 五种引用举例
- 强引用:一般平常代码中大部分引用都是强引用。
//-Xmx20m -XX:+PrintGCDetails -verbose:gc
public class Demo5 {
public static final int _4MB = 1024*1024*4;
public static void main(String[] args) {
List list1 = new ArrayList<>();
for (int i = 0;i < 5;i++){
list1.add(new byte[_4MB]);
System.out.println(list1.get(i));
}
System.out.println(list1);
}
}
将堆内存最大设置为20M后,由于强引用byte数组无法被gc回收,造成了堆内存溢出,如下图。
- 软引用
public class Demo5 {
public static final int _4MB = 1024*1024*4;
public static void main(String[] args) {
List> list = new ArrayList<>();
for (int i = 0;i < 5;i++){
SoftReference ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束,"+ list.size());
for (SoftReference ref:list){
System.out.println(ref.get());
}
}
}
打印结果如下所示。GC日志可以看出,Eden区内存不足触发的 minor GC只是将新生代对象转移到老年代,并没有回收。在老年代内存不足时,触发FULL GC,但是第一次触发并未回收,直到第二次触发再回收所有软引用对象。即最后打印发现前4个都为null,只打印出最后一个。
[B@6ff3c5b5
1
[B@3764951d
2
[B@4b1210ee
3
[GC (Allocation Failure) [PSYoungGen: 2390K->504K(6144K)] 14678K->13096K(19968K), 0.0011594 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@4d7e1886
4
[GC (Allocation Failure) --[PSYoungGen: 4712K->4712K(6144K)] 17304K->17392K(19968K), 0.0007981 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 4712K->4517K(6144K)] [ParOldGen: 12680K->12618K(13824K)] 17392K->17136K(19968K), [Metaspace: 3452K->3452K(1056768K)], 0.0052533 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 4517K->4517K(6144K)] 17136K->17144K(19968K), 0.0010589 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 4517K->0K(6144K)] [ParOldGen: 12626K->733K(8704K)] 17144K->733K(14848K), [Metaspace: 3452K->3452K(1056768K)], 0.0081892 secs] [Times: user=0.13 sys=0.00, real=0.01 secs]
[B@3cd1a2f1
5
循环结束,5
null
null
null
null
[B@3cd1a2f1
Heap
PSYoungGen total 6144K, used 4265K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 5632K, 75% used [0x00000000ff980000,0x00000000ffdaa450,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 8704K, used 733K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
object space 8704K, 8% used [0x00000000fec00000,0x00000000fecb7760,0x00000000ff480000)
Metaspace used 3459K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
软引用配合引用队列:
public class Demo5 {
public static final int _4MB = 1024*1024*4;
public static void main(String[] args) {
List> list = new ArrayList<>();
//引用队列
ReferenceQueue queue = new ReferenceQueue<>();
for (int i = 0;i < 5;i++){
//关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
SoftReference ref = new SoftReference<>(new byte[_4MB],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
Reference extends byte[]> poll = queue.poll();
while(poll !=null){
//将软引用中的list对应的元素删除
list.remove(poll);
poll = queue.poll();
}
System.out.println("循环结束,"+ list.size());
for (SoftReference ref:list){
System.out.println(ref.get());
}
}
}
最后发现不像上一次输出了很多null,list中的null已经被去除了。
- 弱引用
public class Demo5 {
public static final int _4MB = 1024*1024*4;
public static void main(String[] args) {
List> list = new ArrayList<>();
for (int i = 0;i < 10;i++){
WeakReference ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference ref1:list){
System.out.print(ref1.get()+" ");
}
System.out.println();
}
}
}
执行结果如下。弱引用对象在首次触发minor GC时,将弱引用对象转移到老年代中,再次触发 minor GC时,回收在Eden中的新生对象。因为软引用占用内存,在没有及时回收引用时,老年代内存逐渐被软引用占用,直到内存不足,触发FULL GC ,回收所有的弱引用对象。若要回收弱引用,可以参考上面的放入引用队列配合删除。
[B@6ff3c5b5
[B@6ff3c5b5 [B@3764951d
[B@6ff3c5b5 [B@3764951d [B@4b1210ee
[GC (Allocation Failure) [PSYoungGen: 2387K->496K(6144K)] 14675K->13092K(19968K), 0.0013523 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@6ff3c5b5 [B@3764951d [B@4b1210ee [B@4d7e1886
[GC (Allocation Failure) [PSYoungGen: 4704K->496K(6144K)] 17300K->13172K(19968K), 0.0008980 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@6ff3c5b5 [B@3764951d [B@4b1210ee null [B@3cd1a2f1
[GC (Allocation Failure) [PSYoungGen: 4704K->480K(6144K)] 17380K->13188K(19968K), 0.0006768 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@6ff3c5b5 [B@3764951d [B@4b1210ee null null [B@2f0e140b
[GC (Allocation Failure) [PSYoungGen: 4687K->480K(6144K)] 17395K->13252K(19968K), 0.0006705 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@6ff3c5b5 [B@3764951d [B@4b1210ee null null null [B@7440e464
[GC (Allocation Failure) [PSYoungGen: 4686K->496K(6144K)] 17459K->13268K(19968K), 0.0007710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@6ff3c5b5 [B@3764951d [B@4b1210ee null null null null [B@49476842
[GC (Allocation Failure) [PSYoungGen: 4702K->496K(5120K)] 17474K->13268K(18944K), 0.0009481 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[B@6ff3c5b5 [B@3764951d [B@4b1210ee null null null null null [B@78308db1
[GC (Allocation Failure) [PSYoungGen: 4682K->64K(5632K)] 17454K->13264K(19456K), 0.0005649 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 64K->0K(5632K)] [ParOldGen: 13200K->752K(8192K)] 13264K->752K(13824K), [Metaspace: 3450K->3450K(1056768K)], 0.0066990 secs] [Times: user=0.13 sys=0.00, real=0.01 secs]
null null null null null null null null null [B@27c170f0
Heap
PSYoungGen total 5632K, used 4278K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
eden space 4608K, 92% used [0x00000000ff980000,0x00000000ffdadb18,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 8192K, used 752K [0x00000000fec00000, 0x00000000ff400000, 0x00000000ff980000)
object space 8192K, 9% used [0x00000000fec00000,0x00000000fecbc0f8,0x00000000ff400000)
Metaspace used 3457K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
2 垃圾回收算法
2.1 标记清除
- 速度较快
- 会造成内存碎片化
注:标记后清除并不会真的清除,是把起始地址放入空闲地址列表,下次直接数据覆盖就行了。
2.2 标记整理
与标记清除的区别是:它会在清理垃圾的过程中,把可用的对象向前移动,从而让内存更为紧凑,避免内存碎片。
- 速度慢
- 不会有内存碎片
2.3 复制
把from存活的对象复制到to中,并完成碎片整理,然后清空from,最后交换from,to。
- 不会有内存碎片
-
需要占用双倍内存空间
2.4 总结
- 复制算法回收速度与存活对象数量相关,数量越少,速度越快,如果存活对象过多,速度可能会比标记整理速度要慢。
- 实际JVM垃圾回收是根据情况配合三种算法使用,因为三种算法都有其实用的场景
3 分代回收
- 对象首先分配在伊甸园(Eden)区内(如果新生代放不下会直接放入老年代);
- 在新生代空间不足时,触发Minor GC,将Eden和From中存活的对象复制到To中,存活的对象年龄加1,将Eden和From中标记为垃圾的对象清除,然后交换To和From;
- Minor GC触发 stop the world(STW),暂停其他用户线程,直到垃圾回收完毕,才恢复线程运行。
- 当对象寿命超过阈值时,会晋升至老年代,最大阈值为15(4bit)。
- 设置-XXPretenureSizeThreshold=3145728(3M)大对象会直接进入老年代。
- 当老年代空间不足,尝试Minor GC,空间仍不足,触发Full GC,触发 stop the world,时间相比于Minor GC会长的多,因为新生代和老年代回收算法不同,且老年代中对象多,回收较为复杂。如果依旧回收失败,则会触发堆内存溢出异常(注:多线程下某一个线程出现内存溢出不会导致其他线程停止运行)。
3.1 相关VM参数
4 垃圾回收器
- 串行
- 单线程
- 适合堆内存较小的场景
- 吞吐量优先
- 多线程
- 适合堆内存大,多核cpu的场景
- 单位时间内STW时间最小 0.2+0.2 = 0.4
- 响应时间优先
- 多线程
- 适合堆内存大,多核cpu的场景
- 尽可能单次STW时间减小 0.1+0.1+0.1+0.1+0.1=0.5
4.1 串行垃圾回收器
通过 -XX:+UseSerialGC=Serial + SerialOld
开启串行垃圾回收器。
其中新生代使用Serial,老年代使用SerialOld垃圾回收器。
- Serial垃圾回收器使用的是 复制 算法,新生代中对象变化很大,存活对象相对于其他分区比较少,使用复制算法能够有效地提高效率。
- SerialOld使用的是 标记整理 算法,老年代存活的都是些价值很高的对象,回收的对象数量很少,整理耗用很小,复制存活对象会消耗更多的时间,且会耗用多一倍的内存空间(老年代本身空间就已经较大)。
4.2 吞吐量优先垃圾回收器
- MaxGCPauseMillis设置回收停顿的最大时间。收集器尽可能将垃圾回收消耗时间保持在这个数值之下;但是并不是把这个数设置越小,垃圾收集就越快。
停顿时间减少是以牺牲吞吐量和新生代空间换来的,系统会将新生代内存降低,来达到每次回收停顿时间缩小,但是这也导致了停顿次数增加,总停顿时间并没有减少,吞吐量减小。 - GCTimeRatio设置一个1-99(默认99)的整数,1/(1+X)表示垃圾回收时间占总时间的比率,整数设置越大,吞吐量越大。
这又回到上面那个问题,整数设置越大相当于停顿时间减少,系统会减少新生代内存,导致停顿增加,吞吐量减小。 - 所以这两个参数不可一味的增加某一个,他们互相制约,寻找一个平衡。
- -XX:+UseAdaptiveSizePolicy 系统会根据系统运行情况修改新生代大小(-Xmn),Eden和Survivor比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数;来提供最合适的停顿时间和最大吞吐量。
总结
对于不是很了解收集器运作的开发人员来说,ParallelGC是个更好的选择。事先设置好堆大小-Xmx,然后设置 MaxGCPauseMillis(重视停顿时间)或者GCTimeRatio(重视吞吐量);设置完优化目标,剩下的具体参数修改交给系统自己。这也是ParallelGC和ParNew一个重要的区别。
4.3 响应时间优先垃圾回收器
- 老年代使用CMS垃圾回收器,使用的是标记清除算法。
- 新生代ParNewGC垃圾收集器,复制算法(ParNewGC是多线程版本的Serial)。
- -XX:ParallelGCThread=n,并行线程数,一般为cpu核心数,图中N=4。
- -XX:ConcGCThreads=threads,一般设置为n/4,并发垃圾回收线程数。
- -XX:CMSInitiatingOccupancyFraction=percent,达到比例就会出发老年代垃圾回收。
- jdk1.6将这一值从1.5中的68改为92,为了防止频繁的触发老年代垃圾回收;
- 为什么要设置这一比例?
因为在并发垃圾清理的同时,其它用户线程也在运行,产生的浮动垃圾CMS当前批次中难以处理,只能留到下次GC;也正是因为如此,所以需要留下空间给用户线程运行所产生的对象使用。 - 如何设置最好?
1). 在内存无法满足运行需要时,会出现Concurrent Mode Failure失败,VM会使用Serial Old代替CMS对老年代中垃圾对象进行整理,停顿时间更长,比例设置过高,更加容易触发Concurrent Mode Failure失败,影响性能(Serial Old暂停所有用户线程防止产生更多的对象,同时可以清理内存碎片)。
2). 如果项目产生对象不是很快可以将这一比例设置高一点,这样既不会很快出现Concurrent Mode Failure失败,也不会太容易触发GC。 - 因为CMS老年代使标记-清除算法,会产生很多碎片内存,会给大对象分配内存带来麻烦,则会出现老年代内存很充裕,但是大对象放不进去,不得不触发FULL GC;-XX:+UseCMSCompactAtFullCollection默认开启,在CMS要开始FULL GC时,开启内存碎片的合并,无法并发,整理时间变长。
- -XX:CMSFullGCsBeforeCompaction,设置执行多少次不压缩的FULL GC后,执行一次带压缩的FULL GC。
5 G1
定义: Garbage First,jdk9后默认垃圾收集器。
适用场景:
- 同时注重吞吐量和低延迟,默认暂停目标时200ms。
- 超大堆内存会将内存划分为多个相等的Region。
- 整体上使用标记-整理算法,两个区域使用复制算法。
相关VM参数:
- -XX:UseG1GC
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
5.1 G1垃圾回收阶段
5.2 Young Collection
如图所示:
- 内存被分为很多个大小相等的区域(Region)。
- E(Eden)区内存不足时触发minor GC,将存活对象转移到S(Surviror)区(To),From中存活对象转移到To中(S->S),From和To调换位置(minor GC 后Surviror中对象存在于From中)。
- S->O,幸存区对象晋升到老年代。
5.3 Young Collection +CM
如图所示:
- 在Young GC时会进行GC ROOT的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),阈值由下面的设置JVM参数决定。
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
5.4 MixedCollection
会堆E、S、O进行全面垃圾回收。
如图所示:
- 最终标记会STW
- 拷贝存活会STW
- 可以通过以下指令设置最大GC停顿时间(默认200毫秒)。
-XX:MaxGcPauseMillis=200
注:
- O->O,回收老年代中的垃圾使用复制算法,将一个老年代区域中存活的对象复制到另一个老年代区域。
- G1垃圾收集器选择回收价值高的老年代区域(即可回收较大的空间),为了在MaxGCPauseMillis时间内完成垃圾回收。
- 最终标记,需要STW,避免其他用户线程在标记过程中,产生浮动垃圾。
5.5 Full GC
SerialGC
- 新生代内存不足minor GC
- 老年代内存不足Full GC
ParallelGC
- 新生代内存不足minor GC
- 老年代内存不足Full GC
CMS
- 新生代内存不足minor GC
- 老年代内存不足
- 并发成功继续并发收集
- 并发失败会触发FULL GC
G1
- 新生代内存不足minor GC
- 老年代内存不足
- 老年代占用达到阈值(默认45%)时,若垃圾回收大于垃圾产生速度,继续并发垃圾收集
- 反之,会发生FULL GC
5.6 Young Collection 跨代引用
- 老年代对象引用了新生代对象,跨代引用。
- 老年代中红色区域为卡表中的脏卡,新生代对象通过Remember Set记录对应脏卡。
- 在引用变更时,post-write barrier更新脏卡指令放入dirty card queue队列中,等待线程执行。
- current refinement threads 更新 Remember Set。
5.7 Remark
- 图中黑色表示已经处理完毕(并且被引用),灰色表示正在处理,白色表示还未处理,箭头代表引用。
- 灰色引用白色对象,由于是并发收集,所以该对象存在被其他线程取消引用的可能。
- 当然也有可能将本来已经被回收的没有被引用的白色对象重新被引用,但是对象已经被回收,无法再引用。
- 采用写屏障(per-write barrior )在引用关系改变之前设置屏障并放入satb_mark_queue队列中,直到收集完毕。
- 收集完毕后,remark 队列中的对象,判断是否为垃圾,此时STW。
5.8 JDK 8u20 字符串去重
优点:节约大量内存
缺点:略微多占用cpu运行时间,新生代回收时间略微增加
-XX:+UseStringDEduplication
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,检查是否有重复的字符串
- 相同的字符串指向同样的字符数组char[]
- 注意 与String.intern()不同
1. String.intern()注重的时字符串对象
2. 字符串去重更加注重char[]
3. JVM内部使用不同的字符串去重
5.9 JDK 8u40 并发标记类卸载
所有对象在并发标记后,直到哪些类不在被使用,当一个类加载器内所有的类都不再被使用,则卸载他加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark 默认开启
5.10 JDK 8u60 回收巨型对象
- 一个对象大于region的一半时,被称为巨型对象。
- G1不会对巨型对象进行复制(太大,复制算法好费时间)
- 回收优先考虑。
- G1会跟踪老年代所有的incoming引用,老年代引用为0时的巨型对象就可以在新生代的垃圾回收中被处理。
5.11 JDK9并发标记起始时间调整
- 并发标记必须在堆空间沾满之前完成,否则触发FULL GC
- JDK9之前使用-XX:InitiatingHeapOccupancyPercent
- JDK9 可以去动态调整
- -XX:InitiatingHeapOccupancyPercent设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
6 GC调优
在cmd中输入以下命令查看虚拟机运行参数
"jdk下bin目录下java命令的绝对地址" -XX:+PrintFlagFinal -version | findstr "GC"
"C:\Program Files\Java\jdk1.8.0_181\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AutoGCSelectPauseMillis = 5000 {product}
bool BindGCTaskThreadsToCPUs = false {product}
uintx CMSFullGCsBeforeCompaction = 0 {product}
uintx ConcGCThreads = 0 {product}
bool DisableExplicitGC = false {product}
bool ExplicitGCInvokesConcurrent = false {product}
bool ExplicitGCInvokesConcurrentAndUnloadsClasses = false {product}
uintx G1MixedGCCountTarget = 8 {product}
uintx GCDrainStackTargetSize = 64 {product}
uintx GCHeapFreeLimit = 2 {product}
uintx GCLockerEdenExpansionPercent = 5 {product}
bool GCLockerInvokesConcurrent = false {product}
uintx GCLogFileSize = 8192 {product}
uintx GCPauseIntervalMillis = 0 {product}
uintx GCTaskTimeStampEntries = 200 {product}
uintx GCTimeLimit = 98 {product}
uintx GCTimeRatio = 99 {product}
bool HeapDumpAfterFullGC = false {manageable}
bool HeapDumpBeforeFullGC = false {manageable}
uintx HeapSizePerGCThread = 87241520 {product}
uintx MaxGCMinorPauseMillis = 4294967295 {product}
uintx MaxGCPauseMillis = 4294967295 {product}
uintx NumberOfGCLogFiles = 0 {product}
intx ParGCArrayScanChunk = 50 {product}
uintx ParGCDesiredObjsFromOverflowList = 20 {product}
bool ParGCTrimOverflow = true {product}
bool ParGCUseLocalOverflow = false {product}
uintx ParallelGCBufferWastePct = 10 {product}
uintx ParallelGCThreads = 10 {product}
bool ParallelGCVerbose = false {product}
bool PrintClassHistogramAfterFullGC = false {manageable}
bool PrintClassHistogramBeforeFullGC = false {manageable}
bool PrintGC = false {manageable}
bool PrintGCApplicationConcurrentTime = false {product}
bool PrintGCApplicationStoppedTime = false {product}
bool PrintGCCause = true {product}
bool PrintGCDateStamps = false {manageable}
bool PrintGCDetails = false {manageable}
bool PrintGCID = false {manageable}
bool PrintGCTaskTimeStamps = false {product}
bool PrintGCTimeStamps = false {manageable}
bool PrintHeapAtGC = false {product rw}
bool PrintHeapAtGCExtended = false {product rw}
bool PrintJNIGCStalls = false {product}
bool PrintParallelOldGCPhaseTimes = false {product}
bool PrintReferenceGC = false {product}
bool ScavengeBeforeFullGC = true {product}
bool TraceDynamicGCThreads = false {product}
bool TraceParallelOldGCTasks = false {product}
bool UseAdaptiveGCBoundary = false {product}
bool UseAdaptiveSizeDecayMajorGCCost = true {product}
bool UseAdaptiveSizePolicyWithSystemGC = false {product}
bool UseAutoGCSelectPolicy = false {product}
bool UseConcMarkSweepGC = false {product}
bool UseDynamicNumberOfGCThreads = false {product}
bool UseG1GC = false {product}
bool UseGCLogFileRotation = false {product}
bool UseGCOverheadLimit = true {product}
bool UseGCTaskAffinity = false {product}
bool UseMaximumCompactionOnSystemGC = true {product}
bool UseParNewGC = false {product}
bool UseParallelGC := true {product}
bool UseParallelOldGC = true {product}
bool UseSerialGC = false {product}
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
6.1 调优领域
- 内存
- 锁竞争
- cpu占用
- io
6.2 调优目标
- 低延迟 还是 高吞吐量 ,选择合适收集器
- CMS G1 ZGC(超低延迟)
- Parallel GC
6.3 最快的GC是不发生GC
- 查看FULL GC前后的内存占用,考虑下面几个问题
- 数据是不是太多
- result = statement.Query("select * from bigTable limit n");
- 数据表示太臃肿
- 对象图(用到哪个查哪个)
- 对象大小 16字节 Integer 24 int 4
- 是否存在内存泄漏
- static Map map 不断加入对象,强引用不得回收
- 软
- 弱
- 第三方缓存产品 redis等
- 数据是不是太多
6.4 新生代调优
6.4.1 新生代特点:
- 所有new操作分配的内存都是廉价的
- TLAB thread local allocation buffer
- 死亡对象回收代价是0
- 垃圾回收将Eden和From中存活的对象复制到To中,剩下的垃圾对象直接全部删除,代价很小。
- 大部分对象用过即死
- Minor GC比Full GC 时间小很多。
6.4.2 新生代是否越大越好
- 建议新生代大小设置为25%-50%的堆内存大小。
- 新生代过小,会频繁触发Minor GC。
- 新生代过大,老年代相应的变小,FULL GC 门槛变低,FULL GC 的STW时间更长。
- 在新生代变大的过程中,由于Minor GC次数变少,吞吐量首先是增长的,之后会下降,这是由于随着新生代变大,单次Minor GC时间也会相应增大。
- 标记与复制,复制占用的时间更长,新生代对象大多数都是用过即死,存活的对象极少,在新生代内存增大后,新生代的复制算法也不会受太多影响。
- 新生代能够容纳所有【并发量*(请求/响应)】的数据。
如果一次请求产生512k的对象,同一时间有1000个并发用户,则要保证新生代要能够存储512M的对象。
6.4.3 幸存区大到能保留(当前活跃对象以及需要晋升的对象)
- 这样能保证用不着的垃圾对象下次就能被回收
- 如果幸存区不够存放,那么对象就会被转移到老年代,回收时间就会增加
6.4.4 晋升阈值配置得当,使长时间存活对象尽快晋升
- -XX:MaxTenuringThreshold=threshold 配置阈值,默认15
- -XX:+PrintTenuringDistribution 打印晋升细节
6.5 老年代调优
以cms为例(浮动垃圾产生又导致内存不足,则并发失败,退化成串行垃圾回收器):
- 老年代内存越大越好
- 先尝试不要做调优,如果没有FULL GC,那么已经满足需求,否则先尝试新生代调优
- 观察发生FULL GC时老年代的内存占用,将老年代的内存提高 -
- -XX:CMSInitialOccupancyFraction = percent 推荐设置为75%-80%
6.6 案例
- 案例一:FULL GC和Minor GC频繁
首先分析可能原因,先做新生代调优,新生代内存过小,对象放不下直接进入老年代,不仅Minor GC频繁,老年代中也会存入大量垃圾对象,FULL GC 也会频繁发生。
所以,适当增大新生代内存大小,使得Eden可以存下新生的多个对象,不会过于频繁触发Minor GC,幸存区存下幸存对象能够使幸存对象在新存区中保存,不会过早进入老年代,老年代内存占用减轻。
- 案例二:请求高峰期发生FULL GC,单次暂停时间过长(CMS)
分析:请求高峰,并发用户很多,产生的新对象很多,堆中对象数目较大。CMS中重新标记会扫描整个堆内存来标记对象,所以会耗用大量时间。
调优:-XX:+CMSScavengeBeforeRemark 设置在 remark重新标记 前对 新生代 进行一次垃圾清理,会大大减少重新标记的时间。
- 案例三 老年代充裕情况下,发生FULL GC(cms jdk1.7)
在jdk1.7及以前,方法区以永久代方式实现,永久代内存不足时,也会发生FULL GC ,增加
jdk1.8及以后,利用元空间实现,使用的是系统内存,内存充裕,很少会触发FULL GC。