文章目录
JVM内存区域
JVM系列文章是基于:《深入理解Java虚拟机:JVM高级特性与最佳实践》-周志明第二版
一、小历史
- 在JVM的发展历史中,关于JVM有几个比较重要的事件,一个是HotSpot VM,这是一家名为Longview Technologies公司设计的,该公司在1997年被Sun公司收购从而Sun获得了HotSpot。
- 2006年Sun宣布将Java开源,在开源的基础是建立了OpenJdk,因此HotSpot成了Sun JDK和Open JDK的共同虚拟机
- Oracle在2008年收购了BEA,得到了JRockit VM,在2009年收购了Sun,得到了HotSpot VM,Oracle在得到了这两款虚拟机之后也开始了两款虚拟的整合工作,使之优势互补,合并也是JDK中去除永久代的一个原因,因为JRockit VM中没有永久代,当然永久代可能导致的OOM PermGen space也是一个原因。
二、内存区域划分
2.1 运行时数据区
- 下图是Java虚拟机运行时数据区,不过需要强调的是,下图是根据《Java虚拟机规范(Java SE 7版的规定)》
- 相关区域作用如下:
区域 | 功能 | 线程共享? | 相关异常 | 相关参数 |
---|---|---|---|---|
程序计数器 | 字节码行号指示器 | 线程独享 | 无 | 无 |
虚拟机栈 | 方法调用栈,生命周期与线程一致 | 线程独享 | StackOverflowError和OutOfMemoryError | -Xss 设置栈大小 |
本地方法栈 | 本地方法调用栈 | 线程独享 | StackOverflowError和OutOfMemoryError | HotSpot中,本地方法栈与虚拟机栈做在了一起 |
堆 | 大部分对象保存区 | 线程共享 | OutOfMemoryError Java heap space | -Xms20m -Xmx20m 堆最大最小值 |
方法区 | 类信息、常量、静态变量、运行时常量池、即时编译器编译后的代码等 | 线程共享 | OutOfMemoryError: PermGen space | -XX:PermSize及-XX:MaxPermSize |
- 关于方法区:JVM规范没有强制如何实现方法区,在HotSpot中使用永久代来实现方法区,在JRockit中则没有方法区。使用永久代实现方法区实际上并不好,容易产生OutOfMemoryError: PermGen space,因此在其他虚拟机中只有没有叨叨物理内存的上限就不会发生该异常。方法区是JVM的一种规范,而永久代和元空间则属于对该规范的不同实现。
2.2 JDK1.8的变化
-
前面提到方法区的实现有永久代和元空间,在1.8的堆部分就没有永久代,取而代之的是元空间(MetaSpace)。而且MetaSpace是分配在直接内存,其使用大小不受堆大小的限制,而且它会有一个自动增长的机制来避免1.7中可能抛出的OutOfMemoryError: PermGen space,
-
元空间和永久代对比
1.相同点:都是对方法区的实现
2.不同点:前者保存在直接内存,理论上只会收到物理内存大小的限制,后置受到堆大小的限制
- 元空相关配置如下:
-XX:MetaspaceSize,初始元空间大小,达到该值就会触发垃圾收集进行类型卸载,越大GC触发越晚,越小GC触发越早。GC时会对该
值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
在默认情况下,该值根据不同的平台在12M到20M浮动。使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,
-XX:Metaspacesize为21810376B(大约20.8M)
-XX:MaxMetaspaceSize=128m,最大空间,默认是没有限制的。防止无限使用影响到其他应用。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后会计算元空间空闲空间比例,如果小于该值,则会增加元空间大小,该值配置很低的话,就
会使得元空间大小增加比较缓慢,比如配置10%,在元空间垃圾回收之后空闲空间只有15%,那么此时也不会增加元空间大小,但是实际上
元空间的大小已经不太够用了,反之配置很大比如60%,每次回收之后还有40的空间其实是比较足的,但是也会增加元空间大小,因此配
置过大,会让元空间大小增加的比较快。
-XX:MaxMetaspaceFreeRatio,这个参数和前面的相对于,在GC之后会计算元空间空闲空间比例,如果空闲比大于这个参数,就会释放
元空间的内存,如果配置的很低,那么元空间的内存就会很容易释放,反之配置很高,那么回收之后空闲比没有这么多,空间就不容易释放
-XX:MaxMetaspaceExpansion=N ,Metaspace增长时的最大幅度。
-XX:MinMetaspaceExpansion=N,Metaspace增长时的最小幅度。
2.3 变化原因?
- 永久代到元空间的变化原因是什么?在以前的永久代中,我们需要设置其大小,因为其占用了堆的部分空间,如果过大则会减少堆其他部分可用空间大小,可能会导致较多的Full GC,如果过小,则可能会导致OutOfMemoryError: PermGen space。其次这也带来了一定的复杂性,导致使用者需要关注这一块和整个堆的内存分配,总的来说原因有下面这些:
1.字符串存在永久代中,容易出现性能问题和内存溢出。
2.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3.永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4.Oracle 可能会将HotSpot 与 JRockit(不含永久代) 合二为一。
- 移除永久代的过程在JDK1.7就进行了一部分,不过1.7中还依然保留着永久代,但是已经将部分保存在永久代的区域移到了堆区或者直接内存。比如1.7将字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:
public class StringOomMock {
static String base = "string";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
- 如上的代码,在1.6、1.7、1.8中抛出的异常各不一样,1.6中是OutOfMemoryError: PermGen space,因为1.6字符串常量保存在在永久代区域,但是1.7中则会报OutOfMemoryError:Java heap space,因此1.7已经将字符串常量保存到了堆区,1.8中也会提示堆空间的OOM,不过1.8会忽略永久代的设置参数。
三、异常
3.1 堆溢出
- 设置下面的参数,然后不断创建对象,即会触发堆溢出
//VM Options: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
//堆溢出
ArrayList<OOM> arrayList = new ArrayList<OOM>();
while (true){
arrayList.add(new OOM());
}
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14960.hprof ...
Heap dump file created [28256509 bytes in 0.188 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
3.2 栈溢出
- 递归调用
//栈溢出 -Xss128k 设置栈容量128k
new TestA().test();
void test(){
test();
}
Exception in thread "main" java.lang.StackOverflowError
3.3 方法区和运行常量池溢出
- 常量池是方法区的一部分,因此有多种方法导致方法区溢出,使用字符串常量是一种方法
//常量池导致方法区溢出
ArrayList<String> list = new ArrayList<String>();
int i=0;
while (true){
list.add(String.valueOf(i++).intern());
}
- 可以动态加载很多类来导致方法区溢出:
public class Test1 {
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test1.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,objects);
}
});
enhancer.create();
}
}
}
3.4 元空间溢出
- 注意因为1.8的元空间和1.7的永久代都是对方法区的实现,因此他们保存的数据,有一些是一样的,比如都会保存加载的类信息,因此前面那段动态生成很多类的代码在1.8运行就会导致元空间溢出。
-XX:MaxMetaspaceSize=32m
//运行之前设置元空间的最大大小
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
3.5 直接内存溢出
- 直接内存默认不受堆大小的限制,但是收到本机内存的限制,不过如果不设置的话默认直接内存大小和堆大小相同。设置方法:-XX:MaxDirectMemorySize=10M
-XX:MaxDirectMemorySize=10M
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true){
unsafe.allocateMemory(1024);
}
}
- 上面这段代码可能会导致笔记本死机,另外直接内存OOM的时候,Heap Dump文件比较小,如果遇到这样的情况并且程序使用了NIO,那么就需要关注是否是直接内存溢出了。
四、参考
- [1] 永久代和元空间的区别
- [2] jdk8 Metaspace 调优