JVM自动内存管理之二

栈异常
  1. 如果线程请求分配的栈容量超过JVM允许的最大容量时,会抛出StackOverflowError异常
  2. 如果java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去扩展,会抛出OutOfMemoryError
  3. 如果创建新线程时没有足够的内存去创建对应的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 list = new ArrayList();

        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

后面详细说明对应的调试。

方法区

  1. 方法区也是各个线程共享的。
  2. 用于存储已经被jvm加载到方法区中的类型信息。
  3. gc效率很低,一般用于运行时常量池的数据回收和类的卸载。
运行时常量池
  1. 是方法区的一个组成部分
  2. 存储Java类文件中常量信息,用于存储编译期就生成好的字面量和符号引用。这部分信息在类被加载到方法区支行,会存入运行时常量池。
  3. �存在运行期间生成的常量,比如string的intern方法对应的常量。
永久代—>方法区
  1. jdk6之前,hotspot使用永久代来实现方法去。
  2. jdk7中,开始移除永久代:
    1. 符号表被移入native heap中
    2. 字符串常量和类的静态引用被移到java heap中
  3. 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 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代码么?

直接内存

  1. 直接内存并不是JVM运行时内存的一部分

  2. 堆外内存,NIO被引入,目的是为了避免java堆和native堆中来回复制数据带来的性能损耗。

  3. 通过一个存储在java heap中的DirectByteBuffer对象引用(通过虚引用(Phantom Reference)来实现堆外内存的释放)来管理native heap中的内存空间。

  4. 全局共享的内存区域,能够被管理,但是检测手段上会比较简陋

  5. 会出现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.(DirectByteBuffer.java:123)
     at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
     at DirectByteBufferOOM.main(DirectByteBufferOOM.java:28)
    
    

    可以看到,如果是堆外内存OOM,会显示Direct Buffer Memory字样,或者如果没有明确的指示,但是dump出来的内存很小,而且代码中直接或者间接使用了NIO,那么也大概率是堆外内存惹的祸。

你可能感兴趣的:(JVM自动内存管理之二)