背 景
近期多个java类系统生产上出现内存逐步增长的问题,在分析过程中发现有些内存增长是由于发生内存泄露问题,有些增长属于正常情况。为了能够有效识别出是否为真正内存问题,本文将从java进程运行时数据区、以及java内存分配及垃圾收集等几方面进行简要介绍,而后根据笔者经验给出java内存问题分析指导,期望能为相关系统提供调优思路。(本文主要基于JDK1.8相关环境进行)

java进程运行时数据区✦
1、程序计数器:用于存储当前线程执行的字节码指令地址,这个区域是每个线程独有的,不共享。
2、栈(Stacks):每个线程创建时都会创建一个虚拟机栈,每个方法调用都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接地址等信息,是线程运行时的数据结构,每个线程独有,不共享。
3、Java本地方法栈(Native Method Stacks):用于存储JNI调用的相关信息,每个线程独有,不共享。
4、堆(Heap):用于存储对象实例、数组、字符串等,是java进程占用的内存中最大的一块内存区域,下一节将详细介绍其内部组成,往往发生内存泄露也是在堆内存中进行分配的,是所有线程共享的区域。
5、方法区(Method Area):用于存储类的元数据、常量池、静态变量以及编译器编译后的代码等信息,是可共享的区域。
6、直接内存(Direct Memory):非堆空间,一般用于存储大对象,减少数据复制开销及对象回收,使用的类有ByteBuffer、MappedByteBuffer等。
内存组成如下所示


java对象内存分配及垃圾收集✦
Java虚拟机启动后,操作系统会根据jvm设置的堆大小来分配初始空间,比如java -Xms1g -Xmx2g MyApp,设置堆最小为1g,最大值为2g。
当线程创建时,如上节所说,JVM会为每个线程在栈区上分配一个栈内存,栈内存的大小可以通过-Xss参数来设置,比如java -Xss500k MyApp,不设置默认为1M。当方法被调用时,JVM会在栈内存中为该方法分配一个栈帧,用于存储方法的局部变量和操作数栈。当方法执行完毕后,栈帧会被弹出,释放栈内存。
在方法执行过程中使用到的对象是从堆中引用,如果在执行过程中产生需要新创建对象,分析如果是会发生逃逸的对象(对象逃逸是指在对象创建后,其引用被传递到其他方法或线程中,导致该对象在方法或线程外部可见)则会在堆中创建,如果对象大小可控并且只在方法内部使用,则可以直接放在栈区中分配内存。在堆中为对象分配内存还需要判断对象是否能在新生代分配,如果新生代没有空间分配则会进行一系列的判断,有可能直接进入老年代,也有可能发生垃圾收集。如下图所示:

JVM主要使用分代垃圾收集器,其算法是基于两个假设来的,即大多数分配对象的存活时间很短,存活时间久的对象很少引用存活时间短的对象,基于此JVM将堆主要分成新生代和老年代,这就是所谓的分代。垃圾回收的触发条件跟上述内存分配紧密相关,一般来说MinorGC主要是发生在新生代空间不足时,FullGC发生在老年代空间不足的时候。在MinorGC进行之前会计算老年代剩余可用空间,如果可用空间足够容纳新生代所有对象,则直接进行MinorGC;否则再判断如果老年代可用空间足够容纳历史每次MinorGC后进入老年代对象的平均值,则也可进行MinorGC;否则会触发老年代的回收,然后再触发对新生代的回收,如果回收完还没有足够的空间存放新对象则会发生OOM。

目前jdk所提供的各种垃圾回收器基本都是基于新生代老年代而来,现在大家使用较多的G1回收器虽然是将堆分成很多小块统一管理,但是也是有新生代及老年代区域,只不过根据垃圾收集的情况,会动态调整新生代及老年代的区块大小,优先回收垃圾对象最多的区域,所以大部分的时候G1只收集新生代区即进行MinorGC,如果发生老年代空间不足的情况下,会发生混合回收MixedGC,所以在使用G1垃圾回收器的时候很少发生FullGC的情况。如果想开启G1回收器可以在jvm启动参数中使用 -XX:+UseG1GC。

常用监控命令及使用✦
在监测java内存问题中,笔者经常使用到的命令有 如下几个:
1、top命令:top命令可以在Linux系统中显示当前系统中各个进程的CPU、内存、I/O等资源消耗状况。其中,“RES”列表示进程使用的物理内存大小,这个值真实反应进程所占用内存,输入M会按照内存大小排序。如下图所示为容器中的top结果,可以看到1号进程占用的内存最多,res常驻内存为3g。

2、jstat命令:jstat命令可以用来统计Java应用程序的各种性能指标,jstat -gcutil 是非常常用的监控垃圾回收情况的命令。jstat -gcutil可以看出新生代,老年代的占用变化,以及新生代,老年代发生了多少次垃圾收集等情况,用于为内存分析提供数据支持。如下图所示,E和O列分别代表新生代中的Eden区和老年代Old区,YGC代表当前发生过的MinorGC次数,FGC代表当前发生过的FullGC的次数。

3、vmstat命令:vmstat命令可以用来显示系统整体性能指标,vmstat 可以每隔interval参数指定的时间输出内存指标,输出的指标中内存指标可以主要查看swap中的si和so数值是否有大的变化来判断有内存页的交换,发生内存页频繁交换可能说明物理内存逐渐耗尽,系统开始试用虚拟内存swap区。
如下图所示,分为memory,swap,io,system,cpu等几大专项,下面一行中的si和so就是对应的虚拟内存页交换情况。

4、还有其他一些java提供的性能分析工具,包括java提供的VisualVM,jmc等可视化的监控分析工具,大家可参考相关使用手册。同时通过在jvm参数中增加相关的gc日志的详细细节也可以得到有用的一些信息,比如在启动参数中使用-XX:+PrintTenuringDistribution 可以打印出显示新生代survior中对象年龄段晋升分布,当超过晋升阈值时就会被提升到老年代。如下图所示,表示在survior中的对象年龄分布包含了有4、5两种年龄的对象所占有的对象空间,new threshold为6 就是根据垃圾回收动态的判断,对象超过6就要晋升到老年代,晋升年代的降低可以尽量减少在新生代不断的复制对象,不过也会加快老年代内存的增长。


java进程内存问题分析流程✦
对java内存的基础知识以及相关监控手段学习后,我们就可以运用在非功能测试或者生产上的java内存问题的分析,包括内存占用率过高,内存缓慢增长等。(本文主要针对堆内内存调优流程进行分析,后续会通过新的文章对堆外内存的排查进行总结。)
笔者认为内存的分析关键点主要是:*是否频繁fullgc、gc时长、fullgc后的内存是否合适、是否存在生产速度远大于消费速度的情况、是否有频繁创建对象不释放的情况*等。
解决问题的思路主要还是*检查生产消费的匹配度、对象生命周期的控制、提高线程的处理能力减少对象的驻留时间*。各系统应该根据排查出的内存增长对象寻找其使用生命周期,分析总体的应用上下文,进行相应的优化,优化手段根据其问题原因进行不同的调整,调优的解决思路主要是按照以上提的三种思路进行。具体分析过程可参考笔者总结流程图:

上述流程中,当发现内存占用异常时,需要查看垃圾收集情况,垃圾收集具体情况可以通过打印gc日志来直接通过文件,也可以通过用jstat -gcutil 命令直观动态来查看其情况。首先判断是否存在fullgc,如果存在则检查下java堆最大值的设置是否远大于fullgc后的内存大小,如果不是,则说明堆的设置可能过小,将其调整为fullgc之后至少4倍以上的堆空间后再进行测试。如果堆大小设置满足,则检查是否有大对象创建或频繁创建对象的情况,一般这种情况下需要使用jmap -dump:format=b,file=命令间隔打印多个堆快照,使用MAT等内存分析工具进行比对分析是否有内存泄露或常驻内存对象的情况。如果不存在fullgc的情况,则需要查看是否存在gc时长异常的情况,如果有,则查看是否由于系统态与用户态时间的对比检查是否存在页交换等情况,如果没有则排查是否有堆外内存异常情况。
当然不是所有的内存都在增长都是java内存泄露引起的。如果经过排查及解决内存不出现频繁fullgc,但是测试结果中TPS仍然出现锯齿形的图形,那么可以检测下是否测试机请求有问题。通过netstat -ant |grep 测试机ip,查看是否有端口已经占满不可用的情况。
如果通过上述的内存分析流程进行排查后发现应用层并没有存在内存泄露的情况,那就需要根据上述分析指导中的提示,看jvm的大小设置、垃圾回收器的选择等是否需要调整。有时候jvm堆设置占用物理内存较大的情况下,容易出现超过物理内存80%的一些限制,因为java进程除了堆的占用外,还有上述其他运行时数据区要占用空间,比如线程栈默认一个线程占用1M的物理内存,还有元数据空间等,所以在计算堆内存的设置时需要综合场景进行考虑。
特别提醒下在容器环境中目前监控默认使用的是 Working-set使用率=(rss+total_mapped_file+active_file)/limit,通过试验验证,频繁读取文件的容器(比如处理批量业务),active_file部分会升高,从而working-set使用率会持续上涨,并可能超过80%,使用量接近limit值时会触发自动回收,但不会导致pod被kill重启,这种情况下可以申请将监控指标 rss+mapper=(rss+total_mapped_file)/limit 来替换,如果还出现增长大概率需要从应用层进行排查了。

掌握内存分析方法的意义✦
在目前大量应用以java语言进行构建的背景下,虽然有很多工具可以抓取大量的数据,或者提供一些可能性的建议,但是没有一套通用的方法论,能够让开发人员在短时间内掌握内存问题分析的路径,本文所提出的抓住内存分析关键点提供三种解题思路,让普通开发人员也能快速解决内存问题,具有如下意义:
• 提升性能:内存分析能够帮助开发项目组识别并解决性能瓶颈,从而提升应用的响应速度和处理能力。
• 降低成本:通过优化内存使用,减少对硬件资源的依赖,降低运维成本。
• 增强稳定性:及时发现和解决内存问题,减少系统崩溃和重启的风险,提高服务的可靠性,保障系统安全运营能力。
• 知识共享:本文将复杂的内存分析方法,用简单的分析路径法将内存分析难度降低,可以普及至普通的开发人员,从而提升开发人员的分析能力,助力问题快速解决。
结 语
java内存问题分析过程是一个系统性工程,需要结合实际应用场景、应用代码、jvm设置、操作系统各项指标一同判定,本文为java内存问题分析提供一个思路,期望能为系统排查内存问题提高一些效率,如果在非功能或生产环境中遇到内存、吞吐量等java性能问题,也欢迎大家与笔者进行交流讨论,后续笔者将会按需为需要的项目组开展相关的培训活动。
1054

被折叠的 条评论
为什么被折叠?



