
# Java容器内存虚高以及OOM问题定位
#### Java进程内存相关背景
Java堆内存（Heap Memory）是Java虚拟机（JVM）管理的主要内存区域，但将Java堆内存限制参数Xmx并不能当作进程内存限制参数使用，将容器内存限制设置为和Xmx一样大，可能会导致OOM。除了堆内存，JVM还管理其他类型的内存，Java进程的内存占用情况可以简略总结为下图：
![](https://support.huaweicloud.com/cce_faq/zh-cn_image_0000002090224177.png "点击放大")
Java应用程序的内存使用情况包括JVM内存和非JVM内存。
- **JVM内存（JVM Memory）** ：这是JVM管理的内存部分，可以进一步分为堆（Heap）和非堆（Non-Heap）内存。
  - 堆内存（Heap Memory）：堆是OOM故障最主要的发生区域，它是内存区域中最大的一块区域，被所有线程共享，存储着几乎所有的实例对象、数组。所有的对象实例以及数组都要在堆上分配。Java堆也是垃圾收集器管理的主要区域，因此很多时候也被称为GC堆。从内存回收的角度来看，由于现在收集器基本都采用分代收集算法，所以Java堆中还可以细分为新生代（Young Gen）和老年代（Old Gen）。
  
  - 非堆内存（Non-Heap Memory）：这部分内存不用于存储对象实例，而是用于存储类的元数据、线程信息等。
    - 元空间（MetaSpace）：在Java 8中，永久代（PermGen）被元空间代替，元空间的本质和永久代类似，都是对JVM规范中方法区的实现，不过元空间和永久代最大的差别在于元空间并不在虚拟机中，而是使用的本地内存，元空间的大小只受本地内存限制。
    
    - Java虚拟机线程栈（VM Thread Stacks）： 对于每一个线程，JVM都会在线程被创建的时候，创建一个单独的栈。线程是私有的，除了本地方法以外，Java方法都是通过Java虚拟机栈来实现调用和执行过程的。
    
    - 本地方法栈（Native Thread Stacks）： 与虚拟机栈所发挥的作用是非常相似的，它们之间的区别不过是虚拟机栈为虚拟机执行Java方法（也就是字节码）服务，而本地方法栈则为虚拟机使用到的本地方法服务。
    
    - 代码缓存（CodeCache）：JVM代码缓存是JVM将其字节码存储为本机代码的区域。一般情况下不关心这部分内存区域，如果这块区域OOM了，在日志里面就会看到*java.lang.OutOfMemoryErrorcode cache*。
    
    - Native Memory：是指在JVM堆内存（heap memory）以外的内存，也会被叫做堆外内存。但它仍然属于这个Java程序的进程内存。通俗的说就是JVM管不到的原生内存。常见的是Java调用汇编/C/C++的时候，汇编/C/C++那部分所占用的内存。
     
   
- **非JVM内存（Non-JVM Memory）**：这是JVM之外的内存，包括本地库和JNI本地代码。
![](https://support.huaweicloud.com/cce_faq/public_sys-resources/notice_3.0-zh-cn.png)
Java容器过程中，尤其要注意*-Xmx* 或者*--XX:MaxRAMPercentage* 限制的是Heap空间，而不是整个Java进程的空间。比如配置*--XX:MaxRAMPercentage=70%* ，只能限制最大Heap内存为*70%\*总内存*，但是由于非Heap内存也会占用内存空间，最后导致整个Java进程内存可能超过预期。
 #### 容器内存指标说明
很多用户在虚拟机上部署Java业务时，因为整个虚拟机都给整个Java业务使用，所以很少关注Java业务整体内存使用问题；而在通过容器化提高调度密度（最终提高整体资源使用率）的过程中，绝大部分用户都会配置Memory Limit来控制单个容器（每个容器都是通过内核cgroup限制资源上限）的内存使用上限，防止某个容器使用过多内存从而影响本节点其他容器。由于内存是不可压缩资源，一旦超过Limit限制则会触发操作系统OOM导致容器重启，所以业务容器化后需要关注容器内存使用率指标。
- 操作系统以memory_limit_in_bytes值作为某cgroup可使用内存上限，当某个cgroup内存使用总量memory_usage_in_bytes即将超过memory_limit_in_bytes时，操作系统将尝试回收内存，如果可以回收且回收后低于memory_limit_in_bytes，则不会触发OOM，否则触发OOM。memory_usage_in_bytes计算方式如下： **memory_usage_in_bytes = 匿名内存（inactive_anon + active_anon） + file内存（inactive_file+active_file）**
  - active_anon：活跃的LRU列表中匿名和swap缓存的字节数，包括tmpfs。
  
  - inactive_anon：不活跃的LRU列表中匿名和swap缓存的字节数，包括tmpfs。
  
  - active_file：活跃LRU列表中文件支持的（file-backed）的内存字节数。
  
  - inactive_file：不活跃列表中文件支持的（file-backed）的内存字节数。
  
  
  由于Kubernetes暂时不支持容器中开启swap内存，所以可以简单理解容器业务中匿名内存不可回收，file内存（主要包括应用自己管理的内存映射文件mmap、文件读写缓存、二进制、动态链接库等）可以被回收。当触发回收时优先将inactive_file对应的文件内容写回磁盘后回收对应内存（由于active_file列表中为最近访问过文件的内存，所以尽量不回收active_file对应文件内存）。
  
- 云原生监控均以容器WorkingSet指标作为容器内存指标： **WorkingSet内存 = 匿名内存（inactive_anon + active_anon） + active_file**
  
 
#### Java容器内存虚高定位思路
通过以下配置示例分析Java进程在容器中的使用场景：
![](https://support.huaweicloud.com/cce_faq/zh-cn_image_0000002193279184.png "点击放大")
在配置示例中，可以看到以下关键参数：
- -XX:MaxRAMPercentage=70.0：表示最大堆内存为容器内存限制的70%
- -XX:MaxMetaspaceSize=512m：表示元空间内存占用触发FGC的阈值为512M
- -Xss256k：表示为每个线程分配256k的内存
- -XX:+UseG1GC：表示使用G1 GC作为垃圾收集器
在该示例中，容器内存Limit为6G，假设容器内存实际使用率偏高，可通过以下步骤进行排查：
1. **确认是WorkingSet内存高还是真实内存高**
   登录目标容器，运行以下命令查看内存统计信息：
   ```
   cat /sys/fs/cgroup/memory/memory.stat
   ```
   查看total_cache（缓存内存量）、total_rss（当前应用进程实际使用内存量）、total_inactive_file（不活跃文件内存使用量）。
   则**WorkingSet内存 = total_cache + total_rss - total_inactive_file**
   假设memory.stat文件内容如下：
   ```
   rss 1048576
   cache 524288
   inactive_file 262144
   ```
   则WorkingSet内存为：
   ```
   WorkingSet = 1048576 + (524288 - 262144) = 1310720 bytes
   ```
   由于标准云原生监控都是采集WorkingSet内存，当看到容器内存高时，先确认容器WorkingSet内存和匿名内存使用率，如果active_file内存过高，结合[容器内存指标说明]，需要分析代码是否频繁读写文件、读取大文件或者有内存映射文件mmap。
   如果WorkingSet和匿名内存基本上重合，则重点需要分析JVM内存构成、GC等判断影响。
   
2. **分析Java业务内存分配和GC**
   1. 进入业务容器查看Java最大实际堆内存大小 进入容器业务容器使用**-XshowSettings:vm -version**选项查看JVM的设置，确认Java进程heap最大堆内存配置是否符合预期。
      本例中容器内存Limit为6G，Java启动参数-XX:MaxRAMPercentage=70%，则最大heap内存为4.2G时符合预期。
      
   
   2. 分析查看GC日志，根据上述启动参数**-Xloggc:.../logs/gc.log**查看GC日志。由于Java业务内存主要由JVM回收，通过分析GC日志主要是jvm是否回收是否符合预期（比如是否有fullGC等问题）。
   
   3. 通过Java自带的Native Memory Tracking（NMT）分析内存JVM内存占用情况
      1. 首先需要在Java进程的启动参数中添加**-XX:NativeMemoryTracking=detail**
         ```
         # Java 启动时先打开 NativeMemoryTracking，默认是关闭的。注意不要在生产环境长期开启，有性能损失 
         java -XX:NativeMemoryTracking=detail -jar
         ```
         
      
      2. 然后执行命令 **jcmd *pid* VM.native_memory summary** ，其中pid为指定进程号， 查看内存分配信息。
         ```
         # 查看详情 
         jcmd pid VM.native_memory summary
         ```
         ![](https://support.huaweicloud.com/cce_faq/zh-cn_image_0000002193283840.png "点击放大")
         
      
      
      Java进程所占的内存项有：Java Heap、Class、Thread、Code等对应内存空间。其中Total committed内存大约为：5.2G，但是Java Heap committed内存就占了大约4.2G，因此Pod使用率高主要还是因为Java堆内内存使用较多。
      
   
   4. **heap dump分析**
      通过jmap获取Java进程的dump内存数据，分析dump后的bin文件，是否符合预期。
      执行命令**jmap -dump:format=b,file=/xxx/dump.hprof pid** （注意加live参数会触发GC，命令如下**jmap -dump:live,format=b,file=dump.hprof pid**）
      通过分析dump bin文件，可以看到在Java进程中未被GC回收的内存数据有数据库查询的大对象、大数组等信息。该部分为堆内内存数据，如果不触发Full GC不会进行内存回收。建议部分SQL语句可以进行优化，非必要字段不进行查询。
      
    
**综上以上分析和大规模测试过程结果可以得出以下结论：**
1. Java业务在压测过程中 -XX:MaxRAMPercentage配置为70%生效，对heap内存上限设置在4G左右时业务压测均正常无问题，heap内存主要存储缓存数据库查询的结果，可以被JVM主动回收。
2. Java业务在压测过程中no-heap内存诉求大约在1G左右。
3. 因此云原生监控内存高的主要原因为： -XX:MaxRAMPercentage配置为70%，容器Limit配置为6G，上述配置导致Java总内存约为heap内存4.2G+non-heap内存1G共5.2G，因此云原生监控内存使用率约为5.2/6=87%。
**问题处理：**
建议调整Java进程启动参数将-XX:MaxRAMPercentage参数由70.0%设置为50.0%，同时调整memory.Limits参数由6G扩大至8G，如此保证heap内存达到最大诉求的同时，给non-heap内存预留足够空间。
#### 附录：JVM参数配置说明
在Java进程可以通过一些启动参数可以堆栈内存的使用。下面将对JDK8以后版本中的常用参数进行介绍。
**Java堆栈内存相关参数**
Java堆内存是Java虚拟机所管理的内存中最大的一块，是所有线程共享的一块内存区域，在虚拟机启动时创建。Java堆内存区域唯一目的就是存放对象实例，几乎所有的对象实例和数组都在这里分配内存。
- -Xms ：设置最小堆内存，并初始化堆内存大小。单位可以是k/K、m/M、g/G，最小值为1M；例如*-Xms6m*。
- -Xmx：设置最大堆内存。一般建议与*-Xms*保持一致，防止动态扩展。
- -Xmn：设置年轻代内存最大允许大小，并初始化年轻代内存大小。官方建议该区域大小一般为堆内存的1/2-1/4之间。设置太小会导致频繁发生Minor GC，设置过大只有full GC才会生效，fullGC完成时间一般较久。例如*-Xmn2G*。
- -XX:NewSize：设置年轻代内存初始大小。
- -XX:MaxNewSize：设置年轻代最大内存大小。
- -XX:MetaspaceSize：设置元空间的大小（使用的是本地内存），超出时将触发FGC。MetaspaceSize表示使用过程中触发GC的阈值。
- -XX:MaxMetaspaceSize：设置元空间的最大值（使用的是本地内存）。例如*-XX:MaxMetaspaceSize=256m*。
- -Xss：设置线程栈的大小。Linux/x64平台下默认值是1024kb。例如*-Xss1024k*。
**GC相关参数**
这些参数控制JVM如何执行垃圾回收策略：
- -XX:+UseG1GC：启用G1（garbage first）算法的垃圾回收策略。对于堆内存分配大小大于6G，且GC延迟要求低的应用程序建议使用G1收集器。
- -XX:+PrintGCDetails：在每次发生GC时打印GC详细信息，默认关闭该参数。
- -XX:+PrintGCDateStamps：在每次发生GC时打印日期戳。
- -Xloggc: gc.log：GC日志文件的输出路径。
**OOM相关参数**
- -XX:+HeapDumpOnOutOfMemoryError：当JVM抛出java.lang.OutOfMemoryError异常时，将heap转储到物理文件中。
- -XX:HeapDumpPath=/var/log/xxx/xx.hprof：写入dump文件的路径，使用.hprof格式。
**容器场景下相关参数**
因为容器场景下，容器的资源分配是动态可调的，如果每次调整完容器的资源配额，然后再手动设置最大堆内存，比较繁琐。可以通过以下参数动态调整最大堆内存大小。
- -XX:+UseContainerSupport：默认启用容器支持。JVM能够自动进行容器平台检测，能够确定容器中运行的Java进程可用的内存量和CPU处理器。
- -XX:MaxRAMPercentage=xxx：设置Java堆内存的最大百分比。例如，设置为-XX:MaxRAMPercentage=50，意味着Java堆内存最大不超过容器可用内存的50%。适用于容器场景下资源限制量动态变化，动态调节最大堆内存大小，等同于*-Xmx*参数。
- -XX:MinRAMPercentage=xxx：设置Java堆内存的最小百分比，当容器的内存资源限制较低时，确保堆内存的最低分配。当容器Memory资源Limits大小为250MB及以下时，使用该参数计算最大堆内存。
 
