内存分区
jvm内存主要分为以下几块:
- 堆:实例对象和数组
- 方法区:类变量、常量、类信息、即时编译器编译后的代码
- 运行时常量池:class文件的常量池
- 虚拟机栈:局部变量、操作栈、动态链接、方法返回地址
- 本地方法栈:Native方法
- 程序计数器:当前线程执行的字节码的行号指示器
- 直接内存:不属于java运行时数据区,供NIO分配内存使用
其中堆和方法区是线程共享的,虚拟机栈、本地方法栈、程序计数器是线程私有的。运行时数据区如下图1所示(摘自《深入理解jvm虚拟机》)
内存溢出异常
内存溢出异常根据内存的分区类型可分为:堆溢出、栈溢出、本地方法栈溢出、方法区和运行池常量溢出、直接内存溢出。
堆溢出
溢出条件在堆中创建实例对象且被GC Roots到对象之间有可达路径。代码如下:
1 | /** |
list为GC roots,且list与对象实例存在可达路径,故新建的对象不会被垃圾回收,导致堆内存溢出:
1 | "E:\Program Files\Java\jdk1.8.0_91\bin\java" -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2017.3.4\lib\idea_rt.jar=50605:D:\Program Files\JetBrains\IntelliJ IDEA …… |
栈溢出
栈溢出包含两种错误:
- 栈深度过大,超过虚拟机允许的最大深度,抛出StackOverflowError
- 虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError
java方法调用时会建立相对应的栈信息,因此栈溢出可通过无限制的方法调用来实现。代码如下:
1 | public class StackOverflowError { |
stackLeak方法递归调用自己,最终导致栈深度超过虚拟机指定的栈深度而报异常,异常信息如下:
1 | "E:\Program Files\Java\jdk1.8.0_91\bin\java" -Xms20m -Xmx20m "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2017.3.4\lib\idea_rt.jar=51044:D:\Program ……memory.StackOverflowError |
栈是线程私有的,所以每建立一个线程都会分配相应的栈内存。可以通过无线创建栈来验证第二种OutOfMemoryError报错信息。代码如下:
1 | public class StackOutMemory { |
运行结果会报OutOfMemoryError。
方法区溢出
jdk1.6以前方法区包含永久代PermGen,1.7、1.8已经着手去除永生代,1.8方法区中已经不包含永久代。重点分析jdk1.8中方法区的溢出情况,代码如下:
1 | public class PermGen { |
当参数只设置-XX:PermSize=10M -XX:MaxPermSize=10M时,程序并不会抛出异常,会一直执行。当指定堆大小时:-Xmx20M,程序抛出堆溢出异常:
1 | "E:\Program Files\Java\jdk1.8.0_91\bin\java" -Xmx20M -XX:PermSize=10M -XX:MaxPermSize=10M -XX:MaxMetaspaceSize=10m "-javaagent:D:\Program …… |
通过上述例子输出结果可以发现,jdk1.8之后,PermSize、MaxPermSize参数无效,证明永久代已从1.8中移除,那么思考如下问题:
- 方法区分配在哪个内存区域呢?
- 为何报堆内存溢出?难道方法区在堆内存中分配吗?
查看资料发现jdk1.7中方法区在堆内存中分配,但在jdk1.8中方法区在元空间中分配。具体如下图所示:
在jdk1.8中,将方法区分配到一个新的内存区域:元空间。元空间与虚拟机的内存大小无关,受到宿主机本身的总内存大小的限制,与直接内存类似。
回到刚才的问题,常量池在方法区中分配,而方法区又不在堆内存中,为何会报堆内存溢出异常?这得仔细探讨另一个问题:intern()函数生成的对象存放在哪里?
查阅资料发现:
- jdk1.6中,String.intern()函数会检测常量池中是否已存在字符串常量,若不存在(首次遇到),则将对象实例复制到字符串常量池中,若已存在则返回常量池中的字符串引用。
- 而jdk1.7以后,intern()实现并不会将首次出现的字符串实例复制到常量池,而是只记录字符串首次出现的实例引用。
故上述函数list.add(base.intern())操作,会将base对象实例创建在堆内存上,常量池只是存放字符串常量的引用,故我们加上-Xmx=10m限制堆内存大小而没有限制元空间大小时,会报堆内存溢出。
如果运行时修改虚拟机参数为:-Xmx200M -XX:MaxMetaspaceSize=1m,结果为:
1 | "E:\Program Files\Java\jdk1.8.0_91\bin\java" -Xmx200M -XX:PermSize=10M -XX:MaxPermSize=10M -XX:MaxMetaspaceSize=1m "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2017.3.4\lib\idea_rt.jar=60736:D:\Program Files\JetBrains\IntelliJ IDEA 2017.3.4\bin" -Dfile.encoding=UTF-8 -classpath …… |
此时报元空间内存溢出,我们将堆内存的大小设为元空间大小的200倍,此时元空间比堆内存先溢出,常量池存储的引用超过1m,报元空间溢出。
因此jdk1.8中,字符串实例存储在堆内存中,实例引用存储在运行时常量池,而运行时常量池在方法区中,方法区位于元数据区。
直接内存溢出
直接内存可通过-XX:MaxDirectMemorySize指定,未指定则默认与java的堆内存大小一致,可通过Unsafe实例进行内存分配。代码如下:
1 | public class DirectMemoryError { |
异常结果为:
1 | "E:\Program Files\Java\jdk1.8.0_91\bin\java" -Xms20M -XX:MaxDirectMemorySize=10M "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2017.3.4\lib\idea_rt.jar=50267:D:\Program Files\JetBrains\IntelliJ IDEA 2017.3.4\bin" -Dfile.encoding=UTF-8 -classpath "E:\Program …… |