理解JVM(4)- 堆内存的分代管理

前一篇从整体上了解了一下JVM的运行时数据区,它由_线程私有的栈内存_和_线程共享的堆内存、方法区_组成。本章节将详细了解一下堆内存又被分为哪些区域,或者说JVM是如何把对象分配到这些区域上的

JVM根据对象在内存中存活时间的长短,把堆内存分为新生代(包括一个Eden区、两个Survivor区)和老年代(Tenured或Old)。Perm代(永久代,Java 8开始被“元空间”取代)属于方法区了,而且仅在Full GC时被回收。大致如下图

为对象分配空间,就是把一块确定大小的内存从堆中划分出来(有一种例外情况,就是有可能经过JIT优化编译后,对象被拆分成标量类型从而变成了栈上分配)。新创建的对象主要分配在新生代的Eden区上,如果JVM启动了本地线程分配缓冲(TLAB,Thread Local Allocation Buffer),则对象将按线程优先分配在TLAB上,此区域仍然位于新生代的Eden区内。

关于TLAB

创建对象需要从堆中划分出一块确定大小的区域,那分配内存就是把指针从可用空闲区域挪动一段与对象大小相等的距离。而对象的创建是很频繁的行为,在并发情况并不是线程安全的,可能出现在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。为了解决这个问题,一个可行的方案就是TLAB,即把内存分配的动作按照线程划分在不同的空间内进行,即每个线程在堆内预先分配一小块内存,称为“本地线程分配缓冲”。哪个线程要给对象分配内存,就在自己的TLAB上分配,当自己的TLAB用完再去申请新的TLAB,这个时候再去进行指针的同步锁定,从而减小开销。

对象优先分配在Eden区

大部分情况下,对象会在新生代的Eden区中分配空间,当Eden区没有足够大小的连续空间来分配给新创建的对象时,JVM将会触发一次Minor GC

HotSpot的开发人员将GC执行分为比较模糊的三种模型:

  • Minor GC:发生在新生代,回收新生代中的垃圾,速度很快但也很频繁
  • Major GC:发生在老年代,比Minor GC慢10倍以上;通常会伴随一次Minor GC
  • Full GC:回收所有区域,包括堆内存、方法区(Java 8之前的“永久代”,Java 8开始取代永久代的“元空间”)和直接内存,速度慢,工作线程的暂停时间长

绝大多数对象所占的内存空间会在Minor GC中被回收(IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的),那些存活下来的对象会被分配到某一个Survivor(幸存区,名字也很形象),但如果Survivor的空间不足以安置存活对象的话,JVM会通过“空间分配担保机制”提前转移这些对象到老年代去。

  1. 新生代中为什么有两个Survivor区?为什么每次只使用其中一个呢?

这跟新生代采用的垃圾回收算法有关,新生代用的是“复制”算法,该算法的特点是牺牲一定的空间成本,来换取高效率的垃圾回收,此算法不会产生内存碎片,回收后内存比较规整。关于各回收算法的细节,下一个章节再介绍,这里就不累赘了。

  1. “空间分配担保”是什么?

在发生Minor GC之前,JVM会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则JVM会查看HandlePromotionFailure设置值是否允许担保失败。若允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时要改为进行一次Full GC。

下面这个示例代码演示了Survivor区空间不足,对象通过分配担保机制被提前转移到老年代去。Debug执行三条对象创建语句,通过JDK自带的Java VisualVM工具jvisualvm(同时安装Visual GC插件),可以直观的看到各个内存区的变化情况。

/**
 *  -Xms90m -Xmx90m -XX:+UseParNewGC
 *
 * 固定堆大小:90m
 *     - Young Gen: 1/3 * 90 = 30m (默认 Tenured / Young = 2)
 *         - Survivor * 2 : 1/10 * 30 = 3m * 2 (两个Survivor,默认 Eden / Survivor = 8)
 *         - Eden: 8/10 * 30 = 24m
 *     - Tenured: 2/3 * 90 = 60m  (默认 Tenured / Young = 2)
 */
public class HandlePromotionDemo {

    public static void main(String[] args) {
        byte[] obj1 = new byte[1024 * 1024 * 2];
        byte[] obj2 = new byte[1024 * 1024 * 10];
        byte[] obj3 = new byte[1024 * 1024 * 20];
    }

}

以下三个截图分别展示了三个对象依次创建后的内存各区情况

大对象直接进去老年代

大对象就是那些需要大量连续内存空间的对象,比如数组及很长的字符串。过多的大对象容易导致当内存空间仍然还有不少时就会提前触发GC以获取足够连续的空间来分配给这些大对象。
虚拟机提供了一个参数-XX:PretenureSizeThreshold,那些大于这个参数值的对象将直接在老年代分配,避免在Eden区和两个Survivor区之间发生大量的内存复制(新生代采用的是“复制”垃圾回收算法)。

下面这个示例代码指定一个Survivor区域容量大小为4MB,同时设置-XX:PretenureSizeThreshold=3145728,即3MB,之后创建一个略大于3MB的对象。运行此程序后,从VisualVM GC中可以看到此对象被分配到了老年代。

/**
 * -Xmn16m -Xms30m -Xmx30m -XX:SurvivorRatio=2 -XX:+UseParNewGC -XX:PretenureSizeThreshold=3145728 -XX:-UseTLAB
 *
 * Fixed Heap: 30MB
 *    - Survivor * 2: 4MB * 2
 *    - Eden: 8MB
 *    - Tenured: 14MB
 */
public class BiggerThanPretenureSizeThresholdObjToOld {

    public static void main(String[] args) throws Exception {
        System.gc(); // 尝试清除由监测工具生成的临时对象
        Thread.sleep(10000L);

        byte[] obj = new byte[1024 * 1024 * 3 + 1];
        boolean flag = true;
        while(flag) {
            Thread.yield();
        }
    }

}

对于极端情况,参数-XX:PretenureSizeThreshold未设置,而对象大于Eden空间的话,则同样直接在老年代分配空间

长期存活的对象会被晋升到老年代

虚拟机在进行内存回收的时候,为了能够识别哪些对象应该继续留在新生代(某一个Survivor区)、哪些对象应该被晋升(转移)到老年代,它给每个对象定义了一个对象年龄(Age)计数器。所有在新生代出生的对象,年龄可以认为是0,此时的数值没有任何意义。当对象经过第一次Minor GC后任然存活,并且Survivor有足够的空间来容纳它的话,对象被顺利转移到Survivor中,此时对象开始拥有实际意义的年龄,为1岁。在此之后,Survivor中的对象每“熬过”一次Minor GC,年龄就会增加1岁,当达到一定的年龄阈值(默认是15岁,可通过参数-XX:MaxTenuringThreshold设置),对象就会被晋升到老年代中。老年代中的对象就没有年龄的意义了。

下面我们通过一个示例来演示一下:对象年龄达到阈值后被晋升到老年代。设置参数,固定堆大小为90MB,新生代45MB,其中Eden和Survivor各占15MB、15MB、15MB,年龄阈值为2岁。

/**
 *  -XX:+PrintGCDetails -Xmn45m -Xms90m -Xmx90m -XX:SurvivorRatio=1 -XX:MaxTenuringThreshold=2 -XX:+UseParNewGC
 *
 *  Fixed Heap: 90M
 *      - Survivor *2: 15M *2
 *      - Edeb: 15M
 *      - Tenured(Old): 45M
 */
public class AgeOlderThanTenuringThresholdObjToOld {

    public static void main(String[] args) throws Exception {
        System.gc(); //尝试清除由监测工具生成的临时对象

        byte[] obj1 = new byte[1024 * 1024 * 2]; //执行完这一行之后各区使用情况: Eden[obj1]: 2/15, S0: 0/15, S1: 0/15, Old: 0/45
        byte[] obj2 = new byte[1024 * 1024 * 2]; //执行完这一行之后各区使用情况: Eden[obj1,obj2]: 4/15, S0: 0/15, S1: 0/15, Old: 0/45
        byte[] obj3 = new byte[1024 * 1024 * 12]; //对象obj3创建成功之前,虚拟机检测到Eden剩余空间(15-4=11)不足以分配给obj3,因此触发第一次Minor GC来释放空间给obj3,obj1和obj2晋升到幸存区,年龄为1,各区使用情况: Eden[obj3]: 12/15, S0: 0/15, S1[obj1(age=1),obj2(age=1)]: 4/15, Old: 0/45
        obj3 = null; //obj3不再有任何引用关联,下次GC的时候将会被回收
        byte[] obj4 = new byte[1024 * 1024 * 4]; //对象obj4创建成功之前,虚拟机检测到Eden剩余空间(15-12=3)不足以分配给obj4,因此触发第二次Minor GC来释放空间给obj4,这次GC中obj3会被回收,之后各区使用情况: Eden[obj4]: 4/15, S0:[obj1(age=2),obj2(age=2)]: 4/15, S1: 0/15, Old: 0/45
        byte[] obj5 = new byte[1024 * 1024 * 12]; //对象obj5创建成功之前,虚拟机检测到Eden剩余空间(15-4=11)不足以分配给obj5,因此触发第三次MinorGC来释放空间给obj5,这次GC中由于obj1,obj2的年龄都到达了阈值2岁,所以这两个对象将被晋升到老年代,之后各区使用情况:Eden[obj5]: 12/15, S0: 0/15, S1[obj4(age=1)]: 4/15, Old[obj1,obj2]: 4/45
    }

}

Debug逐行执行上面5个对象的创建语句,每个对象创建成功后的各区使用情况如下各图:

![对象obj1创建成功之后各区使用情况: Eden[obj1]: 2/15, S0: 0/15, S1: 0/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-bad6d5c9ff99db64.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj2创建之后各区使用情况: Eden[obj1,obj2]: 4/15, S0: 0/15, S1: 0/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-10c3d79b93943702.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj3创建成功之前,虚拟机检测到Eden剩余空间(15-4=11)不足以分配给obj3,因此触发第一次Minor GC来释放空间给obj3,obj1和obj2晋升到幸存区,年龄为1,各区使用情况: Eden[obj3]: 12/15, S0: 0/15, S1[obj1(age=1),obj2(age=1)]: 4/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-b21140f9bf41f6c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj4创建成功之前,虚拟机检测到Eden剩余空间(15-12=3)不足以分配给obj4,因此触发第二次Minor GC来释放空间给obj4,这次GC中obj3会被回收,之后各区使用情况: Eden[obj4]: 4/15, S0:[obj1(age=2),obj2(age=2)]: 4/15, S1: 0/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-aedc42eed3dbd0a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj5创建成功之前,虚拟机检测到Eden剩余空间(15-4=11)不足以分配给obj5,因此触发第三次MinorGC来释放空间给obj5,这次GC中由于obj1,obj2的年龄都到达了阈值2岁,所以这两个对象将被晋升到老年代,之后各区使用情况:Eden[obj5]: 12/15, S0: 0/15, S1[obj4(age=1)]: 4/15, Old[obj1,obj2]: 4/45](http://upload-images.jianshu.io/upload_images/6423761-79c5ed7bd567cf32.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

动态对象年龄判断

虚拟机并不是永远的要等到对象年龄达到阈值后才能晋升到老年代,当Survivor中相同年龄(比如N)的所有对象的大小总和大于Survivor的一半的时候,那些年龄大于等于N所有对象将会直接提前进入老年代。

示例代码如下:固定堆大小为90MB,新生代45MB,其中Eden和Survivor各占15MB、15MB、15MB,未设置最大年龄阈值,使用默认值15

/**
 *  -XX:+PrintGCDetails -Xmn45m -Xms90m -Xmx90m -XX:SurvivorRatio=1 -XX:+UseParNewGC
 *
 *  Fixed Heap: 90M
 *      - Survivor *2: 15M *2
 *      - Edeb: 15M
 *      - Tenured(Old): 45M
 */
public class DynamicAge {

    public static void main(String[] args) throws Exception {
        System.gc(); //尝试清除由监测工具生成的临时对象

        byte[] obj1 = new byte[1024 * 1024 * 4]; //执行完这一行之后各区使用情况: Eden[obj1]: 4/15, S0: 0/15, S1: 0/15, Old: 0/45
        byte[] obj2 = new byte[1024 * 1024 * 4]; //执行完这一行之后各区使用情况: Eden[obj1,obj2]: 8/15, S0: 0/15, S1: 0/15, Old: 0/45
        byte[] obj3 = new byte[1024 * 1024 * 12]; //对象obj3创建成功之前,虚拟机检测到Eden剩余空间(15-8=7)不足以分配给obj3,因此触发第一次Minor GC来释放空间给obj3,obj1和obj2晋升到幸存区,年龄为1,各区使用情况: Eden[obj3]: 12/15, S0: 0/15, S1[obj1(age=1),obj2(age=1)]: 8/15, Old: 0/45
        obj3 = null; //obj3不再有任何引用关联,下次GC的时候将会被回收
        byte[] obj4 = new byte[1024 * 1024 * 4]; //对象obj4创建成功之前,虚拟机检测到Eden剩余空间(15-12=3)不足以分配给obj4,因此触发第二次MinorGC来释放空间给obj4,这次GC中由于Survivor区中的obj1和obj2的大小之和8超过了Survivor大小15的一半,所以这两个对象将被提前晋升到老年代,而对象obj3由于没有任何引用,直接被回收了,之后各区使用情况: Eden[obj4]: 4/15, S0: 0/15, S1: 0/15, Old[obj1,obj2]: 8/45
     
    }

}

Debug逐行执行上面4个对象的创建语句,每个对象创建成功后的各区使用情况如下各图:
![对象obj1创建之后各区使用情况: Eden[obj1]: 4/15, S0: 0/15, S1: 0/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-d1da3806cf24547b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj2创建之后各区使用情况: Eden[obj1,obj2]: 8/15, S0: 0/15, S1: 0/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-b7184c060f87a5d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj3创建成功之前,虚拟机检测到Eden剩余空间(15-8=7)不足以分配给obj3,因此触发第一次Minor GC来释放空间给obj3,obj1和obj2晋升到幸存区,年龄为1,各区使用情况: Eden[obj3]: 12/15, S0: 0/15, S1[obj1(age=1),obj2(age=1)]: 8/15, Old: 0/45](http://upload-images.jianshu.io/upload_images/6423761-e51bf980e8676d9d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![对象obj4创建成功之前,虚拟机检测到Eden剩余空间(15-12=3)不足以分配给obj4,因此触发第二次MinorGC来释放空间给obj4,这次GC中由于Survivor区中的obj1和obj2的大小之和8超过了Survivor大小15的一半,所以这两个对象将被提前晋升到老年代,而对象obj3由于没有任何引用,直接被回收了,之后各区使用情况: Eden[obj4]: 4/15, S0: 0/15, S1: 0/15, Old[obj1,obj2]: 8/45](http://upload-images.jianshu.io/upload_images/6423761-cf6a06679aeb7c33.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

至此,关于对象在堆内各区分配的几种情况就大致讲解到这里。下一章将了解一下垃圾收集器的原理。

上一篇:理解JVM(3)- 运行时数据区

时间: 2024-12-24 11:10:10

理解JVM(4)- 堆内存的分代管理的相关文章

[jjzhu学java]深入理解JVM笔记之内存管理机制

深入理解JVM笔记之内存管理机制 运行时数据区域 程序计数器 JVM栈 本地方法栈 Java堆 方法区 运行时常量池 直接内存 对象访问 OutOfMemoryError异常 Java堆溢出示例 JVM栈和本地方法栈溢出 运行时常量池溢出 本机直接内存溢出 深入理解JVM笔记之内存管理机制 运行时数据区域 程序计数器 每个线程都有一个程序计数器(PC),是当前线程所执行的字节码的行号指示器,通过改变程序计数器的值来选取下一条指令.各线程之间的计数器互不影响,是线程私有的内存. 如果线程执行的是一

centos中修改tomcat中JVM非堆内存默认配置解决内存溢出

系统CentOS6.4下yum安装了tomcat6和jdk1.6,安装配置过程如下: http://www.111cn.net/sys/CentOS/72007.htm tomcat中部署两个项目A.B,同时部署时报内存溢出错误,系统CPU负载飙升,而单独部署A.B 和 只部署多个A或者只部署多个B项目系统运行正常. 查看日志报错:OutOfMemoryError: PermGen space-.   查询资料得知:是非堆溢出(永久保存区域溢出) 这种错误常见在web服务器对JSP进行pre c

jvm 非堆内存是否可gc

问题描述 如题:各位大侠,我在网上看了很多帖子,各执一词,欢迎讨论! 解决方案 解决方案二:GC不会在主程序运行期对PermGenSpace进行清理解决方案三:应该不会的解决方案四:非堆内存有gc的必要吗?栈里的数据随着方法的调用而产生,又随着方法的退出而释放内存,整个main方法退出之后,所有的栈内存都释放了解决方案五:引用3楼nokiaisacat的回复: 非堆内存有gc的必要吗?栈里的数据随着方法的调用而产生,又随着方法的退出而释放内存,整个main方法退出之后,所有的栈内存都释放了 +1

JVM 分代GC策略分析

我们以Sun HotSpot VM来进行分析,首先应该知道,如果我们没有指定任何GC策略的时候,JVM默认使用的GC策略.Java虚拟机是按照分代的方式来回收垃圾空间,我们应该知道,垃圾回收主要是针对堆(Heap)内存进行分代回收,将对内存可以分成新生代(Young Generation).年老代(Tenured Generation)和永久代(Permanent Generation)三个部分. 分代GC 分代GC包括如下三代: 新生代(Young Generation) 新生代有划分为Ede

jvm中堆和非堆的划分,请不要从别处粘帖内容或者发链接给我,看清题目再回答

问题描述 jvm中堆和非堆的划分,请不要从别处粘帖内容或者发链接给我,看清题目再回答 哪位大神能给我讲解一个问题,我们使用参数-Xms -Xmx设置堆内存的最小值和最大值,-XX:PermSize -XX:MaxPermSize设置非堆内存的最小值和最大值,这里的非堆内存包括Permanent Space(永久存储区).但是为什么又说jvm中的堆内存分为三部分:Permanent Space 永久存储区.Young Generation Space 新生区.Tenure generation s

详解Java的堆内存与栈内存的存储机制_java

堆与内存优化    今天测了一个项目的数据自动整理功能,对数据库中几万条记录及图片进行整理操作,运行接近到最后,爆出了java.lang.outOfMemoryError,java heap space方面的错误,以前写程序很少遇到这种内存上的错误,因为java有垃圾回收器机制,就一直没太关注.今天上网找了点资料,在此基础上做了个整理.  一.堆和栈     堆-用new建立,垃圾回收器负责回收          1.程序开始运行时,JVM从OS获取一些内存,部分是堆内存.堆内存通常在存储地址的

JVM内存管理:GC算法精解---分代搜集算法

引言 何为终极算法? 其实就是现在的JVM采用的算法,并非真正的终极.说不定若干年以后,还会有新的终极算法,而且几乎是一定会有,因为LZ相信高人们的能力. 那么分代搜集算法是怎么处理GC的呢? 对象分类 上一章已经说过,分代搜集算法是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生.与其说分代搜集算法是第四个算法,不如说它是对前三个算法的实际应用. 首先我们来探讨一下对象的不同特性,接下来LZ和各位来一起给这些对象选择GC算法. 内存中的对象按照生命周期的长短大致可以分为三种

Java虚拟机:JVM内存分代策略

Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代.老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略. 为什么要分代? 堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中.给堆内存分代是为了提高对象内存分配和垃圾回收的效率.试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费

深入理解Java之JVM堆内存分配

Java堆是被所有线程共享的一块内存区域,所有对象和数组都在堆上进行内存分配.为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代.老年代和永久代(1.8中无永久代,使用metaspace实现)三块区域. Java把内存分成两种:栈内存和堆内存.关于堆内存和栈内存的区别与联系.简单的来讲,堆内存用于存放由new创建的对象和数组,在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.而栈内存由使用的人向系统申请,申请人进行管理. 堆内存初始化 Java中分配堆内存是自动初始化的,其入口位于Univ