jvm(1)--内存管理

内存分区

jvm内存主要分为以下几块:

  • 堆:实例对象和数组
  • 方法区:类变量、常量、类信息、即时编译器编译后的代码
    • 运行时常量池:class文件的常量池
  • 虚拟机栈:局部变量、操作栈、动态链接、方法返回地址
  • 本地方法栈:Native方法
  • 程序计数器:当前线程执行的字节码的行号指示器
  • 直接内存:不属于java运行时数据区,供NIO分配内存使用

其中堆和方法区是线程共享的,虚拟机栈、本地方法栈、程序计数器是线程私有的。运行时数据区如下图1所示(摘自《深入理解jvm虚拟机》)

图 1 jvm运行时数据区

内存溢出异常

内存溢出异常根据内存的分区类型可分为:堆溢出、栈溢出、本地方法栈溢出、方法区和运行池常量溢出、直接内存溢出。

堆溢出

溢出条件在堆中创建实例对象且被GC Roots到对象之间有可达路径。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOutMemory {

static class OOMObject{
int a = 15;
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}

list为GC roots,且list与对象实例存在可达路径,故新建的对象不会被垃圾回收,导致堆内存溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"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 ……
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3704.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [31531866 bytes in 0.195 secs]
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at memory.HeapOutMemory.main(HeapOutMemory.java:15)

Process finished with exit code 1

栈溢出

栈溢出包含两种错误:

  • 栈深度过大,超过虚拟机允许的最大深度,抛出StackOverflowError
  • 虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError

java方法调用时会建立相对应的栈信息,因此栈溢出可通过无限制的方法调用来实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StackOverflowError {

private int stackLength = 1;

public void stackLeak(){
stackLength++;
stackLeak();
}

public static void main(String[] args){
StackOverflowError stackOverflowError = new StackOverflowError();
try {
stackOverflowError.stackLeak();
}catch (Throwable e){
System.out.print("stack length:" + stackOverflowError.stackLength);
throw e;
}
}
}

stackLeak方法递归调用自己,最终导致栈深度超过虚拟机指定的栈深度而报异常,异常信息如下:

1
2
3
4
5
6
7
"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
stack length:19505
Exception in thread "main" java.lang.StackOverflowError
at memory.StackOverflowError.stackLeak(StackOverflowError.java:9)
at memory.StackOverflowError.stackLeak(StackOverflowError.java:9)
at memory.StackOverflowError.stackLeak(StackOverflowError.java:9)
……

栈是线程私有的,所以每建立一个线程都会分配相应的栈内存。可以通过无线创建栈来验证第二种OutOfMemoryError报错信息。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StackOutMemory {
private void running(){
while (true){

}
}
public void stackOutMemory(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
running();
}
});
thread.start();
}
}
public static void main(String[] args){
StackOutMemory stackOutMemory = new StackOutMemory();
stackOutMemory.stackOutMemory();
}
}

运行结果会报OutOfMemoryError。

方法区溢出

jdk1.6以前方法区包含永久代PermGen,1.7、1.8已经着手去除永生代,1.8方法区中已经不包含永久代。重点分析jdk1.8中方法区的溢出情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class PermGen {

public static void main(String[] args) throws InterruptedException{
List<String> list = new ArrayList<>();
String base = "intern";
int i = 0;
while (true) {
list.add(base.intern());
base += base;
}
}
}

当参数只设置-XX:PermSize=10M -XX:MaxPermSize=10M时,程序并不会抛出异常,会一直执行。当指定堆大小时:-Xmx20M,程序抛出堆溢出异常:

1
2
3
4
5
6
7
8
9
10
"E:\Program Files\Java\jdk1.8.0_91\bin\java" -Xmx20M -XX:PermSize=10M -XX:MaxPermSize=10M -XX:MaxMetaspaceSize=10m "-javaagent:D:\Program ……
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at memory.PermGen.main(PermGen.java:19)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0

通过上述例子输出结果可以发现,jdk1.8之后,PermSize、MaxPermSize参数无效,证明永久代已从1.8中移除,那么思考如下问题:

  1. 方法区分配在哪个内存区域呢?
  2. 为何报堆内存溢出?难道方法区在堆内存中分配吗?

查看资料发现jdk1.7中方法区在堆内存中分配,但在jdk1.8中方法区在元空间中分配。具体如下图所示:

图 2 不同版本的jdk方法区

在jdk1.8中,将方法区分配到一个新的内存区域:元空间。元空间与虚拟机的内存大小无关,受到宿主机本身的总内存大小的限制,与直接内存类似。

回到刚才的问题,常量池在方法区中分配,而方法区又不在堆内存中,为何会报堆内存溢出异常?这得仔细探讨另一个问题:intern()函数生成的对象存放在哪里?

查阅资料发现:

  • jdk1.6中,String.intern()函数会检测常量池中是否已存在字符串常量,若不存在(首次遇到),则将对象实例复制到字符串常量池中,若已存在则返回常量池中的字符串引用。
  • 而jdk1.7以后,intern()实现并不会将首次出现的字符串实例复制到常量池,而是只记录字符串首次出现的实例引用。

故上述函数list.add(base.intern())操作,会将base对象实例创建在堆内存上,常量池只是存放字符串常量的引用,故我们加上-Xmx=10m限制堆内存大小而没有限制元空间大小时,会报堆内存溢出。

如果运行时修改虚拟机参数为:-Xmx200M -XX:MaxMetaspaceSize=1m,结果为:

1
2
3
4
5
6
7
"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 ……
Error occurred during initialization of VM
OutOfMemoryError: Metaspace
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0

Process finished with exit code 1

此时报元空间内存溢出,我们将堆内存的大小设为元空间大小的200倍,此时元空间比堆内存先溢出,常量池存储的引用超过1m,报元空间溢出。

因此jdk1.8中,字符串实例存储在堆内存中,实例引用存储在运行时常量池,而运行时常量池在方法区中,方法区位于元数据区。

直接内存溢出

直接内存可通过-XX:MaxDirectMemorySize指定,未指定则默认与java的堆内存大小一致,可通过Unsafe实例进行内存分配。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class DirectMemoryError {

public static final int _1MB = 1024*1024;
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

异常结果为:

1
2
3
4
5
6
"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 ……
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at memory.DirectMemoryError.main(DirectMemoryError.java:15)

Process finished with exit code 1