晓我课堂

精通JVM(一)——JVM内存结构

2022-04-01  本文已影响0人  liushiping

一.JVM内存结构概览

JVM在运行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。JVM内存结构如下图所示,总体而言包括5大区域:方法区、虚拟机栈、本地方法栈、堆、程序计数器。


JVM内存结构

二.各个击破五大区域

1.方法区

方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在Java8中移除了永久代的内容,方法区由MetaSpace实现,并直接放到了本地内存中,不受JVM参数的限制。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
以下例子我们通过不断创建并加载新的类来触发OutOfMemoryError异常:

package cn.lsp.demo.jvm;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
public class MetaSpaceOOM extends ClassLoader{
    public static void main(String[] args) {
        int j = 0;
        try {
            MetaSpaceOOM test = new MetaSpaceOOM();
            for (int i = 0; i < 10000; i++) {
                //创建ClassWriter对象,用于生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                //指明版本号,修饰符,类名,包名,父类,接口
                classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回byte[]
                byte[] code = classWriter.toByteArray();
                //类的加载
                test.defineClass("Class" + i, code, 0, code.length);//Class对象
                j++;
            }
        } finally {
            System.out.println(j);
        }
    }
}

在64位虚拟机上,我们使用如下虚拟机参数运行应用程序:

-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:-UseCompressedClassPointers

从输出中我们可以看到,随着对象的不断生成和加载,最终出现OutOfMemoryError: Metaspace异常

2022-03-19T21:06:49.549-0800: [GC (Metadata GC Threshold) [PSYoungGen: 27528K->1864K(76288K)] 27528K->1872K(251392K), 0.0015480 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2022-03-19T21:06:49.551-0800: [Full GC (Metadata GC Threshold) [PSYoungGen: 1864K->0K(76288K)] [ParOldGen: 8K->1652K(120832K)] 1872K->1652K(197120K), [Metaspace: 9372K->9372K(10240K)], 0.0064921 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
2022-03-19T21:06:49.557-0800: [GC (Last ditch collection) [PSYoungGen: 0K->0K(76288K)] 1652K->1652K(197120K), 0.0008775 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2022-03-19T21:06:49.558-0800: [Full GC (Last ditch collection) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 1652K->1633K(227328K)] 1652K->1633K(303616K), [Metaspace: 9372K->9372K(10240K)], 0.0098603 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
9344
Heap
 PSYoungGen      total 76288K, used 1966K [0x00000007aab00000, 0x00000007b2a80000, 0x0000000800000000)
  eden space 65536K, 3% used [0x00000007aab00000,0x00000007aaceb9e8,0x00000007aeb00000)
  from space 10752K, 0% used [0x00000007af580000,0x00000007af580000,0x00000007b0000000)
  to   space 10752K, 0% used [0x00000007aeb00000,0x00000007aeb00000,0x00000007af580000)
 ParOldGen       total 227328K, used 1633K [0x0000000700000000, 0x000000070de00000, 0x00000007aab00000)
  object space 227328K, 0% used [0x0000000700000000,0x0000000700198738,0x000000070de00000)
 Metaspace       used 9404K, capacity 10208K, committed 10240K, reserved 10240K
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
    at cn.lsp.demo.jvm.MetaSpaceOOM.main(MetaSpaceOOM.java:20)

Process finished with exit code 1

2.Java堆

Java堆也是被所有线程共享的一块内存区域,是JVM所管理的内存中最大的一块,它在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。因此它是垃圾收集器管理的主要区域,由于现在的垃圾手集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代还细分为Eden空间、From Survivor空间、To Survivor空间。如果没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常。
以下例子演示堆内存OutOfMemoryError异常:

package cn.lsp.demo.jvm;
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
    static class OOMObject{

    }

    public static void main(String[] args) {
        List<OOMObject> objects = new ArrayList<>();
        while (true) {
            objects.add(new OOMObject());
        }
    }
}

我们使用如下虚拟机参数运行应用程序:

-Xms10M
-Xmx10M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

从输出中我们可以看到,随着在堆上不断创建对象分配内存,且无法回收,最终出现OutOfMemoryError: Java heap space异常

2022-03-19T21:42:43.994-0800: [GC (Allocation Failure) [PSYoungGen: 1926K->498K(2560K)] 1926K->822K(9728K), 0.0013184 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2022-03-19T21:42:43.999-0800: [GC (Allocation Failure) [PSYoungGen: 2546K->496K(2560K)] 2870K->2220K(9728K), 0.0023827 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2022-03-19T21:42:44.003-0800: [GC (Allocation Failure) [PSYoungGen: 2304K->512K(2560K)] 4029K->4038K(9728K), 0.0033042 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
2022-03-19T21:42:44.007-0800: [GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 6086K->6080K(9728K), 0.0034903 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
2022-03-19T21:42:44.011-0800: [Full GC (Ergonomics) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 5584K->4868K(7168K)] 6080K->4868K(9728K), [Metaspace: 3135K->3135K(1056768K)], 0.0623826 secs] [Times: user=0.36 sys=0.01, real=0.06 secs] 
2022-03-19T21:42:44.074-0800: [GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] 6916K->6947K(9728K), 0.0028029 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
2022-03-19T21:42:44.077-0800: [Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6435K->5964K(7168K)] 6947K->5964K(9728K), [Metaspace: 3152K->3152K(1056768K)], 0.0458218 secs] [Times: user=0.34 sys=0.00, real=0.05 secs] 
2022-03-19T21:42:44.123-0800: [Full GC (Ergonomics) [PSYoungGen: 1513K->479K(2560K)] [ParOldGen: 5964K->6927K(7168K)] 7478K->7407K(9728K), [Metaspace: 3174K->3174K(1056768K)], 0.0676921 secs] [Times: user=0.48 sys=0.01, real=0.07 secs] 
2022-03-19T21:42:44.191-0800: [Full GC (Allocation Failure) [PSYoungGen: 479K->479K(2560K)] [ParOldGen: 6927K->6909K(7168K)] 7407K->7389K(9728K), [Metaspace: 3174K->3174K(1056768K)], 0.0627649 secs] [Times: user=0.49 sys=0.00, real=0.06 secs] 
Heap
 PSYoungGen      total 2560K, used 545K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 2048K, 26% used [0x00000007bfd00000,0x00000007bfd887f8,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 7168K, used 6909K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
  object space 7168K, 96% used [0x00000007bf600000,0x00000007bfcbf6c0,0x00000007bfd00000)
 Metaspace       used 3213K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K
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:267)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
    at java.util.ArrayList.add(ArrayList.java:464)
    at cn.lsp.demo.jvm.ch01.HeapOOM.main(HeapOOM.java:14)

Process finished with exit code 1

3.虚拟机栈

与方法区、Java堆不同,Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行是同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈和出栈的过程。比如下面的例子,fun1调用fun2,fun2调用fun3,fun3创建Hello对象。

public void fun1() {
    fun2();
}

public void fun2() {
    fun3();
}

public void fun3() {
    Hello hello = new Hello();
}
虚拟机栈

虚拟机栈存在两种异常情况:如果线程请求的栈深度大于虚拟机所运行的深度,将抛出StackOverflowError异常;如果虚拟机栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
下面代码演示虚拟机栈StackOverflowError异常:

package cn.lsp.demo.jvm;

public class StackSOF {

    private int stackDeep = 1;

    public void stackLeak(){
        stackDeep++;
        stackLeak();
    }

    public static void main(String[] args) {
        StackSOF sof = new StackSOF();
        try {
            sof.stackLeak();
        } finally {
            System.out.println("栈深度:" + sof.stackDeep);
        }
    }
}

运行结果如下,当栈深度到15944(不同机器可能不同)时就会出现StackOverflowError异常:

栈深度:15944
Exception in thread "main" java.lang.StackOverflowError
    at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
    at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
    at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
    at cn.lsp.demo.jvm.StackSOF.stackLeak(StackSOF.java:9)
  ... ...

下面代码演示虚拟机栈OutOfMemoryError异常:

package cn.lsp.demo.jvm;
public class StackOOM {
    public void stackLeacByThread(){
        while (true) {
            new Thread(() -> {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    public static void main(String[] args) {
        StackOOM oom = new StackOOM();
        oom.stackLeacByThread();
    }
}

我们使用如下虚拟机参数运行应用程序:

-Xss2M

运行结果如下,通过不断创建线程,最终在栈上出现OutOfMemoryError异常:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:717)
    at cn.lsp.demo.jvm.StackOOM.stackLeacByThread(StackOOM.java:14)
    at cn.lsp.demo.jvm.StackOOM.main(StackOOM.java:21)

4.本地方法栈

本地方法栈与虚拟机栈所发生的作用几乎是一样的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常,此处不再赘述。

5.程序计数器

程序计数器是一块较小的内存区域,它可以看做是当前线程执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java是多线程的,在线程切换回来后,它需要知道原先的执行位置在哪里。就需要用程序计数器来记录这个执行位置的,保证线程间的计数器相互不影响,这个内存区域是线程私有的。
来一个简单的代码,计算(1+2)并返回

package cn.lsp.demo.jvm;

public class HelloWorld {

    public int add() {
        int a = 1;
        int b = 2;
        return a + b;
    }

}

这段代码经过编译再加载到虚拟机的时候,就变成了以下的字节码,虚拟机执行的时候,就会一行行执行。

william@liushipingdeMacBook-Pro Demo % javac JVM/src/main/java/cn/lsp/demo/jvm/HelloWorld.java   
william@liushipingdeMacBook-Pro Demo % javap -c JVM/src/main/java/cn/lsp/demo/jvm/HelloWorld  
警告: 二进制文件JVM/src/main/java/cn/lsp/demo/jvm/HelloWorld包含cn.lsp.demo.jvm.HelloWorld
Compiled from "HelloWorld.java"
public class cn.lsp.demo.jvm.HelloWorld {
  public cn.lsp.demo.jvm.HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int add();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: ireturn
}

三.总结

JVM整体工作流程如下:
1.通过类加载器将类加载到方法区;
2.创建对象时,会在堆内存中创建;
3.线程调用方法的时,会创建一个栈帧;
4.执行方法前,读取方法区的字节码执行指令;
5.执行指令时,会把执行的位置记录在程序计数器中;
6.方法执行完,这个栈帧就会出栈;

上一篇下一篇

猜你喜欢

热点阅读