JVM的组成的运行时数据区

JVM 由三大部分组成:

- 类加载器
- 运行时数据区
- 执行引擎

运行数据区也分五个小区域,两大类

区域名称线程共享线程私有用途说明
程序计数器当前执行指令位置
Java 虚拟机栈方法调用过程中的数据存储
本地方法栈本地方法调用
堆(Heap)所有对象存储
方法区/元空间类信息、常量、静态变量
内存区域存储内容生命周期
堆(Heap)所有对象实例、数组和JVM同生共死
栈(Stack)每个线程的方法调用栈帧随线程创建与销毁
方法区类的结构、常量池、静态变量等和JVM同生共死
本地方法栈Native方法调用信息随线程创建与销毁
程序计数器当前线程执行字节码的行号指示器随线程创建与销毁

程序技术器

程序计数器是 JVM 中的一块非常小的内存空间,用于记录当前线程所执行字节码的行号地址。

- 因为在Java虚拟机的多线程环境下,为了支持线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,否则就会出现线程切换后执行位置混乱的问题。
  • JAVA源代码会先编译成字节码指令,并由类加载器加载到方法区,程序计数器的作用就是记录下一步应该执行的字节码指令的地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
     0 iconst_0
    1 istore_1
    2 iload_1
    3 iinc 1 by 1
    6 istore_1
    7 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
    10 iload_1
    11 invokevirtual #13 <java/io/PrintStream.println : (I)V>
    14 return

    小结

    程序计数器
  • 作用:保存当前线程下一条要执行的指令的地址
  • 特点:
    • 线程私有
    • 不存在内存溢出

虚拟机栈

定义

  • Java虚拟机栈(Java Virtual Machine Stacks)是Java虚拟机为每个线程分配的一块内存区域,用于存储线程的方法调用和局部变量等信息。
  • 每个线程在运行时都有自己的Java虚拟机栈,线程开始时会创建一个新的栈帧(Stack Frame),用于存储该线程的方法调用信息。当方法调用完成后,该栈帧会被弹出,回到上一次方法调用的位置。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

虚拟机栈(一个线程)
┌────────────┐ ← 栈顶
│ 方法C栈帧 │ ← 当前正在执行的方法
├────────────┤
│ 方法B栈帧 │
├────────────┤
│ 方法A栈帧 │ ← 最先被调用的方法(main)
└────────────┘ ← 栈底

当我们看到程序报错出现异常的时候,就会从上到下依次弹出虚拟机栈,所以第一个弹出的就是关键的报错异常

栈帧

一个栈帧保存一个方法执行过程中的所有信息,包括:

组件说明
局部变量表存储方法的参数和局部变量(int、long、Object 引用等)
操作数栈执行字节码指令时用来做计算和临时数据存储
动态链接方法中调用其他方法时的符号引用信息(类似“函数指针”)
返回地址方法返回时跳转的字节码地址
附加信息如异常处理表等,供调试/异常处理用

栈内存溢出

栈帧太多导致栈内存溢出

无限递归或者递归太深会导致栈帧过多会导致栈内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo {
static int count = 0;

public static void main(String[] args) {
try {
method();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}

private static void method() {
count++;
method();
}
}

递归最终停在23678次出现了栈内存溢出的异常

  • 设置栈内存大小

单个栈帧过大

这个情况出现在 一个类A中引用了B类,B类中又引用了A类的时候,也就是循环引用的时候才会出现

本地方法栈

本地方法是指由非Java语言编写的代码,如C或C++,并被编译为本地二进制代码。
因为JAVA没法直接和操作系统底层交互,所以需要用到本地方法栈来调用本地的C或C++的方法
例如Object类的源码中就有本地方法,用native关键字修饰本地方法
本地方法只有函数声明,没有函数体,因为函数体是C或C++写的,通常是通过JNI(Java Native Interface)技术来实现的。

项目虚拟机栈(JVM Stack)本地方法栈(Native Method Stack)
作用管理 Java 方法的调用和执行管理 Native 方法的调用
内容栈帧(方法局部变量、操作数栈等)本地方法所需的结构、变量
是否必须存在不是所有 JVM 实现都支持(HotSpot 把它与 JVM Stack 合并)
触发异常StackOverflowErrorOutOfMemoryError同上,如果单独存在

✅ 定义:
JVM 堆是所有线程共享的一块内存区域,用于存放对象实例、数组等,只要用 new 创建的对象,基本都在堆中分配内存。

Heap堆

  • 通过new关键字创建的对象都会使用堆空间
  • 特点:
    • 它是线程共享的,堆空间内的对象都需要考虑线程安全的问题
    • 有垃圾回收机制

堆大小设置

可以通过 JVM 启动参数设置堆的初始大小和最大大小:

1
java -Xms512m -Xmx1024m MyApp

-Xms: 初始堆大小
-Xmx: 最大堆大小
注意:堆太小,容易频繁 GC;堆太大,Full GC 时间可能过长

堆溢出(OutOfMemoryError)

当堆内存不够用时,会出现:

1
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

常见原因:

  • 创建过多对象且未释放;
  • 内存泄漏(引用未置 null 导致 GC 无法回收);
  • 数据结构过大(如 List 无限添加元素);

arthas 工具可查看堆内存使用情况

方法区

✅定义:
在Java虚拟机(JVM)中,方法区(Method Area)是内存的一部分,属于JVM运行时数据区的一种。它用于存储类的结构信息,而不是对象的实例数据。方法区在JVM中有时也被称为永久代(PermGen)(在JDK 7及以前版本中),在JDK 8中被称为元空间(Metaspace)。

✅方法区主要存储以下内容:

  1. 类的结构信息(Class信息):
  • 类的完全限定名(类名 + 包名)
  • 类的访问修饰符(public、abstract等)
  • 父类名称
  • 实现的接口
  • 字段信息(字段名、类型、修饰符)
  • 方法信息(方法名、参数、返回值、修饰符)
  1. 运行时常量池(Runtime Constant Pool):
  • 字符串字面量(如 “Hello”)
  • 数字常量、方法引用、字段引用等
  1. 静态变量
  • 类的构造方法和普通方法的字节码

JVM会通过类加载器将类的信息加载到方法区,在类的加载期完成

PermGen与Metaspace的区别

特性PermGen(JDK 7及以前)Metaspace(JDK 8及以后)
所在内存JVM内存本地内存(Native memory)
是否容易内存溢出是(PermGen space OOM)较少(可通过参数控制增长)
可调参数-XX:PermSize-XX:MaxPermSize-XX:MetaspaceSize-XX:MaxMetaspaceSize

StringTable

StringTable 是一个专门存放 String.intern() 方法返回的字符串对象 的哈希表。

  • 来看一段代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Demo_08 {
    public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);
    String s5 = "a" + "b";
    System.out.println(s3 == s5);
    }
    }

    反编译结果如下,可以看到s5对象的创建,就是去常量池中直接获取ab,而不会创建新的字符串对象,故s3 == s5的结果是true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=3, locals=6, args_size=1
    0: ldc #2 // String a
    2: astore_1
    3: ldc #3 // String b
    5: astore_2
    6: ldc #4 // String ab
    8: astore_3
    9: new #5 // class java/lang/StringBuilder
    12: dup
    13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
    16: aload_1
    17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20: aload_2
    21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore 4
    29: ldc #4 // String ab
    31: astore 5
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池

    • 1.8中,将这个字符串对象尝试放入串池
      • 如果串池中已有,则不会放入
      • 如果串池中没有,则放入串池,并将串池中的结果返回
        下面是示例代码讲解
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        public class Demo_10 {

        public static void main(String[] args) {
        String s1 = "a"; // 常量池:["a"]
        String s2 = "b"; // 常量池:["a", "b"]
        String s3 = "a" + "b"; // 常量池:["a", "b", "ab"]
        String s4 = s1 + s2; // 堆:new String("ab")
        String s5 = "ab"; // s5引用常量池中已有的对象
        String s6 = s4.intern(); // 常量池中已有"ab",将常量池中的"ab"的引用返回,s6引用常量池中已有的对象

        System.out.println(s3 == s4); // s3在常量池,s4在堆,false
        System.out.println(s3 == s5); // s3在常量池,s5在常量池,true
        System.out.println(s3 == s6); // s3在常量池,s6在常量池,true

        String str1 = "cd"; // 常量池:["cd"]
        String str2 = new String("c") + new String("d"); // 堆:new String("cd")
        str2.intern(); // 常量池中已有"cd",放入失败
        System.out.println(str1 == str2); // str1在常量池,str2在堆,false

        String str4 = new String("e") + new String("f"); // 堆:new String("ef")
        str4.intern(); // 常量池中没有"ef",放入成功,并返回常量池"ef"的引用
        String str3 = "ef"; // 常量池:["ef"]
        System.out.println(str3 == str4); // str4是常量池的引用,str3也是常量池的引用,true
        }
        }