JVM是如何分配和回收内存?有实例! - zhanjindong

标签: jvm 回收 内存 | 发表时间:2014-06-02 00:24 | 作者:zhanjindong
出处:

上一篇博客我简单介绍了下如何手动计算一个Java对象到底占用多少内存?今天就想聊下这个内存JVM到底是是如何分配和回收的。

Java整体来说还是一个GC比较友好的语言,无论是分代的垃圾收集,还是基于GC Roots的可达性算法都是业界普遍的经典做法,关于Java的内存区域划分以及GC的一些基本知识,我这里就不赘述了,可以看我之前的博客: http://zhanjindong.info/category/note/dsbj/

《深入理解Java虚拟机第2版》这本书非常值得一看,最近几篇读博客都算这本书的读书笔记吧。本人文笔很烂,所以都是记流水账枯燥乏味的文章。进入正文之前还是要交代下环境:以下内容都是基于HotSpot虚拟机Server模式,垃圾收集器用的是默认的Serial和Serial Old。

 

JVM内存分配和回收策略

话说一图胜千言,本也打算画张活动图就了事了:

但是画完发现:一画图更麻烦,太大了看的累(想看的可以在新建窗口放大了看),其次感觉还是说不清楚(画的不对的地方欢迎批评),最后觉得还是文字描述一下整个流程:

1、当JVM给一个对象分配内存的时候, 如果启动了本地线程分配缓存,将按线程优先在TLAB上分片,TLAB只是起缓存作用减少高并发下CAS带来的性能损失,跟GC的分代没有冲突。

2、当分配一个对象的时候会优先在Eden区域分配,如果Eden有足够的空间,那么内存分配很顺利的结束,不会触发任何GC操作;

3、当Eden区域空间不足的时候,会尝试着进行一次Minor GC,之所以说尝试是因为在进行Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象空间总和,如果是的那么可以保证这次Minor GC顺利进行;否则,虚拟机会检查HandlePromotionFailure这个参数是否设置为允许担保失败,如果允许那么虚拟机会根据经验值(这个经验值是历次晋升到老年代对象的平均大小)来决定是否尝试这次GC,如果小于或者JVM觉得不能冒险,那么会进行一次Full GC;

4、Minor GC时会采用复制算法将所有存活的对象复制到Survivor空间中(既包括Eden区域存活的对象,也包括另外一个Survivor存活下来的对象),如果这时发现Survivor空间不足,那么这些存活对象会直接进入老年代,这就是“空间分配”担保,前面说到冒险,是因为老年代的空间仍有可能不够,这时还是要进行一次Full GC,但是除了极端情况,大部分时候通过担保还是能有效避免频繁的Full GC的,如果Full GC后仍然没有足够空间,那只能抛出OutOfMemoryError;

5、对象在Eden空间出生,经过第一次Minor GC后能够顺利的被转移到Survivor的话,那么它的GC年龄就变成1,以后每在Survivor中熬过一次Minor GC,年龄就增加1,直到超过一定程度(-XX:MaxTenuringThreshold,默认15岁)则晋升到老年代;

6、规则是死的,人是活的,虚拟机开发人员还想到了一个“动态对象年龄判定”算法:如果Survivor区域中相同年龄所有对象大小总和超过Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进去老年代;

7、对象也可以直接分配在老年代,这主要是针对那些大对象,因为大对象的内存分配代价比较大(需要连续的内存空间),所以JVM提供了-XX:PretenureSizeThreshold这个参数。

 

为什么有两块survivor区域

我一开始很纳闷HotSpot虚拟机为什么要搞出两个Survivor区域内,只用一块有何不妥吗?最后在Stack Overflow找到一个答案:

http://stackoverflow.com/questions/10695298/java-gc-why-two-survivor-regions 按照里面的说法是为了减少虚拟机对内存碎片的处理,我想了半天我的理解是:

因为survivor中的对象在达到“老年”(-XX:MaxTenuringThreshold)之前肯定有对象已经变成“垃圾”了,这时候必须要对其进行回收,如果只使用一个survivor的话,那么要不容忍survivor存在内存碎片,要么要对其进行内存整理,出于和对Eden区域同样的考虑,所以实际上对Survivor的GC也是基于复制算法的,不过是从一个Survivor到另外一个Survivor(这也是GC日志中为什么叫from space和to space),所以Survivor的两个区是对称的,没有先后关系,所以Survivor区中可能同时存在从Eden复制过来对象,以及从前一个Survivor复制过来的对象,某一次GC结束时肯定会有一个Survivor是空的。

 

实例说明

以上都是理论,下面结合一小段代码简单演示下上面的内容,这段代码引自《深入理解Java虚拟机第2版》3.6.3节,我简单展开说明下。为了方便解释,我先把设置的虚拟机参数贴出来:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=5242880
  • -XX:+UseSerialGC这里使用默认的Serial GC进行说明;
  • -verbose:gc是为了打印出GC日志;
  • -Xms初始Java堆为20M;
  • -Xmx20M JVM最大使用的堆为20M不可扩展;
  • -Xmn10M,新生代的内存空间为10M;
  • -XX:SurvivorRatio=8 Eden与两个Survivor的比例是8:1:1;
  • XX:PretenureSizeThreshold直接在老年代区域分配对象的阈值为5M,其实可以不设置,这里为了明确变量。

测试代码如下:

我们一步步来看,首先只执行48,49两行:

allocation1 = new byte[_1MB / 2];
allocation2 = new byte[6 * _1MB];

通过GC日志发现没有发生任何GC,Eden区域够用(注意:关于一个Java对象导致占用多大内存,参看 我前一篇博客。),接着执行51,52两行:

allocation2 = null;
allocation3 = new byte[2 * _1MB];

可以看到了引发了一次GC,这是一次Minor GC,同时可以看到使用的垃圾收集器是默认的( Def,关于GC日志的理解,可以参看《深入理解Java虚拟机第2版》一书)。引发GC的原因是Eden已经没有足够的内存容纳allocation3对象,发生GC之后allocation2对象占用的内存空间被回收了,而allocation1“幸存”下来被转移到了from survivor区域。

接下来我们再执行57,58,59三行代码:

allocation4 = new byte[4 * _1MB];
allocation3 = allocation4 = null;
allocation5 = new byte[2 * _1MB];

第59行代码引发了第二次GC,仍然是一次Minor GC,可以看到allocation3和allocation4都被回收了,allocation5被顺利的分配到了Eden空间,但是为什么from space变成了0%而老年代区域却变成了6%,这6%应该是allocation1占用的,但是为什么跑到老年代了呢?显然它的“年龄”还没有到15岁啊。

 

啊哈!还记得吗JVM很聪明,它会“动态对象年龄判定”,从上一张图可以看到,Survivor区域已经使用超过了50%(67%),而且显然是同一年龄的对象(就一个对象嘛),所以在第二次GC的时候它晋升到了老年代,大家可以把allocation1对象分配为256kb再试试。

Ok,到目前为止都是Minor GC,想Stop-The-World很简单,我们直接分配一个很大的对象试试:

allocation6 = new byte[20*_1MB];

不仅Full GC了,而且内存溢出了,因为我们设置了-Xmx20M。同时可以看到JVM被逼急了在不同区域进行了GC,首先在新生代(Eden+Survivor0)将内存全部回收,导致对象晋升到老年代,其次在老年代和“永久代(HotSpot)”也都进行了GC,但是一点收获都没有,最后只能OOM。关于JVM的一些其他规则,比如大对象的分配,以及其他虚拟机的分配策略,就留给有兴趣的同学自己试试了。

 

今天就写到这,最后祝大家“六一快乐”……靠,看了下时间已经不是6.1,童年已经过去了!

PS:文章同步发布在我的个人博客 http://zhanjindong.info/2014/06/02/jvm-memory-and-gc/

 

 


本文链接: JVM是如何分配和回收内存?有实例!,转载请注明。

相关 [jvm 回收 内存] 推荐:

JVM是如何分配和回收内存?有实例! - zhanjindong

- - 博客园_首页
上一篇博客我简单介绍了下如何手动计算一个Java对象到底占用多少内存. 今天就想聊下这个内存JVM到底是是如何分配和回收的. Java整体来说还是一个GC比较友好的语言,无论是分代的垃圾收集,还是基于GC Roots的可达性算法都是业界普遍的经典做法,关于Java的内存区域划分以及GC的一些基本知识,我这里就不赘述了,可以看我之前的博客: http://zhanjindong.info/category/note/dsbj/.

JVM理论与实践【堆内存结构与垃圾回收】

- - ITeye博客
        在生产环境下,通常都需要对JVM进行参数优化,其中对垃圾回收器的参数优化是一个非常重要的一方面. 下面重点介绍Java的堆内存,垃圾回收算法,常用的垃圾回收器以及Java堆内存的分配策略,这些内容将作为对JVM进行垃圾回收参数优化的重要基础. 然后通过简单示例验证Java的垃圾回收机制.

jvm垃圾回收

- Cano - 淘宝共享数据平台 tbdata.org
在jvm中堆空间划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permanent Generation). 年轻代和年老代是存储动态产生的对象. 永久带主要是存储的是java的类信息,包括解析得到的方法、属性、字段等等. 我们这里讨论的垃圾回收主要是针对年轻代和年老代.

JVM内存分配

- - 移动开发 - ITeye博客
计算机内存,它算是CPU与计算机打交道最频繁的区域,所有数据都是先经过硬盘至内存,然后由CPU再从内存中获取数据进行处理,又将数据保存到内存,通过分页或分片技术将内存中的数据再flush至硬盘. 那JVM的内存结构到底是如何呢. JVM做为一个运行在操作系统上,但又独立于os运行的平台,它的内存至少应该包括象寄存器、堆栈等区域.

JVM 垃圾回收算法

- - 码蜂笔记
《深入理解Java虚拟机:JVM高级特性与最佳实践》-笔记. 垃圾回收,Garbage Collection,简称GC. 判断对象是否存活一般有两种方式:. 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收. 此方法简单,无法解决对象相互循环引用的问题.

jvm内存映像分析

- - ITeye博客
     jdk自带的jmap就是java内存映像工具,可以用于上生成堆转储快照:. 在eclipse中启动一个java类,打开jdk安装目录下的C:\Program Files\Java\jdk1.6.0_11\bin目录,双击jconsole.exe,显示连接窗口:.  ,单击pid为6920的选项,点连接进入,可以看到jvm运行时的多种参数,.

JVM内存的调优

- - ITeye博客
默认的 java 虚拟机的大小比较小,在对大数据进行处理时 java 就会报错: java.lang.OutOfMemoryError. 设置 jvm 内存的方法,对于单独的 .class ,可以用下面的方法对 Test 运行时的 jvm 内存进行设置. -Xms 是设置内存初始化的大小. -Xmx 是设置最大能够使用内存的大小(最好不要超过物理内存大小).

MAT JVM内存分析

- - 开源软件 - ITeye博客
我们使用的是 Eclipse Memory Analyzer V0.8,Sun JDK 6. 和其他插件的安装非常类似,MAT 支持两种安装方式,一种是“单机版“的,也就是说用户不必安装 Eclipse IDE 环境,MAT 作为一个独立的 Eclipse RCP 应用运行;另一种是”集成版“的,也就是说 MAT 也可以作为 Eclipse IDE 的一部分,和现有的开发平台集成.

[译] HotSpot JVM 内存管理

- - IT瘾-dev
HotSpot JVM 内存管理. 更新时间:2018-03-28. 关于 JVM 内存管理或者说垃圾收集,大家可能看过很多的文章了,笔者准备给大家总结下. 这算是系列的第一篇,接下来一段时间会持续更新. 本文主要是翻译《 Memory Management in the Java HotSpot Virtual Machine》白皮书的前四章内容,这是 2006 的老文章了,当年发布这篇文章的还是 Sun Microsystems,以后应该会越来越少人记得这家曾经无比伟大的公司了.

JVM垃圾回收(GC)原理

- kill - yiihsia[互联网后端技术]_yiihsia[互联网后端技术]
引用计数(Reference Counting). 原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数. 垃圾回收时,只用收集计数为0的对象. 此算法最致命的是无法处理循环引用的问题. 标记-清除(Mark-Sweep). 第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除.