深入理解JVM - 实战老年代优化

简介: 通过前面的文章可以了解到JVM优化中老年代的FULL GC对于系统以及垃圾收集器的行为有着十分大的影响,比如CMS并发标记或者回收撑不住的时候要暂停用户线程并且呼叫serrial收集器帮忙进行单线程的高效回收的动作,但是也伴随着"漫长"的stop world时间。

image.png


前言


通过前面的文章可以了解到JVM优化中老年代的FULL GC对于系统以及垃圾收集器的行为有着十分大的影响,比如CMS并发标记或者回收撑不住的时候要暂停用户线程并且呼叫serrial收集器帮忙进行单线程的高效回收的动作,但是也伴随着"漫长"的stop world时间。


综上所述,老年代的优化是JVM优化的一个核心知识点,所以这一节就来讲解如何优化老年代的回收,尽量让对象在新生代回收而不是在老年代进行回收。


前文回顾


之前的文章我们对于JVM分代以及垃圾回收有了一个详细的了解,同时了解了jdk9之前主流的垃圾收集器ParNew收集器和CMS收集器,并且对于cms的细节进行了完整的讲述,这些内容都是十分基础但是又十分重要的知识点,这篇文章则根据前面的知识点,根据一个模拟的案例来讲解一下如何对于JVM老年代进行调优。


案例实战


案例实战会根据一个非常理想化的模拟场景,因为应用的性能实际上会有各种的影响,甚至代码的质量也会影响系统的运行性能,所以下面的模拟场景均为假想的情况,切勿过于认真的对待这个案例的各种参数。


一个电商系统的大致背景:


如果一个电商网站每天的访问量是20次/人,如果要上亿次的请求需要每天500万次的请求,同时如果这500万人按照10%的下单的标准,则是每天50万人会进行下单的操作,而下单操作按照2/8原则在4小时之内付款完成,那么此时的占用大概是50万/4小时 == 500000 / 14400,大概每秒也就 34个订单左右,这种情况下发现系统的影响并不会很大,老年代发生回收大概为几个小时一次,完全可以接受。


高并发的场景


但是如果在秒杀的场景,情况又不一样了,如果在一秒内来1000笔订单,该如何处理?我们假设如果是3台机器,则每台需要处理至少300条请求。


计算JVM消耗


根据上文模拟场景,假设每秒300个请求按照每个对象1KB来看,每一台机器要处理大概300KB的内存,把一个订单系统的处理对象放大10倍,则是3000KB,如果在算上其他的操作比如订单处理,则需要30000KB = 30MB的占用。


如果虚拟机栈每个占用1M,则几百个线程需要几百M的空间。如果是4核心8G的机器,则分4G给JVM,4G中分1G给虚拟机栈500M多M,方法区:256M,堆外内存给256M。同时开启内存担保机制(jdk6之后不需要制定参数)然后新生代和老年代各分配1.5G。


按照上面的换算,我们发现如果每秒都来30M对象,那么1200M左右的EDEN区域(8:1:1的比例大概是1200给EDEN),大概会留下200M左右的内存会进入SURVIOR区域,但是如果SURVIOR区域放不下则会进入老年代,根据之前的参数分配150M的Survior区域肯定是无法存放的,根据内存分配担保的机制,这些对象会分配到老年代。


按照这样的分配效率不到一分钟新生代就会塞满。大概8、9次minor gc就会导致full gc,也就是说 8、9分钟就会触发老年代回收,这个触发的概率就十分高了,这会严重导致系统卡顿并且出现用户线程的停顿现象。


但是如果Survior空间足够,那么此时回收进入到Survior空间之后,在下一次minor gc基本也为垃圾对象被回收了。


垃圾回收的优化


上面的案例优化也非常简单,在讲解最终的优化方案之前,我们按照下面的步骤进行分析:


检查Survior区域是否可以保证每次minor gc都可以全部进入


首先我们需要确认新生代的内存在垃圾回收之后是否都可以进入到Survior区域,很明显,根据案例Survior区域的大小为150M左右,而每次Minor GC之后存活对象通常为200M左右,这时候明显是无法存放下的,所以我们需要。


多大的对象应该进入老年代


之前说过一秒钟会产生30M对象,而Minor gc之后对象基本只剩下100M左右了,也就是说1分钟大概存活100M对象,那么平摊下来一秒大概产生1-2M的大对象。


所以一般情况下设置个1M的阈值就差不多了


对象经过多少年龄进入老年代


一般情况下默认的15就是一个不错的值。但是对于高并发的业务来说,大对象早点进入老年代反而是好事。因为survior存在一个控制值50%,累加对象大小超过Survior区域50%之后大于等于此年龄全部会进入老年代,所以有时候让新生代的对象提前进入老年代也是一种值得考虑的事情。


比如我们可以将进入老年代年龄的对象设置为7或者8。


指定垃圾收集器


注意一定要在参数里面指定垃圾收集器,这是十分重要的内容。


比如:-XX:+UseParNewGC

最终优化参数结果


经过上面的一系列分析,我们可以确定根本问题出在了对象提前进入了老年代导致Survior区域成为摆设并且老年代的对象不断扩展,最终老年代塞满而导致频繁full gc,所以案例最后的优化参数如下:


image.png


老年代的内存要如何优化呢?


针对上面的案例,我们再分析几点内容:


老年代需要开启分配担保失败么?


我们看下如果没有开启分配担保失败会如何?首先如果没有开启,如果此时老年代的可用内存为400M,并且发现新生代总大小 < 老年代可用内存大小,每次Minor Gc都将会伴随着Full Gc,所以jdk6也关闭了这个参数并且这个参数也是默认开启的,绝大多数的情况下这个参数不用去管,默认都是要开启的。


如果老年代的总大小<新生代的大小,那么如果没有开启分配担保每一次请求都是会发生Full Gc的。


如果使用CMS收集器,是否需要改动92%的参数


从案例的角度来看,优化之后如果老年代到达900M说明此时订单系统已经运行很久了(几个小时),一般情况下秒杀早就结束了,此时进行Full GC也不会影响业务的处理和请求的处理。


进入老年代触发full Gc对系统影响可能性


如果某一次gc之后新生代存活对象大于200M,发现 Survior 区域放不下,此时老年代判断历次晋升平均大小,发现基本都是可以分配的,因为调整过后一般情况下只有几十M大小的对象进入,所以这样的概率还是比较小的,即使出现这种情况,此时系统也过去了很久了,高峰下单一般在前10分钟,如果前10分钟没有进行Fu'll Gc 而1小时之后进行了,这样也是不影响订单系统的运行的,因为此时的压力很小了.


Concurrent Mode fail 这种情况的有无影响


和前面说的分析一样,如果900M的老年代空间已经被占用满了,此时系统进程和垃圾回收线程同时进行,如果在并发整理的时候进入200M的对象,那么最坏的情况是触发失败导致stop world 并且serial 进行单线程的回收处理动作,但是需要考虑的是这种情况假设已经过去1个小时了,而此时的订单压力也小很多,Full Gc的影响也是可以承受的,在进行完这次Full GC之后,下一次可能就在几个小时之后的,这种情况虽然有可能发生,但是几率十分小。


内存碎片整理的概率有多大


还是和上面的情况一样,如果出现FULL GC也说明此时系统运行比较久了,一次FULL GC的间隔十分长的情况下每次FULL GC进行内存碎片整理的代价是可以接受的。


总结


下面根据这个案例总结一下如何思考优化的点


  1. 首先业务的对象都是生命周期十分短暂的对象,新生代的压力比老年代要大,所以适当缩小老年代空间是十分划算的
  2. 预测在高并发的场景下对象进入老年代的时机,如果对象经常“跨区”说明有一部分内容空间是浪费了,那就是Survior区域
  3. 对象在各分区需要大致多少的内存空间,比如每个线程需要占用多少的内存空间
  4. 对象的年龄判断是否需要改动,提前让对象进入老年代是好处还是坏处
  5. 关注收集器对于对象垃圾回收的影响,同时在启动的时候要强制使用某一垃圾收集器,因为不同的JDK版本默认的垃圾收集器是不一样的。
相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
42 0
|
23天前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
51 10
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
2月前
|
监控 算法 Java
Java虚拟机垃圾回收机制深度剖析与优化策略####
【10月更文挑战第21天】 本文旨在深入探讨Java虚拟机(JVM)中的垃圾回收机制,揭示其工作原理、常见算法及参数调优技巧。通过案例分析,展示如何根据应用特性调整GC策略,以提升Java应用的性能和稳定性,为开发者提供实战中的优化指南。 ####
47 5
|
2月前
|
监控 Java 开发者
Java虚拟机(JVM)深度优化指南####
本文深入探讨了Java虚拟机(JVM)的工作原理及其性能优化策略,旨在帮助开发者通过理解JVM的内部机制来提升Java应用的运行效率。不同于传统的技术教程,本文采用案例分析与实战技巧相结合的方式,为读者揭示JVM调优的艺术。 ####
58 8
|
3月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
111 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 IDE Java
实战优化公司线上系统JVM:从基础到高级
【11月更文挑战第28天】Java虚拟机(JVM)是Java语言的核心组件,它使得Java程序能够实现“一次编写,到处运行”的跨平台特性。在现代应用程序中,JVM的性能和稳定性直接影响到系统的整体表现。本文将深入探讨JVM的基础知识、基本特点、定义、发展历史、主要概念、调试工具、内存管理、垃圾回收、性能调优等方面,并提供一个实际的问题demo,使用IntelliJ IDEA工具进行调试演示。
45 0
|
3月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
3月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
68 2
|
3月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
88 3