简单了解 JVM 内存模型

JVM 的内存区域主要分为如图所示的几个区域。

简单了解 JVM 内存模型

1. 程序计数器(线程私有)

程序计数器的作用是存储当前线程执行的字节码指令的地址。并且在多线程环境下,每个线程都有一个独立的程序计数器。同时,此内存区域是唯一一个在Java虚拟机规范中没有规定任何 OOM 情况的区域。

2. Java虚拟机栈(线程私有)

栈主要用于存储局部变量、部分结果以及返回地址等,其中局部变量如果是对象,则存储的是对应的地址。另外,栈又分为栈帧,每个方法都会生成一个栈帧,方法执行结束后,对应的栈帧被弹出。

在 Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚
拟机所允许的深度,将抛出 StackOverflowError 异常:如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

以下是代码通过递归调用导致 StackOverflowError:

public class JVMStackSOF {
 
    public static int stackLength = 1;
 
    /**
     * VM 参数:-Xss128k
     * 在单个线程下,无论是由于栈顿太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
     * 如果测试时不限于单线程,通过不断地建立线程的方式倒到是可以产生内存溢出异常
     */
    public static void main(String[] args) {
        JVMStackSOF jvmStackSOF = new JVMStackSOF();
        try {
            jvmStackSOF.stackLeak();
        }catch (Throwable e){
            System.out.println("栈深度:" + JVMStackSOF.stackLength);
            throw e;
        }
    }
 
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }
}

以下代码理论上可以导致 JVM 虚拟机栈 OOM,但是似乎没有实现。

public class JVMStackOOM {
 
    /**
     * VM 参数:-Xss2M
     */
    public static void main(String[] args) {
        JVMStackOOM jvmStackOOM = new JVMStackOOM();
        jvmStackOOM.stackLeakByThread();
    }
 
    public void stackLeakByThread(){
        while (true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
 
    private void dontStop(){
        while (true){}
    }
}

3. 本地方法栈(线程私有)

与 Java 虚拟机栈类似,但为本地(Native)方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

4. Java堆(线程共享)

Java 堆是 JVM 管理的最大的一块内存区域,并且堆是线程共享的。它用于存储对象的实例,包括应用程序的对象和数组。

几乎所有的对象实例都在这里分配内存,在 Java 虚拟机规范中的指述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析折术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么绝对了。

堆内存也是最容易发生 OOM 的区域,所以在 JVM 调优中需要最注意堆内存大小的调整。

以下是一个简单实现 OOM 报错的代码:

public class HeapOOM {
 
    public static class OOMObject{
 
    }
 
    /**
     * VM 参数:-verbose:gc -Xms5M -Xmx5M -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
 
        List<OOMObject> heapOOMList = new ArrayList<>();
        while (true){
            heapOOMList.add(new OOMObject());
        }
    }
}

5. 元空间(线程共享)

在 JDK1.7 之前,HotSpot 虚拟机把方法区当成永久代(方法区的落地实现)来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

元空间和永久代不同的地方在于:

  • 存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
  • 存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。

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

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置人 Class 文件中常量池的内容才能进人方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 internO 方法。

6. 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频紧地使用,而且也可能导致 OOM 异常出现。

在 JDK1.4 中新加人了 NIO(New Input/Output)类,引人了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

直接内存 OOM 的案例代码如下:

public class DirectMemoryOOM {
 
    // 定义一个常量,表示1兆字节的大小
    private static final int _1MB = 1024 * 1024;
 
    /**
     * VM 参数: -Xmx20M -XX:MaxDirectMemorySize=10M -XX:+HeapDumpOnOutOfMemoryError
     * 由DirectMemory导致的内存溢出,一个明显的特征是在HeapDump文件中不会看见明显的异常,
     * 如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
     */
    public static void main(String[] args) throws Exception {
        // 通过反射获取Unsafe类的实例
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限,允许访问私有字段
        unsafeField.setAccessible(true);
        // 获取Unsafe类的实例
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
 
        // 无限循环,不断分配内存,导致直接内存溢出
        while (true) {
            // 使用Unsafe类的allocateMemory方法分配1兆字节的直接内存
            unsafe.allocateMemory(_1MB);
        }
    }
}
简单了解 JVM 内存模型

给TA打赏
共{{data.count}}人
人已打赏
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索