《深入理解Java虚拟机》学习笔记之内存分配

时间:2023-12-31 21:00:44

JVM在执行Java程序的过程中会把它所管理的内存划分若干个不同的数据区域,如下图:

《深入理解Java虚拟机》学习笔记之内存分配

大致可以分为两类:线程私有区域和线程共享区域。

线程私有区域

  1. 程序计数器(Program Counter Register): 是一块很小的内存,可以看做是当前线程所执行的字节码行号指示器,虚拟机根据计数器值获取吓一条要执行的指令。
  2. JVM栈:虚拟机栈(JVM stacks),每个方法被执行时都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  3. 本地方法栈(Native Method Stacks):与虚拟机栈的作用类似,但区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用的Native方法服务。有些虚拟机如Sun HotSpot把它与虚拟机栈合二为一。

线程共享区域

  1. Java堆(Java Heap):是Java编程最频繁使用和最大的内存区域,也是垃圾收集器管理的主要区域,此区域唯一存在的目的就是存放对象实例和数组。
  2. 方法区(Method Area):用于储存已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

  3. 行时常量池(Runtime Constant
    Pool):是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool
    Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法去的运行时常量池中。

其实还一块
内存即直接内存(Direct
Memory),它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁地使用,并且可能导致OOM异
常。如NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用
进行操作,这显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

内存异常


序计数器是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域,其他区域在无法再申请到足够的内存时就会
抛出OOM异常,而虚拟机栈和本地方法栈除了OOM异常外,当线程请求的栈深度大于虚拟机所允许的深度时将抛出*Error异
常。

对象访问

即使最简单的对象访问,都会涉及到Java栈、Java堆和方法区这三个最重要的区域。如:

Object obj = new Object();

“Object obj”将反映到JVM栈的局部变量表中,作为一个reference类型数据出现;而“new Object()”将反映到Java堆中,形成一块储存了Object类型所有实例数据值的结构化内存;另外在Java堆中还必须包含能查找到此对象类型 数据信息(如对象类型、父类、实现的接口、方法等),这些类型数据则储存在方法区。

由于Java虚拟机规范之规定了reference类型指向对象的引用,并没有定义寻址方式,因此目前有两种主流的寻址方式:使用句柄和直接指针。

如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示

《深入理解Java虚拟机》学习笔记之内存分配

如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所示

《深入理解Java虚拟机》学习笔记之内存分配

异常示例

  1. Java堆溢出OOM
    package net.oseye.demo;
    
    import java.util.ArrayList;
    import java.util.List;
    /**
    * VM Args:-Xms20m -Xmx20m
    */
    public class App
    {
    public static void main( String[] args )
    {
    List<OOMObject> list=new ArrayList<App.OOMObject>(); while(true){
    list.add(new OOMObject());
    }
    }
    static class OOMObject{}
    }

    执行

    java -Xms20m -Xmx20m net.oseye.demo.App

    报的溢出信息

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.grow(Unknown Source)
    at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    at net.oseye.demo.App.main(App.java:15)

    ps:通过虚拟机参数“ -Xms20m -Xmx20m”将Java堆最大最小大小设置为20M,即不可扩展;使用List是为了保证GC Roots到list对象之间有可达路径来避免垃圾回收机制清除list对象;

  2. 虚拟机栈*Error
    package net.oseye.demo;
    /**
    * VM Args:-Xss256k
    */
    public class App { private int stackLength = 1; public void stackLeak() {
    stackLength++;
    stackLeak();
    } public static void main(String[] args) throws Throwable {
    App oom = new App();
    try {
    oom.stackLeak();
    } catch (Throwable e) {
    System.out.println("stack length:" + oom.stackLength);
    throw e;
    }
    }
    }

    执行

    java -Xss256k net.oseye.demo.App

    异常信息

    stack length:1890
    Exception in thread "main" java.lang.*Error
    at net.oseye.demo.App.stackLeak(App.java:11)
    at net.oseye.demo.App.stackLeak(App.java:11)
    at net.oseye.demo.App.stackLeak(App.java:11)

    这个测试我是在ubuntu下做的,是单线程的,每个线程都有属于自己的虚拟机栈,当一个方法被调用时就会产生这个方法相关的一个栈帧,当方法执行完毕这个栈帧才会从栈顶pop掉,而实例中使用了递归就会一直向虚拟机栈push栈帧直到深度大于所允许的深度时旧会抛出*Error。
    PS:这里扑捉异常使用的是Throwable,如果使用Exception就不能显示出println的信息,此处不解,有待学习。

  3. 虚拟机栈 OOM
    可以开多个线程让虚拟机栈OOM,但其实这不是虚拟机栈抛出的,只是由于分给栈的内存多了自然会让虚拟机进程内存少了。这里不妨把XSS设置大一些。注意这里有风险哦,会造成操作系统假死,我在ubuntu下执行不仅ubuntu死了,我直接按电源启动ubuntu还让操作系统崩溃了,启动时报的异常“No init found. Try passing init= bootarg”,然后按照这个方法才修复了操作系统
    package net.oseye.demo;
    
    /**
    * VM Args:-Xss10m
    */
    public class App { public static void main(String[] args) throws Throwable {
    App app = new App();
    app.stackLeakByThread();
    } private void dontStop() {
    while (true) {
    System.out.println(Thread.currentThread().getId());
    }
    } private void stackLeakByThread() {
    while (true) {
    Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
    dontStop();
    }
    });
    thread.start();
    }
    }
    }

    执行后报的异常类似

    java.lang.OutOfMemoryError:unable to create new native thread

  4. 运行时常量池溢出
    如果向要运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含了一个等于此String对象的字符串,则返回idaibiao池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量:
    package net.oseye.demo;
    
    import java.util.ArrayList;
    import java.util.List; /**
    * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
    */
    public class App { public static void main(String[] args) throws Throwable {
    List<String> list=new ArrayList<String>();
    int i=0;
    while(true){
    list.add(String.valueOf(i).intern());
    }
    }
    }

    这是书上的例子,说会报异常:

    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)

    但我一直没跑出这个异常(win7,⊙﹏⊙b汗这个总结先后在win2003、ubuntu、win7上做的),及时又重新设小了PermSize。
    PS:google了之后才知道在jdk6及之前都会报上述异常,但jdk7就不会,而我用的是jdk7.简单来说就是在JDK 7里String.intern生成的String不再是在perm gen分配,而是在Java Heap中分配。

  5. 方法区溢出
    访法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本思路是运行时产生大量的类去填满访法区,直到溢出。书中是借助CGLib框架直接操作字节码,生成大量的动态类:
    package net.oseye;
    
    import java.lang.reflect.Method;
    
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy; /**
    * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
    */
    public class App { public static void main(String[] args) throws Throwable {
    while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(OOMObject.class);
    enhancer.setUseCache(false);
    enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object arg0, Method arg1,
    Object[] arg2, MethodProxy arg3) throws Throwable {
    return arg3.invokeSuper(arg0, arg2);
    }
    });
    enhancer.create();
    }
    } static class OOMObject {
    }
    }

    书中说会报异常:

    java.lang.OutOfMemoryError: PermGen space
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

    但遗憾的是我的测试依然没有出现上述异常,而只是报(JDK7)

    Exception in thread "main"
    Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

  6. 本机直接内存溢出
    DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,代码清单2-9越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
    package net.oseye;
    
    import java.lang.reflect.Field;
    import sun.misc.Unsafe; /**
    * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
    */
    public class App { private 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);
    }
    }
    }

    异常

    Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at net.oseye.App.main(App.java:19)