在之前的文章中提到过方法区存放的是虚拟机已经加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。同时运行时常量池也是方法区的一部分。但是在不同的JDK版本中对方法区的实现方式存在一定的差异。
下面通过对 JDK 1.6、JDK 1.7、JDK 1.8 的运行时常量池的对比来看看各自的实现方式。
JDK 1.6
在 JDK 1.6 及之前的版本中(针对 HotSpot 虚拟机),由于常量池分配在永久代内。
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List list = new ArrayList();
int i = 0;
while(true)
{
System.out.println(i);
list.add(String.valueOf(i++).intern());
}
}
}
JDK版本号:
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01, mixed mode)
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.access.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:14)
从运行结果中可以看出,运行时常量池溢出,在 OutOfMemoryError 后面跟随的提示信息是“PermGen spacer“,说明运行时常量池属于方法区(HotSpot 虚拟机中的永久代)的一部分。
通过使用 Java VisualVM 工具观察 PermGen 内存运行轨迹:
从上图可以看出:因为程序通过使用列表保存在PernGem区域中字符串的引用,导致 PermGen 区域的内存不断飙升直至抛出 OutOfMemoryError 异常。
如果不把 String 对象 add 到 list 列表的情况:
从上图可以看出:只要 PermGen 的使用量接近或达到最大大小时就会触发 PermGen 区域的垃圾回收。
JDK 1.7
JDK 版本号:
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
通过使用 Java VisualVM 工具观察 PernGen 内存运行轨迹图:
从 PernGen 运行轨迹看出:PernGen 很平稳,说明常量池没有分配到 PernGen 内存区域中。
通过使用 Java VisualVM 工具观察堆内存运行轨迹图:
从堆内存运行轨迹可以看出:堆不不断增加,最后导致抛出 OutOfMemoryError 异常。从上图可以知道常量池是在堆上分配存储空间。
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at com.access.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:14)
从运行结果可以看出,JDK1.7 把常量池从永久代移动到Java堆中,所以在 Exception 中提示 heap 溢出。
JDK 1.8
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4265607168 (4068.0MB)
NewSize = 89128960 (85.0MB)
MaxNewSize = 1421869056 (1356.0MB)
OldSize = 62914560 (60.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
随着 JDK 8 的到来,JVM不在有 PermGen。但类的元数据信息(metadata)还在,只不过不在是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于 32 位/ 64 位系统可虚拟的内存大小。可见也不是无限的,需要配置参数。
现在大多数的类元数据分配在本地化内存中。默认情况下,类元数据分配受到可用的本机内存容量的限制(容量依然取决于你使用的是 32 位 JVM 还是 64 位操作系统的虚拟内存的可用性。在我的机子上的可用内存是 1082130432 B)。
类的元数据信息转移到Metaspace的原因:
- 字符串存放在永久代中,容易出现性能问题和内存溢出。
- PermGen 这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻。
常用配置参数:
1:MetaspaceSize
初始化 Metaspace 大小,控制元空间发生 GC 的阈值。GC 后,动态增加或降低 MetaspaceSize。在默认情况下,这个值大小根据不同的品台在 12M 到 20M 浮动,使用 Java -XX:+PrintFlagsInitial 命令查看本机的初始化参数
2:MaxMetaspaceSize
限制 Metaspace 增长上限,防止因为某些情况导致 Metaspace 无限使用本地内存,影响到其他程序。
3:MinMetaspaceFreeRatio
当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数(即实际非空闲空间占比过大,内存不够用),那么虚拟机将增加 Metaspace 的大小。默认值为 40,也就是 40%。设置该参数可以控制 Metaspace 的增长速度,太小的值会导致 Metaspace 增长缓慢,Metaspace 的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致 Metaspace 增长过快,浪费内存。
4:MaxMetaspaceFreeRatio
当进行 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比大于这个参数,那么虚拟机就会释放Metaspace的部分参数。默认值为 70,也就是 70%。
5:MaxMetaspaceExpansion
Metaspace 增长时的最大幅度。
6:MinMetaspaceExpansion
Metaspace 增长时最小幅度。
在 eclipse 中选中类设置参数:-Xms20m -Xmx20 -XX:PermSize=8m -XX:MaxPermSize=8M
运行时会提示:
ClassA类
package com.memory;
public class ClassA {
}
把 ClassA 编译成 class 文件,运行下面程序。
package com.memory;
import java.io.File;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class OOMTest {
public static void main(String[] args) {
try{
//准备url
URL url = new File("D:/eclipse/javaEE/WebWorkSpace/ClassProject/src").toURI().toURL();
URL[] urls = {url};
//获取有关类型加载的JMX接口
ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
//用于缓存类加载器
List classLoaders = new ArrayList();
while(true) {
//加载类型并缓存类加载器实例
ClassLoader classLoader = new URLClassLoader(urls);
classLoaders.add(classLoader);
classLoader.loadClass("com.memory.ClassA");
//显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
System.out.println("total: "+ loadingBean.getTotalLoadedClassCount());
System.out.println("active: "+ loadingBean.getLoadedClassCount());
System.out.println("unloaded: "+ loadingBean.getUnloadedClassCount());
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
运行时参数设置:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=512m -XX:PermSize=8m -XX:MaxPermSize=8M
Metaspace 空间的运行轨迹:
可以从上面的 JVisualVM 的截图看出:当加载类的总数到达 87386 之后,就会出现 Metaspace 区域溢出。也可以通过观察垃圾回收活动轨迹了解元空间内存耗尽。Java 程序运行结果:
total: 87416
active: 87416
unloaded: 0
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.security.SecureClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.access$100(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at com.memory.OOMTest.main(OOMTest.java:26)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=8m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=8M; support was removed in 8.0