为什么设置-Xmx4g但是java进程内存占用达到8g?

本文涉及的产品
图片翻译,图片翻译 100张
文本翻译,文本翻译 100万字符
语种识别,语种识别 100万字符
简介: 为什么设置-Xmx4g但是java进程内存占用达到8g?

前言


不知道大家在开发过程中有没有遇到过类似的问题,明明通过JVM参数-Xmx4g设置了最大堆内存大小为4g,但是程序运行一段时间后发现占用的内存明显超过了8g,却并没有出现内存溢出等问题,那是什么东西占用了额外的内存空间呢?


一、背景


1.通过free -g查看服务器内存使用情况

89.png

2.通过ps查看java进程

项目启动命令为:


java -Xmx6g -Xms6g - -XX:+UseG1GC -jar /home/pgcp/pgcp-0.0.1-SNAPSHOT.jar


88.png

3.通过top命令查看资源使用情况

87.png

VIRT:virtual memory usage 虚拟内存

1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等

2、假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量


RES:resident memory usage 常驻内存

1、进程当前使用的内存大小,但不包括swap out

2、包含其他进程的共享

3、如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反

4、关于库占用内存的情况,它只统计加载的库文件所占内存大小


SHR:shared memory 共享内存

1、除了自身进程的共享内存,也包括其他进程的共享内存

2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小

3、计算某个进程所占的物理内存大小公式:RES – SHR

4、swap out后,它将会降下来


问题:

java项目启动通过Xmx6g设置了最大堆内存为6g,但是项目运行一段时间后发现,项目进行占用的内存飙升到了12g。

难道设置的JVM参数没有生效?内存是被什么东西吃掉了?


二、通过jmap查看JVM内存分配


jmap -heap 打印heap的概要信息,GC使用的算法,heap(堆)的配置及JVM堆内存的使用情况.


命令:

jmap -heap 2196

86.png


发现JVM采用的G1垃圾回收器,堆内存的大小为6g。说明项目启动时设置的JVM参数是生效的。


三、通过NMT分析java进程的内存分配


1.什么是NMT

NMT的全称是Native Memory Tracker ,是一个本地内存跟踪工具。

常用来分析JVM的内存使用情况。


2.如何开启NMT

NMT功能默认关闭,可以通过以下方式开启:

-XX:NativeMemoryTracking=[off | summary | detail]


配置项 说明

off 默认配置

summary 只收集汇总信息

detail 收集每次调用的信息

注意,根据Java官方文档,开启NMT会有5%-10%的性能损耗;


如果想JVM退出时打印退出时的内存使用情况,可以通过如下配置项:


-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics


采用如下命令重启项目:

java -Xmx8g -Xms8g - -XX:+UseG1GC -XX:NativeMemoryTracking=detail -jar /home/pgcp/pgcp-0.0.1-SNAPSHOT.jar


3.通过jcmd命令分析java进程的内存

首先通过jps找到对应的Java程序的pid,然后使用如下命令:


jcmd <pid> VM.native_memory


也可以通过一下命令指定内存单位,并将结果输出到文本中

jcmd 16696  VM.native_memory detail scale=MB >temp.txt


85.png

可以看到java进程的整个memory主要包含了Java Heap、Class、Thread、Code、GC、Internal、Symbol、Native Memory Tracking、unknown这几部分;其中reserved表示应用可用的内存大小,committed表示应用正在使用的内存大小。


分析:

可以看到,除了Java Heap占用了8g内存,Class和Internal也各占用了1g左右的内存。


JVM的内存

先放一张JVM的内存划分图,总体上可以分为堆和非堆(粗略划分,基于java8)



84.png

83.png

那么一个Java进程最大占用的物理内存为:

Max Memory = eden + survivor + old + String Constant Pool + Code cache + compressed class space + Metaspace + Thread stack(*thread num) + Direct + Mapped + JVM + Native Memory


堆和非堆内存

堆和非堆内存有以下几个概念:

init

表示JVM在启动时从操作系统申请内存管理的初始内存大小(以字节为单位)。JVM可能从操作系统请求额外的内存,也可以随着时间的推移向操作系统释放内存(经实际测试,这个内存并没有过主动释放)。这个init的值可能不会定义。


used

表示当前使用的内存量(以字节为单位)


committed

表示保证可供 Jvm使用的内存大小(以字节为单位)。 已提交内存的大小可能随时间而变化(增加或减少)。 JVM也可能向系统释放内存,导致已提交的内存可能小于 init,但是committed永远会大于等于used。


max

表示可用于内存管理的最大内存(以字节为单位)。


NMT的输出说明:

Native Memory Tracking:
Total: reserved=6988749KB, committed=3692013KB
 堆内存
- Java Heap (reserved=5242880KB, committed=3205008KB)
 (mmap: reserved=5242880KB, committed=3205008KB)
 类加载信息
- Class (reserved=1114618KB, committed=74642KB)
 (classes #10657)
 (malloc=4602KB #32974)
 (mmap: reserved=1110016KB, committed=70040KB)
 线程栈
- Thread (reserved=255213KB, committed=255213KB)
 (thread #248)
 (stack: reserved=253916KB, committed=253916KB)
 (malloc=816KB #1242)
 (arena=481KB #494)
 代码缓存
- Code (reserved=257475KB, committed=46551KB)
 (malloc=7875KB #10417)
 (mmap: reserved=249600KB, committed=38676KB)
 垃圾回收
- GC (reserved=31524KB, committed=23560KB)
 (malloc=17180KB #2113)
 (mmap: reserved=14344KB, committed=6380KB)
 编译器
- Compiler (reserved=598KB, committed=598KB)
 (malloc=467KB #1305)
 (arena=131KB #3)
 内部
- Internal (reserved=6142KB, committed=6142KB)
 (malloc=6110KB #23691)
 (mmap: reserved=32KB, committed=32KB)
 符号
- Symbol (reserved=11269KB, committed=11269KB)
 (malloc=8544KB #89873)
 (arena=2725KB #1)
 nmt
- Native Memory Tracking (reserved=2781KB, committed=2781KB)
 (malloc=199KB #3036)
 (tracking overhead=2582KB)
- Arena Chunk (reserved=194KB, committed=194KB)
 (malloc=194KB)
- Unknown (reserved=66056KB, committed=66056KB)
 (mmap: reserved=66056KB, committed=66056KB)


nmt返回结果中有reserved和committed两个值,这里解释一下:

reserved

reserved memory 是指JVM 通过mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries),保证了其他进程不会被占用。


在堆内存下,就是xmx值,jvm申请的最大保留内存。


committed

committed memory 是JVM向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,相当于程序实际申请的可用内存。

在堆内存下,就是xms值,最小堆内存,heap committed memory。


注意,committed申请的内存并不是说直接占用了物理内存,由于操作系统的内存管理是惰性的,对于已申请的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存。所以committed > res也是很可能的.


四、解决Internal内存占用过大问题


通过NMT的内存分析,我们已经定位到内存占用持续增长的原因,那么如果解决nternal内部内存持续增长,又不触发full gc的问题呢?

看看我们之前的启动的jvm参数:


-Xmx8g -Xms8g -XX:+UseG1GC  -XX:NativeMemoryTracking=detail


说明:

环境是jdk1.8,堆内存分配8g,采用G1垃圾回收器。


通过jmap查看内存分配:

82.png

发现最大堆内存MaxHeapSize和我们的启动参数设置的一致。

但是发现最大元数据空间内存非常大,这显然不是一个合适的值。


修改jvm参数:

-Xmx4g -Xms4g -Xmn2g   -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m -XX:+UseG1GC  -XX:NativeMemoryTracking=detail


注意:

-XX:PermSize=256m -XX:MaxPermSize=512m 这两个参数对于1.8就是过期的参数。

jdk1.8的元空间大小要通过参数-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m来控制。


修改后重启应用通过jmap查看内存分配:

81.png


JDK8里的元空间实际上使用的也是堆外内存,默认没有设置元空间大小的情况下,元空间最大堆外内存大小和Xmx是一致的。


这里需要额外注意的是:

永久代(JDK8的原生去,换成元数据空间)存放JVM运行时使用的类,永久代的对象在full GC时进行垃圾收集。


下面的图描述了从1.7到1.8,永久代的变更:

80.png


总结


本文主要介绍如何结合top,jmap,NMT工具对java进程的内存进行分析。

1、JDK1.8开始,自带的hostspot虚拟机取消了过去的永久区,而新增了metaspace区,从功能上看,metaspace可以认为和永久区类似,其最主要的功用也是存放类元数据,但实际的机制则有较大的不同。

2、对于JVM里面的内存需要在启动时进行限制,包括我们熟悉的堆内存,也要包括直接内存和元生区,这是保证线上服务正常运行最后的兜底。

3、重新熟悉java进程的内存组成。

java进程的内存组成 = heap + stack + metaspaceSize + directMemory

除了通过-Xmx4g -Xms4g参数控制程序启动的堆内存外,

不要忽视-Xss1024K控制每个stack的大小。

元空间限制:-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m

直接内存使用限制:-XX:MaxDirectMemorySize=128m


参考:https://www.cnblogs.com/rude3knife/p/13570423.html

目录
相关文章
|
8天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
|
3天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
15 1
|
6天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
12天前
|
Java Linux iOS开发
如何设置 Java 的环境变量
设置Java环境变量是使用Java开发工具和运行Java程序的前提。主要步骤包括:安装JDK,配置系统环境变量中的JAVA_HOME、PATH和CLASSPATH,确保命令行可直接调用javac和java命令。
|
13天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
6天前
|
监控 安全 Java
Java Z 垃圾收集器如何彻底改变内存管理
大家好,我是V哥。今天聊聊Java的ZGC(Z Garbage Collector)。ZGC是一个低延迟垃圾收集器,专为大内存应用场景设计。其核心优势包括:极低的暂停时间(通常低于10毫秒)、支持TB级内存、使用着色指针实现高效对象管理、并发压缩和去碎片化、不分代的内存管理。适用于实时数据分析、高性能服务器和在线交易系统等场景,能显著提升应用的性能和稳定性。如何启用?只需在JVM启动参数中加入`-XX:+UseZGC`即可。
122 0
|
4天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
71 38
|
1天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
5天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
20 1
[Java]线程生命周期与线程通信
|
3天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。