JVM-09自动内存管理机制【内存分配和回收策略】

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: JVM-09自动内存管理机制【内存分配和回收策略】

思维导图

20180801164421142.png

对象优先在eden区域分配


20180731222902719.png


理论

大多数情况下,对象在新生代的eden区中分配,当eden区没有足够的空间进行分配时,虚拟机将进行一次Minor GC。


虚拟机提供-XX:+PrintGCDetails参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。


实际应用中,GC日志一般都是输出到文件中,使用GC日志分析工具来进行分析。


案例

虚拟机参数设置及参数说明

JDK1.6

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC  -verbose:gc -XX:+PrintGCDetails


20180801145008128.png


-Xms20M 初始化堆内存 20M


-Xmx20M 最大堆内存20M,结合-Xms20M 即为堆内存不可扩展


-Xmn10M 新生代内存分配10M,结合-Xms -Xmx 可知 老年代也是10M


-XX:SurvivorRatio=8 默认值,可不配置。 新生代中Eden区与一个Survivor区的比例为8:1,即 Eden: from Survivor:to Survivor = 8:1:1,即8MB:1MB:1MB,新生代的可用空间为9MB。


-XX:+UseSerialGC 指定年轻代使用Serial垃圾收集器


-verbose:gc 和 -XX:+PrintGCDetails 发生垃圾回收时,打印GC日志


代码

package com.artisan.gc;
public class EdenAllocationGC {
  private int _1M = 1024 * 1024;
  /**
   * 
   * @Title: testGCAllocation
   * 
   * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
   *               -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails
   * 
   * @return: void
   */
  @SuppressWarnings("unused")
  private void testGCAllocation() {
    // 如下的分配,仅仅是为了占用些内存空间,方便观察GC回收情况
    byte[] object1 = new byte[2 * _1M];
    byte[] object2 = new byte[2 * _1M];
    byte[] object3 = new byte[2 * _1M];
    byte[] object4 = new byte[4 * _1M];
  }
  public static void main(String[] args) {
    new EdenAllocationGC().testGCAllocation();
  }
}


GC结果分析

[GC [DefNew: 6487K->159K(9216K), 0.0052344 secs] 6487K->6303K(19456K), 0.0052735 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4582K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  54% used [0x00000000f9a00000, 0x00000000f9e51f98, 0x00000000fa200000)
  from space 1024K,  15% used [0x00000000fa300000, 0x00000000fa327c28, 0x00000000fa400000)
  to   space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
 tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2995K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  14% used [0x00000000fae00000, 0x00000000fb0eccf8, 0x00000000fb0ece00, 0x00000000fc2c0000)
No shared spaces configured.



因为内容较少可以直接阅读 。 当然了也可以传到http://gceasy.io 在线生成一份GC分析报告。

20180801150700484.png


20180801150813358.png


这里我们直接来分析下这个GC日志

结合JVM参数的设置,通过GC日志的验证,符合设置。

def new generation   total 9216K ......
eden space 8192K ......
from space 1024K ......
to   space 1024K ......



def new generation 是通过-XX:+UseSerialGC指定的垃圾回收器,显示名称是由收集器决定的。


如果是用的Serial收集器,新生代名为“Default New Generation”,所以显示“[DefNew”。

如果是用的ParNew收集器,新生代名为“Parallel New Generation”,所以显示“[ParNew”。

如果是用的Parallel Scavenge收集器,新生代名则显示为“[PSYongGen”


-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8


新生代分配了10M,又因为SurvivorRatio = 8 。 所以 Eden: from Survivor:to Survivor = 8192K:1024K :1024K


通过new创建对象的方式,虚拟机会将对象的实例分配到堆内存中,具体的说是分配object1 、object2 、object3 三个对象到 Eden区+Survivor From,3个对象占6MB空间,而 Eden + Survivor From 的大小为9M,空间足够,优先分配到Eden区。 所以Eden区的内存被占用6M


分配object4时的时候,发现Eden+Survivor From剩余空间只有3M,而object4占用4M的内存,这个时候就会触发一次Minor GC ,输出的信息如下

[GC [DefNew: 6487K->159K(9216K), 0.0052344 secs] 6487K->6303K(19456K), 0.0052735 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

可以看到6487K->159K(9216K) ,新生代的内存由6487K变为了159K。 而堆内存 6487K->6303K基本没有发生变化,是因为 object1,object2,object3都是存活的对象,无法被GC回收。


GC期间又发现已有的3个2MB的对象都无法放入Survivor To空间(1MB),所以通过担保机制提前转移到老年代区(3个2MB的对象),此时Eden区恢复到8MB空间,然后将object4分配到Eden空间。


GC结束后,4M的object4被顺利的分配到了Eden区中,Survivor空闲。 老年代 tenured generation占用6M(object1,object2,object3占用)。 通过如下日志也可以眼睁这个结论

tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)


大对象直接进入老年代


image.png


理论


需要大量连续内存空的Java对象,一般称之为大对象。

PretenureSizeThreshold参数,可以在新生代直接分配的对象最大值,0表示没有最大值 。  可以使大于这个值的对象直接在老年代分配,避免在Eden区和Survivor区发生大量的内存复制,该参数只对Serial和ParNew收集器有效,Parallel Scavenge并不认识该参数

使用方法:-XX:PretenureSizeThreshold=1000000


案例

虚拟机参数设置及参数说明

JDK1.6

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC  -verbose:gc -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728


  • -XX:PretenureSizeThreshold=3145728 : 单位是byte, 3145728 = 3M,大于3M的对象直接在老年代分配,避免在Eden区和Survivor区发生大量的内存复制.

代码

package com.artisan.gc;
public class PretenureSizeThresholdTest {
  private int _1M = 1024 * 1024;
  /**
   * 
   * @Title: testPretenureSizeThreshold
   * 
   * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
   *               -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails
   *               -XX:PretenureSizeThreshold=3145728
   * 
   * @return: void
   */
  @SuppressWarnings("unused")
  private void testPretenureSizeThreshold() {
    // 如下的分配,仅仅是为了占用些内存空间,方便观察GC回收情况
    byte[] object4 = new byte[4 * _1M];
  }
  public static void main(String[] args) {
    new PretenureSizeThresholdTest().testPretenureSizeThreshold();
  }
}


GC日志

JDK1.6

Heap
 def new generation   total 9216K, used 507K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,   6% used [0x00000000f9a00000, 0x00000000f9a7ee98, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2985K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  14% used [0x00000000fae00000, 0x00000000fb0ea690, 0x00000000fb0ea800, 0x00000000fc2c0000)
No shared spaces configured.


可以看到这里并没有发生Minor GC ,仅仅是打印了堆内存信息。 通过-XX:PretenureSizeThreshold=3145728的设置,4M大小的object4 大于设置的3M阀值,直接分配到了老年代。

 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)


长期存活的对象将进入老年代


20180731223005763.png


理论


现在商用虚拟机都采用分代收集的思想来管理内存,那么内存回收就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代。


为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。 如果对象在Eden畜生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设置为1。 对象在Survivor区中没经历过一次Minor GC且存活下来,年龄就增加1岁。当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。


对象晋升老年代的年龄可以通过-XX:MaxTenuringThreshold设置


案例

我们分别将-XX:MaxTenuringThreshold=1-XX:MaxTenuringThreshold=15 来看下GC日志的区别。


虚拟机参数设置及参数说明

JDK1.6

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC  -verbose:gc -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1


结合这个虚拟机参数设置,我们来构造符合预期的数据


-Xms20M -Xmx20M:java堆内存初始化值和最大值均为20M,不可扩展。

-Xmn10M :同时给新生代分配10M内存,可以推算出老年代也是20-10=10M

通过-XX:SurvivorRatio=8可知,新生代中Eden : Survivor From : Survivor To = 8:1:1 ,所以新生代能用的最大的内存为9M。

通过-XX:MaxTenuringThreshold设置对象在新生代存活的最大年龄。


根据Eden : Survivor From : Survivor To = 8:1:1 来构造对象的大小 。 一个256KB的对象,确保在不符合MaxTenuringThreshold的情况下,Survivor To 区能够有足够的空间存放这个256KB的对象。


代码

package com.artisan.gc;
public class MaxTenuringThresholdTest {
  private static final int _1M = 1024 * 1024;
  /**
   * 
   * 
   * @Title: testMaxTenuringThreshold
   * 
   * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
   *               -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails
   *               -XX:MaxTenuringThreshold=1
   * 
   * 
   * @return: void
   */
  public void testMaxTenuringThreshold() {
    // 根据JVM参数的设置,分配合理的大小,达到测试的目的
    byte[] object1 = new byte[_1M / 4];
    byte[] object2 = new byte[_1M * 4];
    // 什么时候进入老年代取决于-XX:MaxTenuringThreshold
    byte[] object3 = new byte[_1M * 4];
    object3 = null;
    byte[] object4 = new byte[_1M * 4];
  }
  public static void main(String[] args) {
    new MaxTenuringThresholdTest().testMaxTenuringThreshold();
  }
}


XX:MaxTenuringThreshold=1时的 GC日志


20180801214551561.png

堆内存新生代可用空间为9M, 首先在堆内存中分配了object1 256KB的内存,紧接着分配了object2 占用4M内存空间,此时新生代中还剩余8M-( 256KB+4M ) 的内存空间, object3 占用一个4M的内存空间,空间已经不够,提前触发了一次Minor GC

[GC [DefNew: 4695K->415K(9216K), 0.0056811 secs] 4695K->4511K(19456K), 0.0057238 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 


发生Minor GC,Survivor From 只有1M的空间可用,也不够存放object2, 所以出发了担保机制,将该对象放到了老年代(10M),可以存放的下4M的object2 。但object1只有256KB,所以Survivor From可以存放的下object1。 同时 MaxTenuringThreshold变为1 。


将object3置为null(这个时候已经没有引用,对象已经死亡,GC可以回收),分配object4 的时候又触发了一次Minor GC 。 此时object1已经达到了MaxTenuringThreshold,符合清理到老年代的要求,可以看到新生代from space已经被清为0了。

from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)

XX:MaxTenuringThreshold=15时的 GC日志


image.png


from space 使用了40% ,存放的是object1对象,因没达到XX:MaxTenuringThreshold,暂时还没有清理到老年代。


动态对象年龄判定


20180731223030352.png

理论


为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。


案例

虚拟机参数设置及参数说明

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC  -verbose:gc -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15


代码

package com.artisan.gc;
public class MaxTenuringThresholdTest {
  private static final int _1M = 1024 * 1024;
  /**
   * 
   * 
   * @Title: testMaxTenuringThreshold
   * 
   * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
   *               -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails
   *               -XX:MaxTenuringThreshold=15
   * 
   * 
   * @return: void
   */
  public void testMaxTenuringThreshold() {
    // 根据JVM参数的设置,分配合理的大小,达到测试的目的
    byte[] object1 = new byte[_1M / 4];
    byte[] object2 = new byte[_1M / 4];
    // 什么时候进入老年代取决于-XX:MaxTenuringThreshold
    byte[] object3 = new byte[_1M * 4];
    byte[] object4 = new byte[_1M * 4];
    object4 = null;
    object4 = new byte[_1M * 4];
  }
  public static void main(String[] args) {
    new MaxTenuringThresholdTest().testMaxTenuringThreshold();
  }
}


GC日志


20180801215651290.png

设置了MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%【和上个案例的结果比对】,也就是说,object1、object2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来已经到达了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。


如果我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了


空间分配担保


20180731223105805.png


理论


在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。


如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。


如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。


JDK1.60 Update24之后HandlePromotionFailure参数不会影响虚拟机的空间分配担保策略了。

相关文章
|
2月前
|
存储 缓存 NoSQL
工作 10 年!Redis 内存淘汰策略 LRU 和传统 LRU 差异,还傻傻分不清
小富带你深入解析Redis内存淘汰机制:LRU与LFU算法原理、实现方式及核心区别。揭秘Redis为何采用“近似LRU”,LFU如何解决频率老化问题,并结合实际场景教你如何选择合适策略,提升缓存命中率。
391 3
|
8月前
|
存储 分布式计算 监控
阿里云服务器实例经济型e、通用算力型u1、计算型c8i、通用型g8i、内存型r8i详解与选择策略
在阿里云现在的活动中,可选的云服务器实例规格主要有经济型e、通用算力型u1、计算型c8i、通用型g8i、内存型r8i实例,虽然阿里云在活动中提供了多种不同规格的云服务器实例,以满足不同用户和应用场景的需求。但是有的用户并不清楚他们的性能如何,应该如何选择。本文将详细介绍阿里云服务器中的经济型e、通用算力型u1、计算型c8i、通用型g8i、内存型r8i实例的性能、适用场景及选择参考,帮助用户根据自身需求做出更加精准的选择。
|
4月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
251 4
AI代理内存消耗过大?9种优化策略对比分析
|
7月前
|
缓存 并行计算 PyTorch
PyTorch CUDA内存管理优化:深度理解GPU资源分配与缓存机制
本文深入探讨了PyTorch中GPU内存管理的核心机制,特别是CUDA缓存分配器的作用与优化策略。文章分析了常见的“CUDA out of memory”问题及其成因,并通过实际案例(如Llama 1B模型训练)展示了内存分配模式。PyTorch的缓存分配器通过内存池化、延迟释放和碎片化优化等技术,显著提升了内存使用效率,减少了系统调用开销。此外,文章还介绍了高级优化方法,包括混合精度训练、梯度检查点技术及自定义内存分配器配置。这些策略有助于开发者在有限硬件资源下实现更高性能的深度学习模型训练与推理。
1463 0
|
3月前
|
机器学习/深度学习 监控 安全
解密虚拟化弹性内存:五大核心技术与实施策略
本文深入解析虚拟化环境中实现内存弹性管理的五大核心技术与实施策略。内容涵盖内存架构演进、关键技术原理、性能优化方法及典型问题解决方案,助力提升虚拟机密度与资源利用率。
204 0
|
3月前
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
152 0
|
弹性计算 安全 数据库
【转】云服务器虚拟化内存优化指南:提升性能的7个关键策略
作为云计算服务核心组件,虚拟化内存管理直接影响业务系统性能表现。本文详解了内存优化方案与技术实践,助您降低30%资源浪费。
162 0
【转】云服务器虚拟化内存优化指南:提升性能的7个关键策略
|
8月前
|
机器学习/深度学习 存储 PyTorch
PyTorch内存优化的10种策略总结:在有限资源环境下高效训练模型
在大规模深度学习模型训练中,GPU内存容量常成为瓶颈,特别是在训练大型语言模型和视觉Transformer时。本文系统介绍了多种内存优化策略,包括混合精度训练、低精度训练(如BF16)、梯度检查点、梯度累积、张量分片与分布式训练、
362 14
PyTorch内存优化的10种策略总结:在有限资源环境下高效训练模型
|
8月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
6月前
|
Java 关系型数据库 MySQL
JVM深入原理(六)(二):双亲委派机制
自定义类加载器打破双亲委派机制的方法:复写ClassLoader中的loadClass方法常见问题:要加载的类名如果是以java.开头,则会抛出安全性异常加载自定义的类都会有一个共同的父类Object,需要在代码中交由父类加载器去加载自定义类加载器不手动指定parent会默认指定应用类加载两个自定义类加载器加载同一个类会被认为是两个对象,只有相同的类加载器+想通的类限定名才会被认为是一个对象。
245 0