Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为以下几个区域,并统称为运行时数据区
其中线程共享的区域有堆和方法区
线程不共享的区域有程序计数器、Java虚拟机栈和本地方法栈
程序计数器(Program Counter Register)是一个较小的内存空间,字节码解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令
每个线程都有一个独立的程序计数器,多线程环境下程序计数器用于跟踪每个线程执行的位置
如果线程正在执行的是一个Java方法,这个计数器记录的值是正在执行的虚拟机字节码指令的地址;乳沟正在执行的是本地native方法,这个计数器则值为空
在Java虚拟机规范中,这个内存区域是没有OutOfMemoryError情况的区域
Java虚拟机栈也是线程私有的,他的生命周期与线程相同;
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
栈帧是JVM栈的基本单位,每当调用一个方法时,就会为该方法创建一个新的栈帧,当方法返回时,该栈帧被弹出
在idea中编写如下方法
public class JvmStack {
public static void main(String[] args){
System.out.println("开始");
People people = new People();
people.eat();
people.sleep();
}
}
class People{
public void eat(){
System.out.println("eat");
}
public void sleep(){
System.out.println("sleep");
}
}
打上断点调试
最后main方法执行完成也会出栈
然后我们修改代码如下,让他抛出一个异常
public class JvmStack {
public static void main(String[] args){
System.out.println("开始");
People people = new People();
people.eat();
people.sleep();
}
}
class People{
public void eat(){
System.out.println("eat");
}
public void sleep(){
System.out.println("sleep");
throw new RuntimeException();
}
}
这里注意到我们在sleep方法中抛出一个异常,捕获信息如下
程序执行到异常发生的位置,main线程中只有两个栈帧,异常就是从栈顶把这些栈帧依次出栈,并显示对应信息
Java虚拟机栈(Java Virtual Machine Stack, JVM Stack)中的局部变量表是用来存放方法参数和方法内部定义的局部变量的数据结构。局部变量表对于每一个线程都是独立的,并且随着方法调用的开始而创建,随着方法调用结束而销毁
局部变量表的特点:
NullPointerException
或遇到未初始化的局部变量错误。参考如下代码
public class LocalVariable {
public void test1(){
int i=0;
int j=1;
}
}
编译后class文件方法内容如下,我们可以查看到字节码文件的局部变量表(这是Class文件的局部变量表,并不是真正的Java虚拟机中的局部变量表)
我们查看他的LocalVariableTable属性
图中观察到i的起始PC为2,长度为3,对应的其实时他在字节码中的定义行数和作用范围
Class文件的LocalVariableTable还有一个序号属性,这个序号就是变量的槽位,这里有i和j两个变量,序号依次是1和2,我们更改代码如下
public class LocalVariable {
public void test1(){
int i=0;
long j=1L;
int k=0;
}
}
再看他的序号,发现k的序号是4,就是因为j的类型为long占据了两个槽位2和3
同样的方法参数也会占用局部变量槽
操作数栈用于存储方法执行过程中的中间结果,例如算术运算的结果、方法调用的参数和返回值等,Java字节码指令集中有许多指令用于操作操作数栈,例如 aload
, istore
, iadd
等如果在执行字节码指令时操作数栈为空,而该指令需要从栈中弹出值,则会抛出 StackUnderflowError
异常
考虑下面这个简单的Java方法:
public static int add(int a, int b) {
return a + b;
}
作流程:
add
被调用时,一个新的栈帧被创建,其中包括操作数栈和局部变量表。a
和 b
被压入局部变量表。aload_0
(加载局部变量表中的第一个整型变量到操作数栈)。aload_1
(加载局部变量表中的第二个整型变量到操作数栈)。iadd
(执行栈顶两个整型值的加法运算,将结果放回操作数栈)。ireturn
(将操作数栈顶部的整型值作为方法的返回值)。Java虚拟机栈(JVM Stack)中的栈帧不仅包含了局部变量表和操作数栈等数据结构,还包括了动态链接信息、方法出口信息和异常表。这些信息对于支持方法调用和返回过程、异常处理至关重要。
在方法调用过程中,动态链接的主要作用是记录方法调用链中的上下文信息,以便在方法返回时能够正确地回到调用者。具体来说,它可以帮助JVM找到方法调用链上的上一层方法,从而支持方法调用的返回和异常处理
动态链接的实现
如下代码
public class Example {
public static void main(String[] args) {
foo();
}
public static void foo() {
bar();
}
public static void bar() {
// 执行一些操作
}
}
在这个例子中,当执行 foo()
方法时,JVM 会在栈中创建一个栈帧,并设置动态链接信息指向 main()
方法的栈帧。当 bar()
方法被调用时,又会在栈中创建一个新的栈帧,并设置动态链接信息指向 foo()
方法的栈帧。
当 bar()
方法完成执行并返回时,JVM 会使用动态链接信息来恢复 foo()
方法的栈帧状态,并继续执行 foo()
方法剩余的部分。同样,当 foo()
方法返回时,会恢复 main()
方法的栈帧状态。
通过这种方式,动态链接确保了方法调用链的正确管理和恢复,支持了Java语言中的方法调用和异常处理机制。
方法出口是指当方法执行完毕或发生异常时,如何从当前方法返回到调用者的方法。这涉及到保存返回值、恢复调用者状态以及跳转到正确的指令位置
正常返回
当方法正常执行完毕时,JVM 会执行以下步骤:
ireturn
, lreturn
, freturn
, dreturn
, 或 areturn
指令之一,这取决于返回值的数据类型。这些指令会弹出栈顶的返回值并将其复制到调用者的操作数栈中。异常处理
当方法抛出异常时,JVM 会执行以下步骤:
如下代码
public class Example {
public static void main(String[] args) {
try {
foo();
} catch (Exception e) {
System.out.println("Caught exception: " + e.getMessage());
}
}
public static void foo() throws Exception {
throw new Exception("An exception occurred");
}
}
在这个例子中,当执行 foo()
方法时,如果抛出了异常,JVM 会执行以下步骤:
foo()
方法抛出了 Exception
。main()
方法)中的异常处理器。main()
方法中的异常处理器,JVM 会恢复 main()
方法的栈帧状态,并跳转到异常处理器的起始位置继续执行。异常表是一组关于方法如何处理异常的信息。当一个方法中定义了 try-catch
块时,异常表中会记录下这些块的起始位置、结束位置以及异常处理器的位置。
异常表的作用:
catch
块可以处理相同的异常,JVM会选择最具体的那个。如下代码
public class Example {
public static void main(String[] args) {
try {
foo();
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException: " + e.getMessage());
} catch (Exception e) {
System.out.println("Caught Exception: " + e.getMessage());
}
}
public static void foo() {
int a = 10;
int b = 0;
int c = a / b; // 抛出 ArithmeticException
}
}
在这个例子中,main()
方法包含两个异常处理器:
ArithmeticException
类型的异常。Exception
类型的异常,这是一个更通用的异常处理器。字节码内容如下
字节码文件异常表信息如下
这里,异常表包含两个表项:
ArithmeticException
,则跳转到第6条指令。编写代码
public class Example {
public static int count = 0;
public static void main(String[] args) {
foo();
}
public static void foo() {
System.out.println(++count);
foo();
}
}
这个代码不断调用foo(),就会一直创建栈帧,最终导致 StackOverflowError
栈的内存溢出
-Xss : 这个参数用来设置每个线程的栈大小
单位:字节(默认,必须是1024的倍数)、k、m、g
下述代码在执行时,默认创建的的栈帧数量大概是一万多个,因为jvm默认的Xss值是1M
public class Example {
public static int count = 0;
public static void main(String[] args) {
foo();
}
public static void foo() {
System.out.println(++count);
foo();
}
}
此时,我们设置Xss为512k
在执行就发现栈帧数量少了很多
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,所有对象实例以及数组都应该在堆上分配,java堆是垃圾收集器管理的内存区域
-Xms
参数设置。-Xmx
参数设置。单位:字节(默认,必须是1024的倍数)、k、m、g
内存溢出会抛出OutOfMemoryError异常
编写如下代码
public class Example {
public static void main(String[] args) {
while(true){
}
}
}
启动arthas查看内存
方法区是JVM内存模型中的一个组成部分,它用于存储每个类的信息,包括类的方法和变量的数据类型、访问修饰符等信息,以及常量池(运行时常量池、字符串常量池)、静态变量等
-XX:MaxPermSize
。当永久代空间不足时,会抛出 OutOfMemoryError: PermGen space
错误。OutOfMemoryError: Metaspace
。通过 -XX:MaxMetaspaceSize
参数可以设置元空间大小方法区内存溢出演示代码如下
引入依赖
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.23</version>
</dependency>
编写代码
public class Demo1 extends ClassLoader{
public static void main(String[] args){
Demo1 demo1 = new Demo1();
int count = 0;
while(true){
String className = "Class"+count;
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V11,Opcodes.ACC_PUBLIC,className,null,"java/lang/Object",null);
byte[] bytes = classWriter.toByteArray();
demo1.defineClass(className,bytes,0,bytes.length);
System.out.println(++count);
}
}
}
设置一个虚拟机参数 -XX:MaxMetaspaceSize=128M
启动运行一端时间后,爆出内存溢出错误
ExampleClass
,它包含了一个字符串常量 "constant"
和一个整型常量 42
,那么这些信息都会存储在该类的运行时常量池中。实例:
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出 true
JVM直接内存(Direct Memory)是一种特殊的内存区域,它不属于JVM的常规内存区域(如堆、方法区等),而是直接分配在本地内存(Native Memory)中的内存。直接内存是由Java NIO(New I/O)包提供的,它允许Java应用程序直接访问本地内存,从而提高I/O操作的性能。
直接内存的特点:
java.nio.ByteBuffer
类的 allocateDirect()
方法分配的。这使得直接缓冲区能够直接在本地内存中创建和操作,而不需要通过JVM堆进行复制。-XX:MaxDirectMemorySize
: 设置最大直接内存大小。如果不设置,默认值通常是系统物理内存的1/4。编写如下代码
public class Demo1 extends ClassLoader{
public static void main(String[] args){
List<ByteBuffer> list = new ArrayList<>();
while(true){
// 分配直接内存缓冲区
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024*1024*100);
list.add(directBuffer);
}
}
}
运行后爆出错误
打开arthas通过memory命令,可以看到直接内存
设置一个 -XX:MaxDirectMemorySize=256M
在启动如下程序
public class Demo1 extends ClassLoader{
public static void main(String[] args){
List<ByteBuffer> list = new ArrayList<>();
int count = 0;
while(true){
// 分配直接内存缓冲区
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024*1024*100);
list.add(directBuffer);
System.out.println(++count);
}
}
}
代码中每次分配100m内存,然后我们设置了最大直接内存只有256M因此,分配两次后直接报错了