jvm,深入理解java虚拟机,内存分配与回收策略(代码片段)

你是我的天晴 你是我的天晴     2022-12-03     207

关键词:

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对

象分配内存以及回收分配给对象的内存。关于回收内存这一点,我们已经使用了大量篇幅去
介绍虚拟机中的垃圾收集器体系以及运作原理,现在我们再一起来探讨一下给对象分配内存
的那点事儿。
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标
量类型并间接地栈上分配[1]
),对象主要分配在新生代的Eden区上,如果启动了本地线程分
配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的
规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟
机中与内存相关的参数的设置

对象内存的分配主要在堆的新生代Eden区,少数情况会直接分配到老年代。

对象优先在Eden上分配

多数情况下,对象在新生代Eden上分配。Eden空间不够,虚拟机发起一次Minor GC。

-XX:+PrintGCDetail 打印内存回收日志,并且在进程退出时输出当前内存各区域的分配情况

-XX:+SurvivorRation=8 设置新生代中Eden区与一个Survivor区的空间比例

代码:

public class TestAllocation   
    private static final int _1MB = 1024*1024;  
      
    public static void main(String[] args)  
        testAllocation();  
      
      
    public static void testAllocation()  
        byte[] allocation1,allocation2,allocation3,allocation4;  
        allocation1 = new byte[2*_1MB];  
        allocation2 = new byte[2*_1MB];  
        allocation3 = new byte[2*_1MB];  
        allocation4 = new byte[4*_1MB];  
      
  
  

运行参数: -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M

参数解释: UseSerialGC 虚拟机使用Serial+SerialOld收集器进行回收

结果:

[GC[DefNew: 6980K->464K(9216K), 0.0048934 secs] 6980K->6608K(19456K), 0.0049317 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]   
Heap  
 def new generation   total 9216K, used 4890K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)  
  eden space 8192K,  54% used [0x00000000f9a00000, 0x00000000f9e52798, 0x00000000fa200000)  
  from space 1024K,  45% used [0x00000000fa300000, 0x00000000fa3741f0, 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 2514K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)  
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb074888, 0x00000000fb074a00, 0x00000000fc2c0000)  
No shared spaces configured.  

解释:

发生了一次Minor GC,新生代内存从6980K变为464K,但是总内存大小基本没变6980K-6608K,那是因为allocation1,allocation2,allocation3三个对象都存活,没有回收对象。

发生回收的原因为新生代不够为allocation4分配内存,此时根据复制算法需要将Eden+Survivor from复制到Survivor to区域,但是Survivor to不够放置3个2M的对象。此时出发分配担保机制,将这3个2M的对象转移到老年代。

所以最终的内存分配情况是:allocation4放在了eden区,allocation1,allocation2,allocation3放在了老年代。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝

生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major  GC/Full  GC):指发生在老年代的GC,出现了Major  GC,经常会伴
随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行
Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

大对象直接进入老年代

大对象指需要大量连续内存的对象,如字符串、数组。大对象会导致内存还有不少空间(但还不够)就会触发垃圾回收。

-XX:PretenureSizeThreshold 大于该值的对象直接在老年代中分配,避免在eden和两个Survivor区间发生复制

代码:

public class TestPretenureThreshold   
    private static final int _1MB = 1024*1024;  
      
    public static void testPretenureSizeThreshold()  
        byte[] allocation;  
        allocation = new byte[4 * _1MB];  
      
      
    //-XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=2M -Xms20M -Xmx20M -Xmn10M  
    public static void main(String[] args)  
        testPretenureSizeThreshold();  
      
  
  

结果:

Heap  
 def new generation   total 9216K, used 999K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)  
  eden space 8192K,  12% used [0x00000000f9a00000, 0x00000000f9af9f70, 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 2511K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)  
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb073d08, 0x00000000fb073e00, 0x00000000fc2c0000)  
No shared spaces configured.  


解释:allocation被直接分配到了老年代。

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

虚拟机采用分代的思想管理内存,每个对象都存在年龄(Age)计数器,如果对象在eden出生并经过第一次Minor GC后依然存活,并被Survivor容纳的话,其年龄就会被设置为1,然后在Survivor区中每熬过一次Minor GC,年龄就会加1,当年龄增加到一定程度(默认15),就会晋升为老年代。

-XX:MaxTenuringThreshold 设置晋升为老年代的年龄阀值。

-XX:+PrintTenuringDistribution 输出Survivor中对象的年龄分布。

代码:

public class TestTenuringThreshold   
    private static final int _1MB = 1024*1024;  
      
    public static void testTenuringThreshold()  
        byte[] allocation1,allocation2,allocation3;  
        allocation1 = new byte[_1MB/4];  
        allocation2 = new byte[4 * _1MB];  
        allocation3 = new byte[4 * _1MB];  
        allocation3 = null;  
        allocation3 = new byte[4 * _1MB];  
      
      
    //-XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -Xms20M -Xmx20M -Xmn10M  
    public static void main(String[] args)  
        testTenuringThreshold();  
      

结果:

[GC[DefNew  
Desired survivor size 524288 bytes, new threshold 1 (max 1)  
- age   1:     737872 bytes,     737872 total  
: 5188K->720K(9216K), 0.0049431 secs] 5188K->4816K(19456K), 0.0050418 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]   
[GC[DefNew  
Desired survivor size 524288 bytes, new threshold 1 (max 1)  
- age   1:        232 bytes,        232 total  
: 5065K->0K(9216K), 0.0018952 secs] 9161K->4816K(19456K), 0.0019256 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
Heap  
 def new generation   total 9216K, used 4178K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)  
  eden space 8192K,  51% used [0x00000000f9a00000, 0x00000000f9e14820, 0x00000000fa200000)  
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa2000e8, 0x00000000fa300000)  
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)  
 tenured generation   total 10240K, used 4816K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)  
   the space 10240K,  47% used [0x00000000fa400000, 0x00000000fa8b4020, 0x00000000fa8b4200, 0x00000000fae00000)  
 compacting perm gen  total 21248K, used 2514K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)  
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb074878, 0x00000000fb074a00, 0x00000000fc2c0000)  
No shared spaces configured.  

解释:

allocation1,allocation2被分配到eden区中,allocation3申请内存不够,触发Minor GC,allocation1 被移入Survivor中,内存还不够allocation2移入老年代

allocation3再次申请内存,allocation1会被移入老年代

最终新生代为allocation3  老年代为allocation1,allocation2

动态对象年龄判定

虚拟机并不永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升为老年代,如果Survivor中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等
到MaxTenuringThreshold中要求的年龄。。

/**
 *  长期存活的对象进入老年代
 * *VM参数:-verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 * XX:+PrintTenuringDistribution得到更详细的GC输出
 *
 * -XX:+UseSerialGC   Serial + SerialG old
 */
public class TestTenuringThreshold 
    private static final int _1MB=1024*1024;

    public static void main(String[] args) 
            byte[] allocation1,allocation2,allocation3;
             allocation1=new byte[_1MB/4];
            //什么时候进入老年代取决于XX:MaxTenuringThreshold设置
             allocation2=new byte[4*_1MB];
             allocation3=new byte[4*_1MB];
             allocation3=null;
             allocation3=new byte[4*_1MB];
    

执行代码,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预
期增加了6%,也就是说,allocation1、allocation2对象都直接进入了老年代,而没有等到15
岁的临界年龄。因为这两个对象加起来已经到达了512KB,并且它们是同年的,满足同年对
象达到Survivor空间的一半规则。我们只要注释掉其中一个对象new操作,就会发现另外一个

就不会晋升到老年代中去了

 空间分配担保

在Minor GC(新生代GC)之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果成立,那么Minor GC是安全的。不成立,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。

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

下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了
内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor
GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要
老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类
似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多
少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋
升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进
行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活
后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle  Promotion  Failure)。
如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full  GC。虽然担保
失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避
免Full GC过于频繁

jvm,深入理解java虚拟机,内存分配与回收策略(代码片段)

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。关于回收内存这一点,我们已经使用了大量篇幅去介绍虚拟机中的垃圾收集器体系以及运作原... 查看详情

jvm,深入理解java虚拟机,内存分配与回收策略(代码片段)

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。关于回收内存这一点,我们已经使用了大量篇幅去介绍虚拟机中的垃圾收集器体系以及运作原... 查看详情

深入理解java虚拟机-java内存区域,垃圾回收机制和内存分配策略(代码片段)

本篇主要参考周志明老师的《深入理解Java虚拟机》第三版一个Java程序,首先要经过javac编译成.class文件,.class文件是给JVM进行识别的,JVM将.class文件加载到方法区,执行引擎会执行这些字节码,执行时,... 查看详情

深入理解java虚拟机内存分配与回收策略实战(代码片段)

文章目录前言一、对象优先在Eden分配二、大对象直接进入老年代三、长期存活的对象将进入老年代四、动态对象年龄判定五、空间分配担保结尾前言对象的内存分配,从概念上讲,基本都是在堆上分配(而实际上也... 查看详情

深入理解java虚拟机系列——垃圾回收器与内存分配策略

判断对象是否存活的算法:简单版:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1。任何时刻计数器为0的对象就是不可能再被使用的。但主流的Java虚拟机都没有引用计... 查看详情

垃圾收集器与内存分配策略(深入理解java虚拟机)

3.1 概述垃圾收集器要解决哪些问题?哪些内存需要回收什么时候回收如何回收引用计数算法:当有一个地方引用,+1,引用失效,-1。   缺点:对象之间相互循环引用的问题。可达性分析算法:思路:通过一系列... 查看详情

深入理解java虚拟机-常用vm参数分析

Java虚拟机深入理解系列全部文章更新中...深入理解Java虚拟机-Java内存区域透彻分析深入理解Java虚拟机-常用vm参数分析深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲深入理解Java虚拟机-如何利用JDK自带... 查看详情

深入理解java虚拟机

title:深入理解Java虚拟机date:2020-05-1410:58:24tags:JVM,虚拟机目录title:深入理解Java虚拟机date:2020-05-1410:58:24tags:JVM,虚拟机1.运行时数据区域2.GC垃圾回收3.内存分配与回收策略4.类加载机制1.加载2.验证3.准备4.解析5.初始化5.类与类加载器1.... 查看详情

深入理解jvm-垃圾收集器与内存分配策略

在上面一篇文章中,介绍了java内存运行时区域,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程生灭;栈中的栈帧随着方法的进入和退出而有条不紊的执行着进栈出栈的操作,每一个栈帧中分配着多少内存基本上是在类... 查看详情

java虚拟机序列java中的垃圾回收与内存分配策略

在【java虚拟机系列】java虚拟机系列之JVM总述中我们已经详细讲解过java中的内存模型,了解了关于JVM中内存管理的基本知识,接下来本博客将带领大家了解java中的垃圾回收与内存分配策略。垃圾回收(GarbageCollection,GC)是java语... 查看详情

深入理解java虚拟机-java内存区域,垃圾回收机制和内存分配策略(代码片段)

本篇主要参考周志明老师的《深入理解Java虚拟机》第三版一个Java程序,首先要经过javac编译成.class文件,.class文件是给JVM进行识别的,JVM将.class文件加载到方法区,执行引擎会执行这些字节码,执行时,... 查看详情

jvm深入理解自动内存分配与垃圾回收

要想了解jvm自动内存分配,首先必须了解jvm的运行时数据区域,否则如何知道在哪里进行自动内存分配,如何进行内存分配,回收哪里的垃圾对象?jvm运行时数据区:程序计数器,虚拟机栈,本地方法栈,方法区,堆程序计数器... 查看详情

深入理解java虚拟机-如何利用visualvm对高并发项目进行性能分析

Java虚拟机深入理解系列全部文章更新中...深入理解Java虚拟机-Java内存区域透彻分析深入理解Java虚拟机-常用vm参数分析深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲深入理解Java虚拟机-如何利用JDK自带... 查看详情

《深入理解java虚拟机》--内存

  JVM对于操作系统来说是一种应用程序,JVM要运行的时候,操作系统会创建对应的进程而且分配一定大小的内存。 一、内存结构  当虚拟机得到系统分配的内存后,它在其内存空间中就是老大,管理对象内存的分配以及... 查看详情

深入理解java虚拟机原理之内存分配策略

更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/4743806801、对象优先在Eden分配大多情况,对象在新生代Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将进行一次MinorGC。虚拟机提供了参数-XX:+PrintGCDetails,在... 查看详情

《深入理解jvm——gc算法与内存分配策略》

 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行... 查看详情

《深入理解java虚拟机》读书笔记-垃圾收集器与内存分配策略

  在堆里存放着java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前需要知道哪些对象还存活,哪些对象已经死去。那怎么样去判断对象是否存活呢?     一、判断对象是否存活算法  1、引用计数法  ... 查看详情

《深入理解jvm——gc算法与内存分配策略》(代码片段)

 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行... 查看详情