最简单例子图解JVM内存分配和回收

原文链接:最简单例子图解JVM内存分配和回收

一、简介

JVM采用分代垃圾回收。在JVM的内存空间中把堆空间分为年老代和年轻代。将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。年轻代中又被分为Eden区(圣经中的伊甸园)、和两个Survivor区。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到年老区。

 

简单讲,就是生命期短的对象放在一起,将少数生命期长的对象放在一起,分别采用不同的回收策略。生命期短的对象回收频率比较高,生命期长的对象采用比较低回收频率,生命期短的对象被尝试回收几次发现还存活,则被移到另外一个地方去存起来。就像现在夏天了,勤劳的doumadoudoudouba常穿的衣服放在顺手的地方,把冬天的衣服打包放在柜子另一个地方。虽然把doudou的小衣服类比成虚拟机里的对象有点不合适,大致意思应该就是这样。

 

本文中通过最简单的一个例子来demo下这个过程,代码很短,很简单,希望剖析的细一点,包括每一步操作后对象的分配和回收对内存堆产生的影响。设定上包括对堆中年轻代(年轻代中eden区和survivor区)、年老代大小的设定,以及设置阈值控制年轻代到年老代的晋升。

二、示例代码

下面是最简单的代码,通过代码的每一步的执行来剖析其中的规则。


01 package com.idouba.jvm.demo; 

02   

03 /** 

04  * @author idouba 

05  * Use shortest code demo jvm allocation, gc, and someting in gc. 

06  * 

07  * In details 

08  * 1) sizing of young generation (eden space,survivor space),old generation.

09  * 2) allocation in eden space, gc in young generation, 

10  * 3) working with survivor space and with old generation. 

11  * 

12  */ 

13 public class SimpleJVMArg { 

14   

15     /** 

16      * @param args 

17      */ 

18     public static void main(String[] args) 

19     { 

20         demo(); 

21     } 

22   

23     /** 

24      * VM arg:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution 

25      * 

26      */ 

27     @SuppressWarnings("unused") 

28     public static void demo() { 

29   

30         final int tenMB = 10* 1024 * 1024; 

31   

32         byte[] alloc1, alloc2, alloc3; 

33   

34         alloc1 = new byte[tenMB / 5]; 

35         alloc2 = new byte[5 * tenMB]; 

36         alloc3 = new byte[4 * tenMB]; 

37         alloc3 = null; 

38         alloc3 = new byte[6 * tenMB]; 

39     } 

40 }

三、执行输出

通过jvm 参数设定几个区域的大小,结合代码执行可以观察到对象在堆上分配和回收的过程。执行参数如下:

-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+PrintTenuringDistribution

通过设这-Xms200M -Xmx200M 设置Java堆大小为200M,不可扩展,-Xmn100M设置其中100M分配给新生代,则200-100=100M,即剩下的100M分配给老年代。-XX:SurvivorRatio=8设置了新生代中eden与survivor的空间比例是1:8。

执行上述代码结果如下:

[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age   1:    2237152 bytes,    2237152 total
: 54886K->2184K(92160K), 0.0508477 secs] 54886K->53384K(194560K), 0.0508847 secs] [Times: user=0.03 sys=0.03, real=0.06 secs]
[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age   2:    2237008 bytes,    2237008 total
: 43144K->2184K(92160K), 0.0028660 secs] 94344K->53384K(194560K), 0.0028957 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 92160K, used 65263K [0x1a1d0000, 0x205d0000, 0x205d0000)
  eden space 81920K,  77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000)
  from space 10240K,  21% used [0x1f1d0000, 0x1f3f2250, 0x1fbd0000)
  to   space 10240K,   0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000)
 tenured generation   total 102400K, used 51200K [0x205d0000, 0x269d0000, 0x269d0000)
   the space 102400K,  50% used [0x205d0000, 0x237d0010, 0x237d0200, 0x269d0000)
 compacting perm gen  total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000)
   the space 12288K,   2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000)
    ro space 8192K,  66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000)
    rw space 12288K,  52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)

从中可以看到eden 大小为81920K, Survivor中from区域和to区域大小都是10240k。新生代总的92160K指的是eden和一个Survivor区域的和。

即原始的内存如图:

为了演示年轻代对象晋级到年老代的过程。需要设置一个VM参数, 这里设置MaxTenuringThreshold=1。前面不设置的时候,默认MaxTenuringThreshold取值15。当设置不同的阈值,jvm在内存处理会有不同。我们重点观察观察alloc1 这么小块区域在不同的MaxTenuringThreshold参数设置下的遭遇。

这时候JVM的参数中加上MaxTenuringThreshold=1如下:

-verbose:gc  -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

可以看到输出结果是:

[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 1)
- age   1:    2237152 bytes,    2237152 total
: 54886K->2184K(92160K), 0.0641037 secs] 54886K->53384K(194560K), 0.0641390 secs] [Times: user=0.03 sys=0.03, real=0.06 secs]
[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 1)
: 43144K->0K(92160K), 0.0036114 secs] 94344K->53384K(194560K), 0.0036418 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
 def new generation   total 92160K, used 63078K [0x1a1d0000, 0x205d0000, 0x205d0000)
  eden space 81920K,  77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000)
  from space 10240K,   0% used [0x1f1d0000, 0x1f1d0000, 0x1fbd0000)
  to   space 10240K,   0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000)
 tenured generation   total 102400K, used 53384K [0x205d0000, 0x269d0000, 0x269d0000)
   the space 102400K,  52% used [0x205d0000, 0x239f2260, 0x239f2400, 0x269d0000)
 compacting perm gen  total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000)
   the space 12288K,   2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000)
    ro space 8192K,  66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000)
    rw space 12288K,  52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)

四、过程解析

下面观察每一步语句执行后,jvm内存的变化情况,并给出解析。

1)在执行第一个语句,alloc1分配2M空间

查看源代码

打印帮助

1 alloc1 = new byte[tenMB / 5];

后,根据分代策略,在新生代的eden区分配2M的空间存储对象。

2)在执行第二语句,alloc2分配50M

查看源代码

打印帮助

1 alloc2 = new byte[5 * tenMB];

前面alloc1分配2M后,因为eden的80M空间还有80-2=78M还可以容纳下allocation2要求的50M空间,因此接着在eden区域分配。

3)当执行第三句,alloc3分配40M

查看源代码

打印帮助

1 alloc3 = new byte[4 * tenMB];

还是尝试在eden上分配,但是eden空间只剩下28M,不能容纳alloc3要求的40M空间。于是触发在新生代上的一次gc,将Eden区的存活对象转移到Survivor区。在这个里先将2M的alloc1对象存放(其实是copy,参见java 垃圾回收策略的描述)到from区,然后copy 50M的alloc2对象,显然survivor区不能容纳下alloc2对象,该对象被直接copy到年老代。需要说明的是复制到Survivor区的对象在经历一次gc后期对象年龄会被加一。

在eden区gc后腾出空间可以存放allocation3的40M对象,则alloc3分配40M对象如图:

4)执行第四句,将alloc3置空

查看源代码

打印帮助

1 alloc3 = null;

这是eden上alloc3分配的的40M对象则变成可被回收状态。

5)执行第5句,对alloc重新分配60M空间

查看源代码

打印帮助

1 allocation3 = new byte[6 * tenMB];

还是尝试先在eden区上分配,发现超出了eden区域的容量,则再次触发新生代上的一次gc。首先eden上分配的40M对象因为没有被再使用,则直接被回收。而根据的设置不同,这次gc的行为会稍有不同。

先看MaxTenuringThreshold不设置,即取默认值15的时候。eden区上无用的40M回收后,再考察Survivor区域的对象是否满足对象晋升老年代的年龄阈值,发现from中的2M对象,年龄是1,不满足晋升条件,则不被处理,只是把Survivor区域的经历这次回收未被处理的对象age加一,即新的age为2.如图:

通过输出日志也显示:经过这次回收年轻代大小,由43114K变为2184k,总的大小由94344k变为53384k,即反映出回收了40M无用对象。

Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age   2:    2237008 bytes,    2237008 total
: 43144K->2184K(92160K), 0.0028660 secs] 94344K->53384K(194560K), 0.0028957 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

在年轻代上gc后腾出空间后,新的alloc3的60M空间被分配到eden 区域上。分配后堆如下:

以上是不设置晋升阈值MaxTenuringThreshold情况下进行的gc,以及gc后alloc3的分配。

再看看当MaxTenuringThreshold设置为1的情况。同样eden区上无用的40M回收后,再考察Survivor区域的对象是否满足对象晋升老年代的年龄阈值,发现from中的2M对象,年龄是1,满足晋升条件,则Survivor区域满足年龄的对象被拷贝到年老区。

通过日志显示年轻代的大小被清0了,表示survivor的存活对象因为满足晋升条件被移到被移到年老代了。

[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 1)
: 43144K->0K(92160K), 0.0036114 secs] 94344K->53384K(194560K), 0.0036418 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

同样的,gc完后会在eden上分配空间来存储alloc3对象,这种情况下堆结构如图:

 

比较上面两个图,发现差别就仅仅在于survivor中的2M对象是否被认为生存时间足够长科院被移到年老代中去。从上面日志高亮部分from区域的最终存储也可反映出了这个差别。

比较前面两个日志可以看到:总的大小和上面设置和不设置MaxTenuringThreshold(其实是MaxTenuringThreshold设置1还是15)没有关系,都是由94344k变为53384k,即都是回收了40M eden区域无用对象。第N次gc时存活的满足晋升条件则由survivor移到年老代,不满足的还留在survivor区域,堆的总的大小没有变。

五、最后

上面通过最简单的例子示意了下在jvm堆上对象是如果分配的,当空间不足时,是如何调整回收的。希望可以对jvm的堆上结构和gc思路有个基本的了解。当然相关参数(其实反映的是机制)远比这个复杂,有挺多细节,更多的是在实践中来体会。

时间: 2024-10-31 10:22:00

最简单例子图解JVM内存分配和回收的相关文章

Android的内存分配与回收

  想写一篇关于android的内存分配和回收文章的想法来源于追查一个魅族手机图片滑动卡顿问题,我们想了很多办法还是没有避免他不停的GC,所以就打算详细的看看内存分配和GC的原理,为什么会不断的GC,GC ALLOC和GC COCURRENT有什么区别,能不能想办法扩大堆内存减少GC的频次等等. 1.JVM内存回收机制 1.1 回收算法 标记回收算法(Mark and Sweep GC)         从"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots

【转载】JVM内存分配与调优参数列表

本文转载自http://shift-alt-ctrl.iteye.com/blog/1842631   一.运行时数据区:   程序计数器:它是一块较小的内存空间,主要作用是当前线程所执行的字节码的行号指示器.由于java虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的(协作式/抢占式?!),即任何时刻,任一CPU只会正在处理一个线程的指令;为了确保线程切换后能够正确恢复执行的位置,每个线程都有一个独立的程序计数器,每个计数器为线程私有.如果线程正在执行java方法,那么此计数器记

jvm 内存与垃圾回收

你对JVM内存组成结构和JVM垃圾回收机制是否熟悉,这里和大家简单分享一下,希望对你的学习有所帮助,首先来看一下JVM内存结构,它是由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示. JVM学习笔记 JVM内存管理和JVM垃圾回收 JVM内存组成结构 JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,

JAVA虚拟机内存分配与回收机制

Java虚拟机(Java Virtual Machine) 简称JVM Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现.Java虚拟机有自己想象中的硬件,如处理器.堆栈.寄存器等,还具有相应的指令系统. Java把内存划分成两种:一种是栈内存,一种是堆内存. 在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配. 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存

浅谈CLR的内存分配和回收机制

相对于C++程序员来说,C#程序员是非常幸运的,至少我们不需要为内存泄漏(Memory Leak)而头疼,不需要负责内存的分配和回收.但这不意味着我们只需要知道new的语法 就可以了,作为一个严肃的C#程序员,我们应该对此有所了解,有助于我们编写性能更好 的代码. 主要内容: CLR的内存分配机制 CLR的回收机制 一.CLR的内存分配机制 .NET Framework 的垃圾回收器管理应用程序的内存分配和释放.每次使用 new 运算 符创建对象时,运行库都从托管堆为该对象分配内存.只要托管堆中

JVM的内存分配与垃圾回收策略

自动内存管理机制主要解决了两个问题: 给对象分配内存以及回收分配给对象的内存. 垃圾回收的区域 前面的笔记中整理过虚拟机运行数据区,再看一下这个区域: 注意在这个Runtime Data Area中: 程序计数器.Java栈.本地方法栈3个区域随线程而生,随线程而灭: 每一个栈帧中分配多少内存基本上在类结构确定下来的时候就已知, 因此这几个区域的内存分配和回收都具有确定性,不需过多考虑回收问题,方法结束或者线程结束时,内存自然就随之回收了. Java堆和方法区Method Area则不一样, 一

Android 优化二 Java内存分配机制及内存泄漏

Java内存分配机制及内存泄漏目录介绍 1.JVM内存管理 1.1 JVM内存管理图 1.2 Java采用GC进行内存管理. 2.JVM内存分配的几种策略 2.1 静态的 2.2 栈式的 2.3 堆式的 2.4 堆和栈的区别 2.5 得出结论 2.6 举个例子 2.7 调用 System.gc();进行内存回收 3.GC简单介绍 3.1 内存垃圾回收机制 3.2 关于GC介绍 3.3 如何监听GC过程 3.4 GC过程与对象的引用类型关系 4.内存泄漏简单介绍 4.1 内存泄漏的定义 4.2 内

JVM内存管理及GC机制

一.概述 Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢.经过这么长时间的发展,Java GC机制已经日臻完善,几乎可以自动的为我们做绝大多数的事情. 虽然java不需要开发人员显示的分配和回收内存,这对开发人员确实降低了不少编程难度,但也可能带来一些副作用: 1. 有可能不知不觉浪费了很多内存 2. JVM花

Tomcat中JVM内存溢出及合理配置

Tomcat本身不能直接在计算机上运行,需要依赖于硬件基础之上的操作系统和一个Java虚拟机.Tomcat的内存溢出本质就是JVM内存溢出,所以在本文开始时,应该先对Java JVM有关内存方面的知识进行详细介绍. 一.Java JVM内存介绍 JVM管理两种类型的内存,堆和非堆.按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Java 虚拟机启动时创建的.""在JVM中堆之外的内存称为非堆内存(Non-heap