美文网首页
JVM发起GC后为何物理内存无明显变化

JVM发起GC后为何物理内存无明显变化

作者: X_3321 | 来源:发表于2020-12-23 18:27 被阅读0次

问题现象

Java进程内存缓慢升高,到达一定值后居高不下:见 RES列

image-20201222110650976

RES: A task's currently used share of available physical memory.

任务当前使用的可用物理内存份额

复现过程

2020/09/30 16:00 运维发现该服务内存占用比较高约2.7G

2020/09/30 16:10 运维重启该服务

2020/09/30 16:40 运维发现该服务内存又升高到1.9G

首次排查历程

启动参数

/usr/local/jdk/bin/java -Djava.util.logging.config.file=/apps/myapp/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -server -Xms4g -Xmx4g -Xss1024K -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:ParallelGCThreads=10 -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/logs/myapp/myapp-gc.log -Djava.endorsed.dirs=/apps/myapp/endorsed -classpath /apps/myapp/bin/bootstrap.jar:/apps/myapp/bin/tomcat-juli.jar -Dcatalina.base=/apps/myapp -Dcatalina.home=/apps/myapp -Djava.io.tmpdir=/apps/myapp/temp org.apache.catalina.startup.Bootstrap start

通过启动参数可知:

该应用最大堆内存配置为:4G

怀疑:存在内存泄露,排查堆内存占用情况

验证过程

第一步:dump堆内存快照信息

jmap -dump:format=b,file=heap.bin <PID>

第二步:mat分析内存

结果:mat分析得知,此时应用堆内存仅几百MB,距离1.9G差距很大。暂时解除是堆内存泄露的怀疑。

怀疑:存在堆外内存泄露,排查堆外内存占用情况

由于中间有别的事情,就没有立即对该怀疑进行验证

第二次排查历程

启动参数中的gc日志引起了我的注意:

-Xloggc:/logs/myapp/myapp-gc.log

我决定先分析一下内存回收的情况


cat  /logs/myapp/myapp-gc.log

...

2020-12-22T10:50:59.014+0800: 2813573.254: [GC pause (G1 Evacuation Pause) (young), 0.1792911 secs]

  [Parallel Time: 7.3 ms, GC Workers: 10]

      [GC Worker Start (ms): Min: 2813573253.9, Avg: 2813573255.6, Max: 2813573258.6, Diff: 4.7]

      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.6, Max: 2.1, Diff: 2.1, Sum: 6.1]

      [Update RS (ms): Min: 0.0, Avg: 1.0, Max: 3.1, Diff: 3.1, Sum: 10.4]

        [Processed Buffers: Min: 0, Avg: 7.9, Max: 33, Diff: 33, Sum: 79]

      [Scan RS (ms): Min: 0.1, Avg: 0.2, Max: 0.4, Diff: 0.3, Sum: 1.9]

      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

      [Object Copy (ms): Min: 0.0, Avg: 0.9, Max: 3.1, Diff: 3.1, Sum: 8.7]

      [Termination (ms): Min: 0.0, Avg: 1.5, Max: 2.3, Diff: 2.3, Sum: 14.8]

        [Termination Attempts: Min: 1, Avg: 5.4, Max: 17, Diff: 16, Sum: 54]

      [GC Worker Other (ms): Min: 0.0, Avg: 1.2, Max: 4.6, Diff: 4.5, Sum: 11.6]

      [GC Worker Total (ms): Min: 2.3, Avg: 5.4, Max: 7.1, Diff: 4.7, Sum: 53.6]

      [GC Worker End (ms): Min: 2813573260.9, Avg: 2813573261.0, Max: 2813573261.0, Diff: 0.1]

  [Code Root Fixup: 0.1 ms]

  [Code Root Purge: 0.0 ms]

  [Clear CT: 0.7 ms]

  [Other: 171.2 ms]

      [Choose CSet: 0.0 ms]

      [Ref Proc: 169.0 ms]

      [Ref Enq: 0.0 ms]

      [Redirty Cards: 0.4 ms]

      [Humongous Register: 0.1 ms]

      [Humongous Reclaim: 0.0 ms]

      [Free CSet: 1.2 ms]

  [Eden: 2452.0M(2452.0M)->0.0B(2448.0M) Survivors: 4096.0K->8192.0K Heap: 2572.1M(4096.0M)->123.7M(4096.0M)]

[Times: user=0.03 sys=0.00, real=0.18 secs]

...

日志中有一行引起了我的注意:

[Eden: 2452.0M(2452.0M)->0.0B(2448.0M) Survivors: 4096.0K->8192.0K Heap: 2572.1M(4096.0M)->123.7M(4096.0M)]

[Times: user=0.03 sys=0.00, real=0.18 secs]

Eden区内存回收从 2452M -> 0.0B 与最高内存2.9G这个数值比较相近

我决定再分析一下堆内存的分配情况:


jmap -heap 32308

Attaching to process ID 32308, please wait...

Debugger attached successfully.

Server compiler detected.

JVM version is 25.65-b01

using thread-local object allocation.

Garbage-First (G1) GC with 10 thread(s)

Heap Configuration:

  MinHeapFreeRatio        = 40

  MaxHeapFreeRatio        = 70

  MaxHeapSize              = 4294967296 (4096.0MB)

  NewSize                  = 1363144 (1.2999954223632812MB)

  MaxNewSize              = 2575302656 (2456.0MB)

  OldSize                  = 5452592 (5.1999969482421875MB)

  NewRatio                = 2

  SurvivorRatio            = 8

  MetaspaceSize            = 21807104 (20.796875MB)

  CompressedClassSpaceSize = 1073741824 (1024.0MB)

  MaxMetaspaceSize        = 17592186044415 MB

  G1HeapRegionSize        = 2097152 (2.0MB)

Heap Usage:

G1 Heap:

  regions  = 2048

  capacity = 4294967296 (4096.0MB)

  used    = 2111526336 (2013.7084350585938MB)

  free    = 2183440960 (2082.2915649414062MB)

  49.1628035902977% used

G1 Young Generation:

Eden Space:

  regions  = 945

  capacity = 2696937472 (2572.0MB)

  used    = 1981808640 (1890.0MB)

  free    = 715128832 (682.0MB)

  73.48367029548989% used

Survivor Space:

  regions  = 4

  capacity = 8388608 (8.0MB)

  used    = 8388608 (8.0MB)

  free    = 0 (0.0MB)

  100.0% used

G1 Old Generation:

  regions  = 64

  capacity = 1589641216 (1516.0MB)

  used    = 121329088 (115.70843505859375MB)

  free    = 1468312128 (1400.2915649414062MB)

  7.632482523653941% used

33825 interned Strings occupying 3865872 bytes.

从中可以读取到的重要信息是:

新生代Eden区:分配内存2572MB、当前使用了1890MB

新生代Survivor区:分配内存8MB、当前使用了8MB

老年代:分配内存1516MB、当前使用了115MB

从中可大致分析出一个场景:

当Eden区内存使用率到100%,触发新生代GC。然后将存活的对象转移到 Survivor区。Survivor区进行多轮标记,如果对象还存活则转移对象到 Old Generation区。

从这里我们可以知道由于Eden分配的内存是2572MB,所以在新生代GC前JVM的内存占用是有可能大于 2572MB的。

通过上面的分析:得出了JVM内存占用是有可能达到 2.5GB及以上的可能性的结论。

新的疑问,但为何应用重启后,进程的RES列缓慢升高,之后就是久高不下呢,通过GC日志可以知道JVM的确也回收了内存。如果按这个逻辑来说,RES列应该随着GC而同步波动才对啊。

怀疑:JVM回收内存后并没有将内存归还给操作系统

为了验证我的怀疑,我开始查阅相关资料。

最终大概得出了一个结论:

在Linux操作系统下,会为每个进程分配一个虚拟内存(32位操作系统为4G)。通过页映射的方式让虚拟内存与物理内存(及硬盘的Swap)建立映射。

当进程真正需要使用内存的时候会申请虚拟内存,此时操作系统会将物理内存与进程的进程的虚拟内存建立绑定。代表该物理内存被进程所占用,但在进程的视野里只有虚拟内存。

同理,当进程主动释放内存的时候,也是释放虚拟内存,进而操作系统会将物理内存释放掉。

这里涉及到很多操作系统的原理知识。有兴趣大家可以进一步做研究。

本地实验

实验环境:

系统:Linux (发行版:ubuntu)

JDK:1.8

实验代码

每100ms创建一个大小约为 1MB的对象


public class Test {

  public static void main (String[] args) throws InterruptedException {

    while (true) {

      TestObj testObj = new TestObj();

      Thread.sleep(100L);

    }

  }

  public static class TestObj {

    private byte[][] field01  = new byte[1024][1024];

  }

}

运行命令:

java -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms1g -Xmx1g -Xloggc:/work/test-gc.log Test

实验现象


Attaching to process ID 3302, please wait...

Debugger attached successfully.

Server compiler detected.

JVM version is 25.271-b09

using thread-local object allocation.

Garbage-First (G1) GC with 2 thread(s)

Heap Configuration:

  MinHeapFreeRatio        = 40

  MaxHeapFreeRatio        = 70

  MaxHeapSize              = 1073741824 (1024.0MB)

  NewSize                  = 1363144 (1.2999954223632812MB)

  MaxNewSize              = 643825664 (614.0MB)

  OldSize                  = 5452592 (5.1999969482421875MB)

  NewRatio                = 2

  SurvivorRatio            = 8

  MetaspaceSize            = 21807104 (20.796875MB)

  CompressedClassSpaceSize = 1073741824 (1024.0MB)

  MaxMetaspaceSize        = 17592186044415 MB

  G1HeapRegionSize        = 1048576 (1.0MB)

Heap Usage:

G1 Heap:

  regions  = 1024

  capacity = 1073741824 (1024.0MB)

  used    = 54341376 (51.823974609375MB)

  free    = 1019400448 (972.176025390625MB)

  5.060935020446777% used

G1 Young Generation:

Eden Space:

  regions  = 51

  capacity = 675282944 (644.0MB)

  used    = 53477376 (51.0MB)

  free    = 621805568 (593.0MB)

  7.919254658385094% used

Survivor Space:

  regions  = 1

  capacity = 1048576 (1.0MB)

  used    = 1048576 (1.0MB)

  free    = 0 (0.0MB)

  100.0% used

G1 Old Generation:

  regions  = 0

  capacity = 397410304 (379.0MB)

  used    = 0 (0.0MB)

  free    = 397410304 (379.0MB)

  0.0% used

708 interned Strings occupying 47224 bytes.

top -p <PID>

image-20201223171327219

RES会逐渐升高,当升高到664752 (约649MB)时,基本不再变换。

大致变化波动约 ±(1-2)MB左右,这里猜测是因为JVM Survivor区在做快速标记回收占用的空间与Eden区的峰值产生的叠加效应。

因此得出结论 低频业务处理的Java服务最高物理内存 约等于 堆区峰值是的使用内存。

结论

当JVM堆内存管理时,当申请要使用虚拟内存时(注意:是申请而不是分配,分配是JVM启动时已经做好了分配。新生代老年代的内存比例也定了)。操作系统会相应的分配内存给Java进程,当GC之后,站在JVM的内存管理角度,会清理掉堆内的对象,但是并不会释放内存,即并没有把曾经申请到的内存归还给操作系统。

这也就印证了实验现象:首次Eden达到峰值触发GC之前,Java进程物理内存占用逐渐升高,因为这个阶段是首次Java进程向操作系统在申请内存,当GC之后Java进程物理内存占用趋于稳定,而不是骤减。

最后总结生产环境的问题:内存占用高应该属于正常现象。因为分配的堆内存高,使 RES ≈ Eden区的内存峰值,首次排查时dump堆内存分析发现堆内存远小于 RES也是偶然,因为正好赶上GC之后不久dump的内存。

之后又再濒临GC前dump过一次堆内存,发现堆内存值约等于RES值。

参考资料:

https://cloud.tencent.com/developer/article/1420898

https://segmentfault.com/a/1190000013504502

https://cloud.tencent.com/developer/article/1432381?from=information.detail.linux%2064%E4%BD%8D%20%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98

https://www.jianshu.com/p/4f5f18077890

相关文章

  • JVM发起GC后为何物理内存无明显变化

    问题现象 Java进程内存缓慢升高,到达一定值后居高不下:见 RES列 RES: A task's curren...

  • java(内存和gc)

    JVM内存和gc机制JVM内存 Java垃圾回收概况 Java GC(Garbage Collection,垃圾收...

  • JVM性能调优的6大步骤,及关键调优参数详解

    JVM内存调优 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。 1.Full GC ...

  • Java架构师面试题——JVM性能调优

    JVM内存调优 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。 1.Full GC ...

  • JVM

    简介 Jvm 系列一:Java类的加载机制Jvm系列二:JVM内存结构 --内存泄漏与内存溢出Jvm系列三:GC算...

  • Java基础

    JVM内存 1、JVM 内存管理和GC知识概述和总结(20190711) https://www.atatech....

  • 假期后来一波干货:一文理清JVM和GC

    “本文主要介绍 JVM和GC解析如有需要,可以参考如有帮助,不忘 点赞 ❥创作不易,白嫖无义! 一、JVM内存体系...

  • JVM内存结构

    以下信息摘录自:深入理解JVM的内存结构及GC机制 JVM内存管理 根据JVM规范,JVM把内存区域划分成了以下几...

  • Java知识图谱

    Java基础 JVM JVM内存模型和结构 GC原理&内存分配策略 性能调优:Thread Dump class ...

  • JVM调优

    一、JVM内存调优 对JVM内存的系统级的调优策略主要是减少GC的频率和Full GC的次数。 1️⃣Full G...

网友评论

      本文标题:JVM发起GC后为何物理内存无明显变化

      本文链接:https://www.haomeiwen.com/subject/pusunktx.html