JVM内存管理机制说到底就是为了解决两个问题:给对象分配内存以及回收分配给对象的内存。
Java Heap被分为两部分:Young Generation 和 Old Gereration。Perm并不属于Heap。
Young Generation (Young Gen)
所有的new出来的对象都放在Young Gen,当Young Gen满了, 就会执行Garbage Collection (GC), 此时的GC称为Minor GC。 Young Gen被分成三部分:Eden Memory和两个Survivor Memory。
- 多数情况下,对象都在新生代Eden区中分配,但一些大对象可能会直接进入到老年代。虚拟机提供了一个
-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接进老年代分配,这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存拷贝。 - Eden满了怎么办?JVM就会执行Minor GC。被引用的对象都会存活下来,它们将被移到Survivor区域里,也就是 图中的S0或S1。
- 同一时间的两个Survivor区,一个用来保存对象,另一个是空的;每次进行Minor GC垃圾回收时,就把Eden,From的可达对象复制到To区域中,一些生存时间长的就复制到老年代,接着就清除Eden,From空间,最后把原来的To空间变为From空间,原来的From空间变为To空间。( 有点类似于双缓冲队列 )
- 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并进过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivro区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置。 - 动态对象年龄判定:为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
- Young区域大部分对象 朝生夕灭,因此Minor GC回收频率高且回收速度很快。
- 新生代采用 复制算法 。
Old Generation(Tenured Gen)
- 回收机制:采用标记压缩算法回收垃圾。
- 对象来源:
- 大对象直接进入老年代
- Young代中生存时间长的可达对象。
- 回收频率:因为很少对象会死掉,所以执行频率不高,而且需要较长时间来完成。
Permanent Generation(永久代)
绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟 “PermGen space”的内存溢出:
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class PermGenOomMock{
public static void main(String[] args) {
URL url = null;
List<ClassLoader> classLoaderList = new ArrayList<>();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("java.lang.Object");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.concurrent.locks.ReentrantLock.<init>(ReentrantLock.java:262)
at java.util.concurrent.ConcurrentHashMap$Segment.<init>(ConcurrentHashMap.java:425)
at java.util.concurrent.ConcurrentHashMap.<init>(ConcurrentHashMap.java:825)
at java.util.concurrent.ConcurrentHashMap.<init>(ConcurrentHashMap.java:869)
at java.lang.ClassLoader.<init>(ClassLoader.java:281)
at java.lang.ClassLoader.<init>(ClassLoader.java:334)
at java.security.SecureClassLoader.<init>(SecureClassLoader.java:99)
at java.net.URLClassLoader.<init>(URLClassLoader.java:140)
at com.boothsun.jvm.PermGenOomMock.main(PermGenOomMock.java:17)
本例中使用的 JDK 版本是 1.7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 "java.lang.OutOfMemoryError: PermGen space " 异常了。这里之所以采用 JDK 1.7,是因为在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。
MetaSpace(元空间)
JDK1.8 永久代的废弃
JDK1.8 永久代变化如下图:
- 新生代:Eden + From Survivor + To Survivor
- 老年代:OldGen
- 永久代(方法区的实现):PermGen ---> 替换为Metaspace(本地内存中)
移除永久代的原因
- 移除永久代是为融合HotSpot JVM与JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
- 永久代大小不确定,PermSize指定的太小很容易造成永久代OOM,因为PermSize的大小很依赖于很多因素,比如JVM加载的class总数,常量池的大小,方法的大小等。
元空间的内存大小
元空间时方法区的具体实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常称为“非堆”。
元空间的本地和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
常用配置参数
-
MetaspaceSize
初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。 -
MaxMetaspaceSize
限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。 -
MinMetaspaceFreeRatio
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。 -
MaxMetasaceFreeRatio
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。 -
MaxMetaspaceExpansion
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。 -
MinMetaspaceExpansion
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。