JVM运行时数据区

来源:互联网 发布:hadoop 数据清洗工具 编辑:程序博客网 时间:2024/06/06 12:23

JVM运行时数据区

  JVM运行时数据区分布如下图所示:
  
JVM运行时数据区示意

1. 程序计数器

  程序计数器(PC)记录当前线程所执行字节码的行号。比如,当前线程正在执行一个java方法,这个计数器就记录正在执行的虚拟机字节码指令的地址。
  由于Java多线程机制,轮流切换线程并分配时间,那么,为了在线程切换后能够恢复到正确的执行位置,每条线程都需要一个私有的PC,所以,PC也是线程隔离的。

2. Java虚拟机栈

  Java虚拟机机栈描述的是Java内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),方法在执行的过程总是在压栈和出栈,栈帧是方法运行时的基础数据结构,用于存储局部变量表、操作数栈、动态链表、方法出口等信息,每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中如占到出栈的过程。
Java虚拟机规范中,对这个区域有两种异常定义:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError;
  • 如果该虚拟机栈可以动态扩展,而扩展时没有足够的内存支持,抛出OutOfMemoryError(简称OOM)。

3. 本地方法栈

  本地方法栈为虚拟机执行Native方法服务。
  同虚拟机栈,本地方法栈区域也抛出同样的两种异常。

4. Java堆

  Java堆是所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实力都在这里分配内存。在Java虚拟机规范中描述:所有的对象实例以及数组都要在堆上分配。
  Java堆是GC管理的主要区域。
  当前主流的虚拟机都是按照可扩展实现的,通过-Xmx和-Xms来控制:
  
  -Xmx20m: 设置JVM允许分配的最大堆内存20m
  -Xms20m: 设置JVM分配的初始堆内存20m
  

  如果堆中没有足够的内存分配实例,并且也没有内存支持扩展,则抛出OutOfMemoryError。
  

5. 方法区

  方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  
  运行时常量池
  属于方法区的一部分,Class文件中除了有类的版本、字段、方法和接口等描述信息外,还存在常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。同样,由于受方法区内存限制,常量池无法申请到内存时也会抛出OOM异常。
  在JDK1.7将字符串常量池移入了堆中,当然字符对象也具有了其他堆中对象的特性了,比如可以被gc回收:

Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931

直接内存

  直接内存并非虚拟机运行时数据区的一部分,也与虚拟机规范定义的内存无关。
  在JDK1.4后引入了NIO类,使用一种基于管道(Channel)和缓冲区(Buffer)I/O方式,可以通过本地函数库直接分配堆外内存,并通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,从而避免了Java堆和本地堆中来回的数据复制。
  本机直接内存不会受到Java堆限制,不过当各个内存区域大小总和超出物理内存,就会导致动态扩展时出现OOM异常。


6. OOM异常

  如上所述,虚拟机规范中,除了PC之外,其他几个运行时区域都有可能发生OOM异常,现用一些实例来说明。

Java堆溢出

  GC对Java堆中对象的回收,通常通过判断GC Roots是否存在可到达对象的路径来确定是否清楚该对象的,如果对象占用内存达到堆最大限制后就会产生内存溢出。
测试堆内存溢出异常

运行参数:

-Dfile.encoding=UTF-8    -Xms20m -Xmx20m ##设置堆大小20m,并将最小和最大值设置相等,避免扩展-XX:+HeapDumpOnOutOfMemoryError ##dump出当前的内存堆转储快照-XX:HeapDumpPath=E:\job   ##指定路径(转储文件还是挺大的)-XX:SurvivorRatio=8    ## 存活比2:8
package net.jvm.oom;import java.util.ArrayList;import java.util.List;/** *  * @description HeapOOM * <p>Java堆异常测试</p> * <code>VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\job</code> * <p>以上参数的含义是:限制Java堆大小为20MB,不可扩展</p> * <p>通过此参数可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照</p> * @author Yampery * @date 2017年6月16日 下午11:42:46 */public class HeapOOM {    static class OOMObject { }    public static void main(String[] args) {        List<OOMObject> list = new ArrayList<OOMObject>();        while (true) {            list.add(new OOMObject());        } /// while end    } /// main} ///~/** * 运行结果: * java.lang.OutOfMemoryError: Java heap space * Dumping heap to E:\job\java_pid4056.hprof ... * Heap dump file created [28010445 bytes in 0.373 secs] * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space */

Memory Analysis工具使用见MAT工具安装使用指南
  如果分析有内存泄漏(Memory leak)则可以查看内存泄漏到GC Roots的路径。就能看到GC为何无法自动回收对象,能够准确定位出泄漏的代码片段。
  如果不是内存泄漏,就需要检查虚拟机参数(-Xmx和-Xms)。

虚拟机栈和本地方法栈溢出

在Java虚拟机中描述了两种异常:

  • StackOverflowError
    如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出;
  • OutOfMemoryError
    如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出。

虚拟机栈和本地方法栈OOM测试

package net.jvm.oom;/** *  * @description StackOOM * <p>测试{@link StackOverflowError}异常</p> * <code>Vm Args: -Xss128k</code> * 通过-Xss参数设置给一个线程分配的堆栈大小 * @author Yampery * @date 2017年6月17日 下午3:52:46 */public class StackOOM {    private int stackDep = 1;    /**     * 递归调用,每次栈深度+1     */    public void stackLeak() {        stackDep++;        stackLeak();    }    public static void main(String[] args) {        StackOOM oom = new StackOOM();        try {            oom.stackLeak();        } catch (Throwable e) { // StackOverflowError是Error的子类            System.out.println("栈深度:" + oom.stackDep);            throw e;        }    } /// main} ///~/** * 运行结果: * 栈深度:994 * Exception in thread "main" java.lang.StackOverflowError * at net.jvm.oom.StackOOM.stackLeak(StackOOM.java:21) * at net.jvm.oom.StackOOM.stackLeak(StackOOM.java:22) * ........ */

创建线程导致内存溢出异常测试
  测试该代码前保存工作,windows平台中,Java的线程是映射到操作系统内核线程上的,测试参数为每个线程分配的栈容量比较大,英雌很容易吧内存耗尽,造成操作系统假死。因此以下代码执行有很大风险。

package net.jvm.oom;/** *  * @description ThreadOOM * <p>测试线程导致内存溢出异常</p> * <code>VM Args: -Xss3M</code> * 上述参数把每个线程堆栈的大小设置为3M * @author Yampery * @date 2017年6月17日 下午10:51:31 */public class ThreadOOM {    private void dontStop() {        while(true);    }    public void stackLeakByThread() {        while(true) {            Thread thread = new Thread(new Runnable() {                @Override                public void run() {                    dontStop();                } /// run end            }); /// new Thread            thread.start();        } /// while end    }    public static void main(String[] args) {        ThreadOOM oom = new ThreadOOM();        oom.stackLeakByThread();    } /// main end}

方法区和运行时常量池溢出

  运行时常量池是方法区的一部分,如前所述,在JDk1.6和之前版本,常量池被分配在永久代内,intern()方法将常量添加到常量池,我们可以通过以下参数,限制非堆内存(non-heap)大小,也可以看做方法区大小,如果是JDK1.7,intern()则不会复制实例,则会一直循环下去。

-XX:PermSize=10M  ##10M-XX:MaxPermSize=10M  ##最大10M
package net.jvm.oom;import java.util.ArrayList;import java.util.List;import org.junit.Test;/** *  * @description ContantPoolOOM * <p>测试运行时常量池溢出异常</p> * <code>VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M</code> * 通过设置方法区大小间接限制常量池大小 * @author Yampery * @date 2017年6月17日 下午11:41:10 */public class ContantPoolOOM {    @Test    public void testContantPoolLeak() {        // 使用List保持对常量池引用,避免Full GC回收常量池行为        List<String> list = new ArrayList<>();        int i = 0;         while (true) {            list.add(String.valueOf(i++).intern());        } /// while end    }}

本机直接内存溢出

  可以通过-XX:MaxDirectMemorySize 指定直接内存容量,如果未指定,则就是-Xmx的最大值。下面的测试代码通过反射获取Unsafe实例进行内存分配,Unsafe类拥有像操作指针一样的方法,是不安全的。

package net.jvm.oom;import java.lang.reflect.Field;import org.junit.Test;import sun.misc.Unsafe;/** *  * @description DirectMemoryOOM * <p>测试本机直接内存溢出</p> * <code>VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M</code> * 指定Java堆最大20M,直接内存最大10M * @author Yampery * @date 2017年6月18日 下午1:27:40 */public class DirectMemoryOOM {    private static final int _1MB = 1024 * 1024;    @Test    public void test() throws IllegalArgumentException, IllegalAccessException {        Field unsafeField = Unsafe.class.getDeclaredFields()[0];        unsafeField.setAccessible(true);        Unsafe unsafe = (Unsafe) unsafeField.get(null);        // 申请分配空间        while (true) { unsafe.allocateMemory(_1MB); }    } ///main} ///~/** * 运行结果: * java.lang.OutOfMemoryError * at sun.misc.Unsafe.allocateMemory(Native Method) * ...... */
原创粉丝点击