JVM:(一次线上调优记录)新生代老年代对象的传递以及调优思路

目录

JVM调优的目的

JVM组成

jvm 垃圾回收机制

垃圾回收中重要的方法

JVM中垃圾回收算法

分代收集器详述

堆的GC方式

对象移入老年代条件

JVM调优

一次JVM调优实践


JVM调优的目的

减少GC 特别是损耗大的Full GC 减少出现STW的时间。stop-the-world

调优部分是:堆、方法区

JVM组成

类加载器 + 运行时内存区 + 执行器

1.8使用的是ParallelGC垃圾收集器 关注系统的吞吐量

jvm 垃圾回收机制

垃圾回收原则:

GC通过判断对象是否被活动对象引用来进行回收。

触发条件:

1、一般情况下 GC运行在最低优先级的线程中,当没有线程在工作是进行GC

2、当线程在运行 创建新对象的时候发现堆内存不足了,这个时候会强制GC 如果仍然不足,会继续触发两次GC 若依旧内存不足,会出现OOM

垃圾回收中重要的方法

1、System.gc() 手动触发GC垃圾回收 Full GC

2、finalize() 释放一些java无法释放的内存空间

JVM中垃圾回收算法

1、标记清除收集算法---标记了垃圾 然后回收 会产生一些内存碎片 存储大对象的时候会有问题

2、复制收集算法 --- 将内存分为两块 垃圾全部移动到另一块 内存利用率低

3、标记-压缩收集算法 ---- 标记了垃圾 然后将垃圾压缩到一起 然后一起清除

分代收集 (目前最常用) 【根据不同的内存区域,使用不同的算法】

分代收集器详述

分代收集器总体分为,老年代(每次GC都能有较多对象存活)、新生代(每次GC有大量对象被清除)。不同的代使用不同的算法。

新生代(堆内存的1/3)使用复制算法,但是内存区域划分并不是以1:1规格进行划分的。新生代分为较大的Eden(新生代内存的8/10)空间和较小的From Survivor(1/10)To Survivor(1/10)空间。垃圾回收时将Eden区和From Survivor区的存活对象移入To Survivor中去,然后清理Eden和 From Survivor。最后From与To身份互换

老年代(堆内存的2/3)中因为每次回收都只会回收较少的对象,因此采用了标记压缩算法。

堆的GC方式

1、minor GC条件

  • Eden区满了 或者 线程空闲 就会触发Minor GC

执行minor GC 前判断老年代中连续空间大小是否大于新生代所有对象的大小。大于的话执行minor GC 否则直接进行Full GC

执行Minor GC时 在From Survivor区的对象根据年龄去不去老年代 可以通过参数配置阈值年龄 -XX:MaxTenuringThreshold 默认15岁。没有达到阈值的会被复制到To Survivor中。

当Eden 和 From 区经过GC仍存活 并且 没有达到去老年代的阈值。大量的对象一下给To区充满。会将多出的对象移入老年代空间分配担保机制

2、Full GC

  • 老年代空间:创建大对象,Eden装不下了 minor gc 后还放不下,会直接放入 老年代,老年代也装不下了,会进行Full GC

如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。

  • 持久代空间不足

如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC

  • Minor GC前 会判断晋升到老年代对象是否大于老年代空闲空间

  • Minor GC后存入老年代的对象过多 老年代空间不足

  • 显示的调用 System.gc()

对象移入老年代条件

  • 根据迭代年龄进行判断

对象的迭代年龄存储在对象的对象头里。在每次minor gc后判断年龄是否超过15,超过15(可通过参数配置CMS收集器默认年龄是6)的直接移入老年代,没有的进To区

  • 大对象直接进入老年代

JVM通过一个参数(- XX:PretenureSizeThreshold)进行配置对象大小,超过这个大小的对象直接被移入老年代。

  • 对象动态年龄判断

如果在 Survivor 空间中所有相同年龄的对象,大小总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就直接进入老年代,无须等到阈值中要求的年龄。

  • 空间分配担保机制

如果老年代中最大可用的连续空间大于新生代所有对象的总空间,那么 Minor GC 是安全的。如果老年代中最大可用的连续空间大于历代晋升到老年代的对象的平均大小,就进行一次有风险的 Minor GC,如果小于平均值,就进行 Full GC 来让老年代腾出更多的空间。   因为新生代使用的是复制算法,为了内存利用率,只使用其中一个 Survivor 空间来做轮换备份,因此如果大量对象在 Minor GC 后仍然存活,导致 Survivor 空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,但老年代要进行担保的前提是自己本身还有容纳这些对象的剩余空间,由于无法提前知道会有多少对象存活下来,所以取之前每次晋升到老年代的对象的平均大小作为经验值,与老年代的剩余空间做比较。

JVM调优

-Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;

-Xms Java Heap初始值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;

-Xmn Java Heap Young区大小,不熟悉最好保留默认值;

-Xss 每个线程的Stack大小,不熟悉最好保留默认值;

一次JVM调优实践

其实这件事已经发生很久了,因为最近想开始总结,所以想把这个过程记录下来,也算自己的总结。

在上半年四月份左右,经历了项目交接,我们在维护新项目的过程中,发现一个问题,有一个系统的线上机器在上线后一周左右,接口变得很慢,TP99等各种性能指标飙升。这是一个很典型的问题,应该很多同学也遇到过,最直接的解决办法就是将服务器重启,这个我们也试过,但是治标不治本。经过我们的观察发现系统上线一段时间以后就会进行频繁的Major GC。

当时项目是使用的jdk 1.7,JVM参数只是指定了堆的大小,没有GC日志输出。我们知道1.7当中默认的垃圾收集器是Parallel Scavenge,这是一个并行垃圾收集器,对年轻代和年老代都适用,而且关注系统的吞吐量,我们可以通过-XX:MaxGCPauseMillis和-XX:GCTimeRatio这两个参数来指定垃圾回收的时间,-XX:GCTimeRatio该参数的默认值为99,即垃圾回收时间只能占系统总运行时间的1%,而且这个时间还是年轻代和年老代回收的总时间。另外还有一个很关键的参数,也是导致这次问题发生的关键:-XX:ParallelGCThreads,这个参数是指定进行垃圾回收的线程数,如果不设置,JVM会根据服务器的情况去设置,若当前服务器核数小于8,则将这个值设置为CPU核数,若大于8,计算公式为:8+(CPU核数-8)*(5/8)。而因为现在的docker容器技术,每台服务器的CPU数量都很大,JVM不是取当前容器申请的核数,而是使用物理服务器的总核数,而我们那台服务器的核数是64,这样计算出来的ParallelGCThreads将达到43个。也就是在进行GC的时候将同时有43个线程共同完成,我们知道在Parallel 中GC时间是固定的,而43个GC线程进行线程间的切换就要花费多少时间,真正用在GC上的时间少的可怜。这也就导致了虽然一直在进行Major GC,但是垃圾却始终回收不掉的情况,导致线上机器的内存使用率一直下不来。

思考一下,指定GC回收时间之后,随着访问量的不断增加,时间和GC线程数一定的情况下,JVM所能回收的垃圾是一定的,这样就必定导致垃圾的堆积,到最终就是频繁的Major GC,甚至是Full GC。那为什么jdk1.7以及jdk1.8的默认收集器都是Parallel呢,我觉得对于业务频繁更新的系统,只要设置好参数,使用Parallel是没有问题的,因为频繁的业务更新,必然会频繁上线使得机器重启,这样就可以避免垃圾的堆积,毕竟垃圾的堆积也是需要时间的,例如我们当时的系统是上线一周之后才出现,而且还是参数设置的极不合理的情况下,Parallel还是能用的,当然最终我们没有再使用Parallel,而是使用了ParNew结合CMS的组合,因为我们更关注的是低停顿而不是高吞吐。

我们的服务器一般都是常规的4C8G的机器,给JVM分配了6G的内存,其中年轻代分配了2G,Eden和Survivor默认比例8:1:1,这样换算下来Eden区1.6G,一个Survivor区为0.2G,这样设置的目的也是尽可能让更多的对象在年轻代就被回收。每次进行Minor GC需要回收的空间可能就有1.8G,为了加快回收速度,我们配置了ParallelGCThreads=4,这样年轻代的回收速度也得到了提升,目前线上Minor GC的停顿平均时间稳定在30ms,如下图所示。

年老代空间大概为4G左右,通过 -XX:CMSInitiatingOccupancyFraction=75以及 -XX:ConcGCThreads=2参数设置触发CMS回收的时机和回收时的线程数,这样的参数设置使得线上机器基本没有发生Major GC和Full GC。

以上提到的部分参数只是针对内存比较小的服务器,对于大内存的服务器,比如32G内存的服务器,可能新生代就会分配10G以上,这种情况下仍然 使用ParNew和CMS的组合效果就很差了,这个时候就需要使用G1,G1设计的初衷就是为用户提供大内存、低GC停顿时间的应用解决方案,它将整个内存区域分成大小相等的若干块,每个块会指定是新生代还是老年代或者大对象区,通过复制算法来进行垃圾回收。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值