在 Java 中,堆被划分成两个不同的区域:年轻代,也叫新生代 ( Young )和老年代 ( OldGen )。而方法区中对应的则是永久代(PermPen),本文详解JVM内存中这三块区域。
Hotspot JVM堆内存结构图如下所示:
从上图可以看出,年轻代中, Eden区、SurvivorFrom区和SurvivorTo区默认比例为8:1:1,即: Eden = 8/10 的年轻代空间大小,from = to = 1/10 的年轻代空间大小。年轻代发生的GC成为Minor GC,老年代发生的GC叫Major GC,Full GC的区域为年轻代和老年代。
注意:
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
年轻代和老年代默认比例为1:2,即,年轻代=1/3堆内存空间,老年代=2/3堆内存空间。
一、年轻代(Young)
也叫新生代,顾名思义,主要是用来存放新生的对象。
A. 新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中)。
B. 在Minor GC开始的时候,对象只会存在于Eden区和Survivor from区,Survivor to区是空的。
C. Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空。
D. “From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。
为什么有 From和To 2块区域?
这就要说到新生代Minor GC的算法了:复制算法
把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了。优点是避免内存碎片。
二、老年代(OldGen)
随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除算法或者标记压缩算法。
标记清除:
- 首先会从GC root进行遍历,把可达对象(存过的对象)打标记。
- 再从GC root二次遍历,将没有被打上标记的对象清除掉。
优点:
老年代对象一般是比较稳定的,相比复制算法,不需要复制大量对象。之所以将所有对象扫描2次,看似比较消耗时间,其实不然,是节省了时间。举个栗子,数组 1,2,3,4,5,6。删除2,3,4,如果每次删除一个数字,那么5,6要移动3次,如果删除1次,那么5,6只需移动1次。
缺点:
这种方式需要中断其他线程(STW),相比复制算法,可能产生内存碎片。
标记压缩:和标记清除算法基本相同,不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩,这样就可以解决内存碎片问题。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
三、永久代(PermPen)
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间,Metaspace)的区域所取代。
值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度。
四、JDK8废弃永久代迎来元空间(Metaspace)
1.关于方法区和永久代:
在HotSpot JVM中,这次讨论的永久代,就是上图的方法区(JVM规范中称为方法区)。《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。在其他JVM上不存在永久代。
JDK8 永久代变化如下图:
1.新生代:Eden+From Survivor+To Survivor
2.老年代:OldGen
3.永久代(方法区的实现) : PermGen—–>替换为Metaspace(本地内存中)
2.为什么要废除永久代?
参照JEP122:http://openjdk.java.net/jeps/122 ,原文截取:
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
3.现实使用中易出问题
由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。
4.理解元空间
元空间是方法区的在HotSpot jvm 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
常用配置参数如下,配置形式都是通过-XX:MetaspaceSize=8m这种形式:
1.MetaspaceSize
初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用 -XX:+PrintFlagsInitial命令查看本机的初始化参数。
2.MaxMetaspaceSize
限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
3.MinMetaspaceFreeRatio
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
4.MaxMetasaceFreeRatio
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。
5.MaxMetaspaceExpansion
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
6.MinMetaspaceExpansion
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB)。
由于JDK8中永久代的废除,要注意以下两点:
1.字符串常量由永久代转移到堆中。
2.永久代已不存在,PermSize 和 MaxPermSize参数已移除。