JVM学习-垃圾回收调优

1.GC调优预备知识

预备知识
掌握 GC 相关的 VM 参数,会基本的空间调整
掌握相关工具
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

查看虚拟机参数命令

"F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

可以根据参数去查询具体的信息

ConGCThreads=0:CMS回收的并发线程数
GCTimeRatio=99:并行GC,与吞吐量相关的GC时间占领
MaxGCPauseMills:最大的GC停顿时间的目标

2.GC调优领域与目标

调优领域
内存
锁竞争
CPU占用
IO

确定目标:
低延迟还是高吞吐量,选择合适的回收器
CMS,G1
ParallelGC

如果是做科学运算,它们追求的是高吞吐量。对于延长一点点的响应时间,不是太紧要。如果做的是互联网项目,响应时间是非常重要的一环。
对于高吞吐量的垃圾回收器,只有ParallelGC,如果是低响应时间就选择CMS,G1
jdk9中G1取代了CMS,G1在更大的内存下,工作的效果比CMS更好。它继承了CMS与ParallelGC二者之长,特别适合管理超大内存的超大回收,既可以做到低延迟,也可以做到类似Parallel一样,追求吞吐量。

3.最快的GC是不发生GC

如果没有GC的发生,也就会没有stop the world,这样程序的性能也会一直保持快速的响应。如果经常发生GC,首先排除减少因为自身编写的代码而引发的内存问题

查看Full GC前后的内存占用,考虑以下几个问题
数据是不是太多?
加载了不必要的数据到内存,内存的数据太多,导致堆内存的压力过大
比如如下代码:resultSet=statement.query(“select * from 大表”);
写完了再去遍历结果集。如果执行这样一条查询,它就会把所有的数据读入到java内存,这样的话内存再大,它也顶不住这么多语句同时查询,导致堆内存过大。我们可以加上limit n
数据表示是否太臃肿
比如查询用户,不需要把用户所有信息都查询出来。有可能只是用到其中的一部分。
对象图
对象大小
java的最小一个对象都要占用16字节。Integer包装头也要用16字节。能用基本类型,就不用包装类型。
是否存在内存泄漏
static Map map=不断往里面放对象,可能会造成内存溢出。
第三方缓存实现,如redis,会有缓存过期。

4.新生代调优

只有排除了自己代码出现问题以后,再来进行内存调优。
内存调优一般从新生代开始
新生代的特点:

  • 所有的new操作分配内存都是非常廉价的
    .当new一个对象时,这个对象首先会在Eden伊甸园区分配,分配速度非常快。每个线程都会在伊甸园中分配一个私有区域TLAB thread-local allocation buffer,线程局部私有缓冲区。当new一个对象的时候,首先会减产TLAB中有没有可用内存,如果有,会在这块区域进行内存分配。TLAB 它的作用就是让每个线程用自己私有的伊甸园区来进行内存分配,这样多个线程即使同时创建对象,也不会有并发问题带来的干扰。
  • 死亡对象回收零代价
    注意:以前介绍的所有垃圾回收器新生代都是采用复制算法。即把伊甸园和幸存区From都复制到幸存区To中去,复制完了之后,它们的内存都被释放出来了,因此,死亡对象回收代价就是0
  • 大部分对象用过即死(朝生夕死)
    垃圾回收时,大部分对象都会被回收
  • MInor GC 所用时间远小于Full GC
    从而minor GC时间短,所以新生代优化空间更大一些

新生代内存越大越好么?
不是

新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长

Oracle建议新生代占整个堆的1/4以上1/2以上的标准。

在这里插入图片描述
但是总的原则,我们还是要将新生代调的尽可能大,因为新生代垃圾回收用的是复制算法,复制算法分为两个阶段,1:标记,2.复制。复制花的时间更多一点。复制牵扯到对象占有内存的移动以及更新其它对象的内存地址。而新生代大部分对象都是朝生夕死的,只有少量的对象能够存活下来,那么复制所占用的时间较短,而标记时间相对复制时间来讲,就不是那么重要了。

设置多大合适:

新生代内存设置为内容纳[并发量(请求-响应)]的数据为宜*
即一次请求和响应中所产生的对象的大小乘以并发量。
比如一次请求和响应过程中会创建很多新的对象,这些新的对象占512K的内存,并发量大概是1000,即同一时刻有1000个用户来访问,那么新生代比较理想的内存就是每一个请求响应乘以并发量。即512K1000约为512M。因为这一次请求响应的过程以后大部分对象都会被回收,而这一次请求并发量所占用的内存不超过新生代的内存,就较少的触发新生代的垃圾回收。这样就可以大约估算出新生代内存占用大概划分为多少合理。

5.幸存区调优

幸存区需要能够保存 当前活跃对象+需要晋升的对象
幸存区中可以看到两类对象,一类对象生命周期较短,很快就会被回收掉,但是现在还在使用它,暂时不能回收。所以就会留在幸存区出。另一类是将来将会晋升到老年代,但是因为年龄还不够,所以暂存在幸存区中。所以幸存区的大小就需要大到能够把这两类对象都能容纳

如果幸存区比较小,它就会由JVM动态的调整晋升阈值,也许有些对象寿命还不够,但它存活时间并不是很长,由于幸存区内存太少,导致会提前把一些对象晋升到老年代中去。如果存活时间短的对象被晋升到老年代之后,那么它就要等老年代内存不足触发Full GC才能把它当成垃圾进行回收,这就间接的延长了对象的存活时间。我们要保证真正需要长时间存活的对象才把它晋升到老年代

晋升阈值配置得当,让长时间存活的对象尽快晋升
因为长时间存活的对象,一直留在幸存区,只能够耗费幸存区的内存。并且新生代的垃圾回收算法主要耗费时间就在对象复制上,如果有大量的长时间存活的对象不能及时的进程,它们就会被留在幸存区复制来赋值去,这样就会降低性能。所以我们应该设置晋升阈值,将晋升阈值调的比较小。将长时间存活的对象尽快调整到老年区
-XX:MaxTenuringThreshold=threshold
调整最大晋升阈值
-XX:+PrintTenuringDistribution
打印晋升的详细信息,以便判断晋升阈值是否更合适。

Desired survivor size 48286924 bytes, new threshold 10 (max 10) 
- age 1: 28992024 bytes, 28992024 total 
- - age 2: 1366864 bytes, 30358888 total 
- - age 3: 1425912 bytes, 31784800 total ...

通过打印幸存区中不同年龄的对象打印,可以更细致的决定的把最大阈值调整到多少合适。

5.老年代调优

以 CMS 为例
CMS 的老年代内存越大越好

CMS垃圾回收器,是一种低响应时间的,并且是一个并发的垃圾回收器,即垃圾回收线程在工作的同时,其他的用户线程也能够并发的执行,但是这样有一个缺点,因为垃圾回收的同时,其他的用户线程也在运行,那么又会产生新的垃圾,如果浮动垃圾产生又导致内存不足,就会导致CMS并发失败,此时CMS垃圾回收器就不能正常工作了,因此它就会退化SerialOld串行垃圾回收器,然后就Stop the world.响应时间就会变得特别长。所以规划内存的时候就尽量大一点,这样就可以预留更多的空间,避免浮动垃圾引起的并发失败。
先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
如果没有引起Full GC,那么说明老年代的空间比较充裕,这种没有Full GC,说明系统工作的很OK了。即便发生了FullGC,也从新生代开始调试。如果还是经常发生Full GC,那么就开始老年代调优
观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
控制老年代内存被占用多少了之后,使用CMS进行垃圾回收。这个值越低,老年代垃圾回收触发的时机就越早。

猜你喜欢

转载自blog.csdn.net/qq_39736597/article/details/113540346