一步步优化JVM四:决定Java堆的大小以及内存占用

简介:

到目前为止,还没有做明确的优化工作。只是做了初始化选择工作,比如说:JVM部署模型、JVM运行环境、收集哪些垃圾回收器的信息以及需要遵守垃圾回收原则。这一步将介绍如何评估应用需要的内存大小以及Java堆大小。首先需要判断出应用存活的数据的大小,存活数据的大小是决定配置应用需要的Java堆大小的重要条件,也能够决定是否需要重新审视一下应用的内存需求或者修改应用程序以满足内存需求。


   注意:存活数据是指,应用处于稳定运行状态下,在Java堆里面长期存活的对象。换一句话说,就是应用在稳定运行的状态下,Full GC之后,Java堆的所占的空间。
 
约束
   有多少物理内存可以供JVM使用?是部署多个JVM或者单个JVM?对做出的决定有重要影响。下面列出了一些要点可以帮助决定有多少物理内存可以供使用。
   1、一个机器上面只是部署一个JVM,且就一个应用使用?如果是这种情况,那么机器的所有物理内存可以供JVM使用。
   2、一个机器上部署了多个JVM?或者一个机器上部署了多个应用?如果是这两个中的任何一种情况,你就必须要决定每一个JVM或者应用需要分配多少内存了。
   无论是前面的哪种情况,都需要给操作系统留出一些内存。
 
HotSpot VM的堆结构
   在做内存占用测量之前,我们必须要先理解HotSpot VM Java堆的结构,理解这个对决定应用需要的Java堆大小以及优化垃圾收器性能有很好的帮助。
   HotSpot VM有3个主要的空间:young代、old代以及permanent代,如上图所示。
 
   当Java应用分配Java对象时,这些对象被分配到young代。在经历过几次minor GC之后,如果对象还是存活的,就会被转移到old代空间。permanent代空间存储了VM和Java类的元数据比如内置的字符串和类的静态变量。
 
   -Xmx和-Xms这个两个命令行选项分别指定yound代加上old代空间的总和的初始最大值和最小值,也就是Java堆的大小。当-Xms的值小于-Xmx的值的时候,Java堆的大小可以在最大值和最小值之前浮动。当Java应用强调吞吐量和延迟的时候,倾向于把-Xms和-Xmx设置成相同的值,由于调整young代或者old代的大小都需要进行Full GC,Full GC降低吞吐量以及加强延迟。
 
   young代的空间可以通过下面的任意一个命令行选项来设置:
 
Html代码   收藏代码
  1. 1、-XX:NewSize=<n>[g|m|k]  
 
   
   young代的初始值和最小值。<n>是大小,[g|m|k]表示单位是G字节,M字节或者千字节。young代的大小不会小于这个值。当设定 -XX:NewSize=<n>[g|m|k] 的时候, -XX:MaxNewSize =<n>[g|m|k] 需要被指定。
Html代码   收藏代码
  1. 2、-XX:MaxNewSize=<n>[g|m|k]  
   young区空间的最大值。同上面反过来,当指定-XX:MaxNewSize=<n>[g|m|k]的需要指定-XX:NewSize=<n>[g|m|k]。
Html代码   收藏代码
  1. 3、-Xmn<n>[g|m|k]  
   直接指定young代的初始值、最小值以及最大值。也就是说,young区的大小被固定成这个值了。这个值用来锁定young代的大小很方便。
 
   有一点需要注意的是,如果-Xms和-Xmx没有被设定成相同的值,而且-Xmn被使用了,当调整Java堆的大小的时候,不会调整young代的空间大小,young代的空间大小会保持恒定。因此,-Xmn应该在-Xms和-Xmx设定成相同的时候才指定。
 
   old代的空间大小可以基于young代的大小进行计算,old代的初始值的大小是-Xms的值减去-XX:NewSize,最大值是-Xmx减去-XX:MaxNewSize,如果-Xmx和-Xms设置成了相同的值,而且使用-Xmn选项或者-XX:NewSize和-XX:MaxNewSize设置成了相同的值,那么old代的大小就是-Xmx减去-Xmn。
 
   permanent代的大小通过下面命令行参数指定
Html代码   收藏代码
  1. 1、-XX:PermSize=<n>[g|m|k]  
   表示permanent代的初始值和最小值,n表示数值,g|m|k表示单位、permanent的空间一定不会比这个空间小。
Html代码   收藏代码
  1. 2、-XX:MaxPermSize=<n>[g|m|k]  
   permanent代的最大值,permanent代的大小不会超过这个值。
 
   Java应用应该指定这两个值成为同一个值,由于这个值的调整会导致Full GC。
 
 
   如果上面提到的Java堆大小、young代、permanent代的大小都没有指定,那么JVM会根据应用的情况自行计算。
 
   在young代、old代以及permanent代中任何一个空间里面无法分配对象的时候就会触发垃圾回收,理解这点,对后面的优化非常重要。当young代没有足够空间分配Java对象的时候,触发minor GC。minor GC相对于Full GC来说会更短暂。
 
 
   一个对象在经历过一定次数的Minor GC之后,如果还存活,那么会被转移到old代(对象有一个“任期阀值”的概念,优化延迟的时候再介绍)。当old代没有足够空间放置对象的时候,HotSpot VM触发full GC。实际上在进行Minor GC的时候发现没有old代足够的空间来进行对象的转移,就会触发FullGC,相对于在MinorGC的过程中发现对象的移动失败了然后触发FullGC,这样的策略会有更小的花费。当permanent代的空间不够用的时候的,也会触发FullGC。
 
   如果FullGC是由于old代满了而触发的,old代和permanent代的空间都会被垃圾回收,即使permanent代的空间还没有满。同理,如果FullGC是由于permanent代满了而触发的,old代和permanent代的空间都会被垃圾回收,即使old代的空间还没有满。另外,young代同样会被垃圾回收,除非-XX:+ScavengeBeforeFullGC选项被指定了,-XX:+ScavengeBeforeFullGC关闭FullGC的时候young代的垃圾回收。
 
 
堆大小优化的起点
   为了进行Java堆大小的优化,一个合适的起点很重要。这节描述的方案是需要先使用比应用需要量更大的Java堆作为开始。这一步的目的是收集一些初始化信息以及为了进一步优化Java堆大小需要的数据。
 
   就像在“选择JVM runtime”小节里面提到过的,由吞吐量垃圾回收器(throughput garbage collector)开始。记住,使用吞吐量垃圾回收器通过设置-XX:+UserParallelOldGC命令行选项,如果你使用的HotSpot VM不支持的这个选项,那么就使用-XX:+UserParallelGC。
 
   如果你能够准确的预估到应用需要消耗的Java堆空间,可以通过设定-Xmx和-Xms来作为这个步骤的起点。如果你不知道该设定什么值,就让JVM来选择吧,反正后面,都会根据实际情况进行优化调整。
 
   关于如何监控GC日志前面的“GC优化基础”已经描述过了。GC日志会展示在使用中的java堆的大小。初始化和最大的堆大小可以通过-XX:+PrintCommandLineFlags来查看。-XX:+PrintCommandLineFlags打印出在HotSpot VM初始化的时候选择的初始值和最大值比如-XX:InitialHeapSize=<n> -XX:MaxHeapSize=<m>,这里n表示初始化的java堆大小值,m表示java堆的最大值。
 
   不管你是指定java堆的大小还是使用默认的大小,必须让应用进入稳定运行的状态,你必须要有能力和手段让应用处于和线上稳定运行的状态相同的状态。
 
   如果在企图让应用进入稳定状态的时候,你在垃圾回收日志里面观察到OutOfMemoryError,注意是old代溢出还是permanent代溢出。下面一个old代溢出的例子:
 
Html代码   收藏代码
  1. 2012-07-15T18:51:03.895-0600: [Full GC[PSYoungGen: 279700K->267300K(358400K)]  
  2. [ParOldGen: 685165K->685165K(685170K)]  
  3. 964865K->964865K(1043570K)  
  4. [PSPermGen: 32390K->32390K(65536K)],0.2499342 secs]  
  5. [Times: user=0.08 sys=0.00, real=0.05 secs]  
  6. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  

 
   上面重要的部分加粗标示了,由于使用的是吞吐量垃圾回收器,old代的统计信息标示为ParOldGen。这行表示了old代的在FullGC的时候占用的空间。从这个结果来看,可以得出的结论是old代的空间太小了,由于FullGC前后old代的被占用的空间和分配的空间基本相等了,因此,JVM报了OutOfMemoryError。相比较,通过PSPermGen这行可以看出permanent代的空间占用是32390K,和他的容量(65536K)比还是有一定的距离。
 
下面的例子展示了由于permanent太少了而导致的OutOfMemoryError发生的例子
 
Html代码   收藏代码
  1. 2012-07-15T18:26:37.755-0600: [Full GC  
  2.   [PSYoungGen: 0K->0K(141632K)]  
  3.   [ParOldGen: 132538K->132538K(350208K)]  
  4.   32538K->32538K(491840K)  
  5.   [PSPermGen: 65536K->65536K(65536K)],  
  6.   0.2430136 secs]  
  7.   [Times: user=0.37 sys=0.00, real=0.24 secs]  
  8.   java.lang.OutOfMemoryError: PermGen space  

 
   同上面一样,把关键行标示出来了,通过PSPermGen这行可以看出在FullGC前后,他的空间占用量都和他的容量相同,可以得出的结论是permanent代的空间条小了,这样就导致了OutOfMemoryError。在这个例子里面,old的占用空间(132538K)远比他的容量(350208K)小。
 
   如果在垃圾回收日志中观察到OutOfMemoryError,尝试把Java堆的大小扩大到物理内存的80%~90%。尤其需要注意的是堆空间导致的OutOfMemoryError以及一定要增加空间。比如说,增加-Xms和-Xmx的值来解决old代的OutOfMemoryError,增加-XX:PermSize和-XX:MaxPermSize来解决permanent代引起的OutOfMemoryError。记住一点Java堆能够使用的容量受限于硬件以及是否使用64位的JVM。在扩大了Java堆的大小之后,再检查垃圾回收日志,直到没有OutOfMemoryError为止。
 
   如果应用运行在稳定状态下没有OutOfMemoryError就可以进入下一步了,计算活动对象的大小。
 
计算活动对象的大小
   就像前面提到的,活动对象的大小是应用处于稳定运行状态时,长时间存活数据占用的Java堆的空间大小。换句话说,就是应用稳定运行是,在FullGC之后,old代和permanent代的空间大小。
 
   活动对象的大小可以通过垃圾回收日志查看,它提供了一些优化信息,如下:
   1、应用处于稳定运行状态下,old代的Java堆空间占用数量。
   2、应用处于稳定运行状态下,permanent代的Java堆空间占用数量。
 
   为了保证能够准确的评估应用的活动对象大小,最好的做法是多看几次FullGC之后Java堆空间的大小,保证FullGC是发生在应用处于稳定运行的状态。
 
   如果应用没有发生FullGC或者发生FullGC的次数很少,在性能测试环境,可以通过Java监控工具来触发FullGC,比如使用VisualVM和JConsole,这些工具在最新的JDK的bin目录下可以找到,VisualVM集成了JConsole,VisualVM或者JConsole上面有一个触发GC的按钮。
 
   另外,jmap命令可以选择来强制HotSpot VM进行FullGC。jmap 需要-histo:live命令选项以及JVM进程id。JVM的进程id可以通过jps命令获取。比如JVM的进程id是348,jmap命令用来触发FullGC可以想如下这样写:
Html代码   收藏代码
  1. $ jmap -histo:live 348  

   jmap不仅仅触发FullGC,而且产生堆的关于对象分配的概要信息。不过就目前这步的目的而言,可以忽略产生的堆概要信息。
 
初始化堆大小配置
   本节描述了怎样利用活动对象的大小来决定初始化的Java堆的大小。下面的图,给出了应用存活的对象的大小。比较明智的做法是多收集几次FullGC信息,有更多的信息,能够做出更加好的决定。

   通过活动对象大小的信息,可以做出关于Java堆的大小有根据的决定,以及可以估计出最坏情况下会导致的延迟。
 
   比较常规是,Java堆大小的初始化值和最大值(通过-Xms和-Xmx选项来指定)应该是old代活动对象的大小的3到4倍。
 
   在上图中显示的FullGC信息中,在FullGC之后old代的大小是295111K,差不多是295M,即活动的对象的大小是295M。因此,推荐的Java堆的初始化和最大值应该是885M到1180M,即可以设置为-Xms885m -Xmx1180m。在这个例子中,Java堆的大小是1048570K差不多1048M,在推荐值范围内。
 
   另外一个常规是,permanent的初始值和最大值(-XX:PermSize和-XX:MaxPermSize)应该permanent代活动对象大小的1.2到1.5倍。在上图中看到在FullGC之后permanent代占用空间是32390K,差不多32M。因此,permanent代的推荐大小是38M到48M,即可以设置为-XX:PermSize=48m -XX:MaxPermSize=48m(1.5倍)。这个例子里面,permanent代的空间大小是65536K即64M,大出了17M,不过在1G内存的系统的中,这个数值完全可以忍受。
 
   另外一个常规是,young代空间应该是old代活动对象大小的1到1.5倍。那么在这里例子中,young代的大小可以设置为295M到442M。本例里面,young代的空间大小的358400K,差不多358M,在推荐值中间。
 
   如果推荐的Java堆的初始值和最大值是活动对象大小3到4倍,而young代的推荐只是1到1.5倍,那么old代空间大小应该是2到3倍。
 
   通过以上规则,我们可以使用的Java命令可以是这样的:
Html代码   收藏代码
  1. <span style="margin: 0px; padding: 0px; font-size: 14px;">  java -Xms1180m -Xmx1180m -Xmn295m -XX:PermSize=48m -XX:MaxPermSize=48m</span>  

 
另外一些考虑
   本节将提及到在进行应用内存占用评估的时候,另外一些需要记住的点。首先,必须要知道,前面只是评估的Java堆的大小,而不是Java应用占用的所有的内存,如果要查看Java应用占用的所有内存在linux下可以通过top命令查看或者在window下面通过任务管理器来查看,尽管Java堆的大小可能对Java应用占用内存做出了最大的贡献。 比如说,为了存储线程堆栈,应用需要额外的内存,越多的线程,越多内存被线程栈消耗,越深的方法间调用,线程栈越多。另外,本地库需要分配额外的内存,I/O缓存也需要额外的内存。应用的内存消耗需要评估到应用任何一个会消耗内存的地方。
 
   记住,这一步操作不一定能够满足应用内存消耗的需求,如果不能满足,就回过头来看需求是否合理或者修改应用程序。比较可行的一种办法是修改应用程序减小对象的分配,从而减少内存的消耗。
   Java堆的大小计算仅仅只是开始,根据需求,在后面的优化步骤中可能会修改。

原文链接:[ http://wely.iteye.com/blog/2339046 ]
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
10月前
|
机器学习/深度学习 算法 PyTorch
125_训练加速:FlashAttention集成 - 推导注意力优化的独特内存节省
2025年,大型语言模型的训练面临着前所未有的挑战。随着模型参数量和序列长度的不断增加,传统注意力机制的内存瓶颈问题日益突出。FlashAttention作为一种突破性的注意力算法,通过创新的内存访问模式和计算优化,显著提升了训练效率和内存利用。
996 3
|
10月前
|
存储 机器学习/深度学习 PyTorch
119_LLM训练的高效内存管理与优化技术:从ZeRO到Flash Attention
大型语言模型(LLM)的训练面临着前所未有的计算和内存挑战。随着模型规模达到数百亿甚至数千亿参数,高效的内存管理成为训练成功的关键因素之一。2025年,LLM训练的内存优化技术已经取得了显著进展,从ZeRO优化器到Flash Attention等创新技术,为训练超大规模模型提供了可能。
966 159
|
缓存 固态存储 Windows
如何让内存发挥到最大效能?全面优化指南,提升电脑运行体验
电脑内存使用不合理会导致卡顿,本文教你如何优化内存性能。检查内存容量与主板支持上限,考虑升级或调整配置;关闭后台程序、管理浏览器标签、结束异常进程以释放内存;设置虚拟内存、调整视觉效果、定期重启提升效率;必要时增加内存条、选择高频内存、更换固态硬盘。避免盲目清理内存和依赖大内存忽视其他硬件瓶颈。只需合理设置,无需额外花钱,就能显著提升电脑速度。
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
970 4
AI代理内存消耗过大?9种优化策略对比分析
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
673 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
763 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
|
存储 自然语言处理 算法
基于内存高效算法的 LLM Token 优化:一个有效降低 API 成本的技术方案
本文探讨了在构建对话系统时如何通过一种内存高效算法降低大语言模型(LLM)的Token消耗和运营成本。传统方法中,随着对话深度增加,Token消耗呈指数级增长,导致成本上升。
1449 7
基于内存高效算法的 LLM Token 优化:一个有效降低 API 成本的技术方案
|
存储 人工智能 API
AI代理性能提升实战:LangChain+LangGraph内存管理与上下文优化完整指南
在AI代理系统开发中,上下文工程成为提升系统性能的关键技术。本文探讨了从提示工程到上下文工程的转变,强调其通过为AI系统提供背景信息和工具支持,显著提升智能化程度和实用价值。文章系统分析了上下文工程的理论基础、核心策略(如写入、选择、压缩和隔离),并结合LangChain和LangGraph工具,展示了如何实现上下文工程技术以优化AI代理性能。通过Scratchpad机制、内存管理、RAG系统集成、多代理架构及沙盒环境等技术手段,开发者可以更高效地构建高性能、可扩展的AI系统。
1684 0
AI代理性能提升实战:LangChain+LangGraph内存管理与上下文优化完整指南
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
379 0
|
存储 运维 Kubernetes
Java启动参数JVM_OPTS="-Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"
本文介绍了Java虚拟机(JVM)常用启动参数配置,包括设置初始堆内存(-Xms512m)、最大堆内存(-Xmx1024m)及内存溢出时生成堆转储文件(-XX:+HeapDumpOnOutOfMemoryError),用于性能调优与故障排查。
1392 0