JVM内存分配与回收

时间:2022-06-23 22:06:13

JVM内存分配

Java虚拟机在运行时将所管理的内存区域分为以下五个部分:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。其中,程序计数器、Java虚拟机栈与本地方法栈这三个区域为线程私有,随线程产生和消亡,不需要过多的考虑内存的回收问题,Java堆是Java垃圾收集器管理的最主要区域,故也被称为“GC堆”

JVM内存分配与回收

程序计数器:一块较小的内存区域,是当前所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器,各个线程间的程序计数器互不影响,因此该区域是线程私有的

Java虚拟机栈:Java虚拟机栈是描述Java方法执行的内存模型,用于支持虚拟机进行方法调用和方法执行,每个方法在被执行的时候都会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息

本地方法栈:该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为本地操作系统(Native)方法服务

Java堆:Java堆是Java虚拟机所管理的内存区域中最大的一块,是所有的线程共享的一块区域,几乎所有的对象实例与数组都存储在这块区域

方法区:方法区它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,方法区也被称为“永久代”,也是各个线程共享的内存区域

对内存分配额情况分析最常见的示例便是对象的实例化,以Object obj=new Object()为例, 这段代码的执行会涉及java虚拟机栈、Java堆、方法区三个最重要的内存区域。obj会作为引用类型(reference)的数据保存在Java栈的本地变量表中,而会在Java堆中保存该引用的实例化对象,另外Java堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中

JVM内存分配与回收

JVM内存回收

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

单纯从JVM的功能考虑,并不需要年轻代,完全可以针对整个堆进行操作。年轻代存在的唯一理由是优化垃圾回收(GC)的性能。更具体说,把堆划分为年轻代和年老代有2个好处:简化了新对象的分配(只在年轻代分配内存),可以更有效的清除不再需要的对象(即死对象)( 年轻代和年老代使用不同的GC算法)

JVM内存分配与回收

简单讲,就是生命期短的对象放在一起,将生命期长的对象放在一起,分别采用不同的回收策略。生命期短的对象回收频率比较高,生命期长的对象回收频率比较低,生命期短的对象被尝试回收几次发现还存活,则被移到另外一个地方去存起来

基于大多数新生对象都会在GC中被收回的假设。新生代的GC 使用复制算法。在GC前To 幸存区(survivor)保持清空,对象保存在 Eden 和 From 幸存区(survivor)中,GC运行时,Eden中的幸存对象被复制到 To 幸存区(survivor)。针对 From 幸存区(survivor)中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold),对象会被复制到To 幸存区(survivor)。如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和From 幸存区中只保存死对象,可以视为清空。如果在复制过程中To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To幸存区会调换下名字,在下次GC时,To 幸存区会成为From 幸存区

现在应该能理解为什么新生代大小非常重要了,如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。我们需要找到一个合适大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优

JVM内存分配与回收


通过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的空间比例18

原始内存图

JVM内存分配与回收

① alloc1 = new byte[tenMB /5];

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

JVM内存分配与回收

alloc2 = new byte[5 * tenMB];

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


JVM内存分配与回收

③alloc3 = new byte[4 * tenMB]

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

JVM内存分配与回收

edengc后腾出空间可以存放allocation340M对象,则alloc3分配40M对象

JVM内存分配与回收④ alloc3 = null
edenalloc3分配的的40M对象则变成可被回收状态

JVM内存分配与回收

 allocation3 = new byte[6 * tenMB]

尝试先在eden区上分配,发现超出了eden区域的容量,则再次触发新生代上的一次gc。首先eden上分配的40M对象因为没有被再使用,则直接被回收。而根据的设置不同,这次gc的行为会稍有不同,先看MaxTenuringThreshold不设置,即取默认值15的时候。eden区上无用的40M回收后,再考察Survivor区域的对象是否满足对象晋升老年代的年龄阈值,发现from中的2M对象,年龄是1,不满足晋升条件,则不被处理,只是把Survivor区域的经历这次回收未被处理的对象age加一,即新的age2

JVM内存分配与回收在年轻代上gc后腾出空间后,新的alloc360M空间被分配到eden区域上
JVM内存分配与回收

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

JVM内存分配与回收

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

JVM内存分配与回收