JVM自动内存管理之二
栈异常
- 如果线程请求分配的栈容量超过JVM允许的最大容量时,会抛出StackOverflowError异常
- 如果java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去扩展,会抛出OutOfMemoryError
- 如果创建新线程时没有足够的内存去创建对应的java虚拟机栈,也会抛出OutOfMemoryError
public class JavaVMStackSOF{
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak(); // 递归调用自己
}
public static void main(String[] args){
JavaVMStackSOF sof = new JavaVMStackSOF();
try{
sof.stackLeak();
}catch(Throwable e){
System.out.println("stack length=" + sof.stackLength);
throw e;
}
}
}
执行结果:
bash> java JavaVMStackSOF -Xss128K
stack length=18357
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
而一直分配新的线程,死循环,就会导致OOM,这里就不演示了。
所有的对象依然在堆中,局部变量表、操作数栈中只有对应的reference引用,以及基础数据类型。
堆
-
堆是所有线程全局共享的内存空间,一般也是jvm内存区域中最大的一部分,几乎所有对象都在其中。
-
是GC回收的主要区域,所以也叫gc堆。
-
为了提高不同线程在堆中内存分配时的效率,减少冲突,有的jvm实现会为每个线程在创建时同时创建TLAB,即threadlocal allocating buffer。线程在各自的tlab中分配对象,当tlab中内存用完时,才会加锁,然后去堆中再去申请新的内存。
-
java堆还有一种划分方式,就是新生代、老年代这种分代划分。
-
java堆可以在物理上不连续的空间上分配,只要逻辑上看起来是连续的即可。可以使用-Xmx,-Xms来设定堆空间的最大和最小值。
-
当java堆空间已经不足以分配一个新对象,并且无法扩展新空间,即使垃圾回收也无法会受到足够的空间,就会报OutOfMemoryError异常。
堆和栈的关联
Snip20180510_13.png如上图所示:
- obj只是一个引用,存放在执行这条语句的线程的java虚拟机栈中,其中一个slot存储了这个reference。
- obj指向了java堆中的内存地址,这个地址对应object对象。
- Object对象头中一般会存放当前实例对应的类型信息,用于从方法区中寻找到对应的类信息。
int a = 1;
int[] array = new int[] {1,2};
a是局部变量,而且是基本数据类型,会直接存在java虚拟机栈中,不过这个其实算是jvm提供的一种性能优化,引用类型还是会放在堆中的,只是其引用在栈中,寻址会存在一定的性能消耗。
array是个对象,array会作为一个引用存在栈中,而对应的数组对象会存储在堆中。
Java堆内存溢出
import java.util.ArrayList;
import java.util.List;
/**
-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM{
static class MyObect{}
public static void main(String[] args){
List<MyObect> list = new ArrayList<MyObect>();
for(;;){
list.add(new MyObect());
}
}
}
执行的时候带上jvm参数。会报错OutOfMemoryError。
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2173.hprof ...
Heap dump file created [27844982 bytes in 0.135 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at HeapOOM.main(HeapOOM.java:16)
Process finished with exit code 1
后面详细说明对应的调试。
方法区
- 方法区也是各个线程共享的。
- 用于存储已经被jvm加载到方法区中的类型信息。
- gc效率很低,一般用于运行时常量池的数据回收和类的卸载。
运行时常量池
- 是方法区的一个组成部分
- 存储Java类文件中常量信息,用于存储编译期就生成好的字面量和符号引用。这部分信息在类被加载到方法区支行,会存入运行时常量池。
- �存在运行期间生成的常量,比如string的intern方法对应的常量。
永久代—>方法区
- jdk6之前,hotspot使用永久代来实现方法去。
- jdk7中,开始移除永久代:
- 符号表被移入native heap中
- 字符串常量和类的静态引用被移到java heap中
- jdk8中,metaspace替代永久代,metaspace是在native heap中的。
演示代码:
public class RuntimeConstantPoolChange {
public static void main(String[] args){
String str1 = new StringBuilder("alan").append("jin").toString();
System.out.println(str1 == str1.intern()); // intern:初次,如果不存在,会加入到常量池中,但是返回的是本身的reference,返回true
// 如果存在,则返回的是方法区中地址,而str1返回的是堆中地址,所以下面两个输出全是false
String str2 = new StringBuilder("alan").toString();
System.out.println(str2.intern() == str2); // false
String str3 = new StringBuilder("java").toString(); // java 关键字已经存在,而且是在方法区中,不在堆中,false
System.out.println(str3.intern() == str3);
}
}
方法区中OOM的代码:
import java.util.ArrayList;
import java.util.List;
/**
* VM args: -Xmx10m -Xms10m
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args){
List<String> strings = new ArrayList<>();
int i = 0;
// 在1.6版本中,int的取值范围2的31次方,足够撑满永久代,所以会oom
// 在1.7及以上的jdk版本中,运行时常量池在java heap中,可以永久执行下去。但是如果java heap太小,会厨下以下异常:
while (true){
strings.add(String.valueOf(i++).intern());
}
}
}
GC overhead limt exceed
检查是Hotspot VM 1.6定义的一个策略,通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。Sun 官方对此的定义是:并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。
方法区又分成两个部分,PermGen和CodeCache。其中PermGen存放java类的相关信息,如静态变量、成员方法和抽象方法等。codecache存放JIT编译后的本地代码。?native代码么?
直接内存
-
直接内存并不是JVM运行时内存的一部分
-
堆外内存,NIO被引入,目的是为了避免java堆和native堆中来回复制数据带来的性能损耗。
-
通过一个存储在java heap中的DirectByteBuffer对象引用(通过虚引用(Phantom Reference)来实现堆外内存的释放)来管理native heap中的内存空间。
-
全局共享的内存区域,能够被管理,但是检测手段上会比较简陋
-
会出现OOM
import sun.misc.Unsafe; import java.lang.reflect.Field; import java.nio.ByteBuffer; /** * -Xmx20M -XX:MaxDirectMemorySize=10M */ public class DirectByteBufferOOM { private static final int size = 1024 * 1024 * 128; // 128M public static void main(String[] args) throws IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; // 获取unsafe对象 unsafeField.setAccessible(true); // 设置操作权限 Unsafe unsafe = (Unsafe) unsafeField.get(null); System.out.println(sun.misc.VM.maxDirectMemory()); while (true){ // unsafe.allocateMemory 就是DirectByteBuffer分配内存时使用到的方法,不建议直接使用,只是为了展示 // unsafe.allocateMemory(size); // 以上代码无法产生OOM,但是下面的方法可以: ByteBuffer.allocateDirect(size); // As the original answer says: // Unsafe.allocateMemory() is a wrapper around os::malloc which doesn't care about any memory limits imposed by the VM. //ByteBuffer.allocateDirect() will call this method but before that, // it will call Bits.reserveMemory() (In my version of Java 7: DirectByteBuffer.java:123) // which checks the memory usage of the process and throws the exception which you mention. } } }
产生的OOM异常如下:
10485760 Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at DirectByteBufferOOM.main(DirectByteBufferOOM.java:28)
可以看到,如果是堆外内存OOM,会显示Direct Buffer Memory字样,或者如果没有明确的指示,但是dump出来的内存很小,而且代码中直接或者间接使用了NIO,那么也大概率是堆外内存惹的祸。