1.JVM内存结构
JVM内存结构分为5个区域:方法区,虚拟机栈,本地方法栈、堆、程序计数器。
1.方法区(Method Area):用于存储类的结构信息、常量、静态变量、即使编译器编译后的代码等数据。方法区也是所有线程共享的区域。
2. 堆(Heap):堆时Java虚拟机管理的最大的一块内存区域,用于存储对象实例。Java堆时所有线程共享的内存区域,在Jvm启动时就被创建。Java堆可以分为新生代(Young Generation)、老年代(Old Generation)等区域,用于实现垃圾回收。
3. 虚拟机栈(VM Stack):每个线程在创建时会被分配一个虚拟机栈,用于存储方法调用的栈帧。栈帧包含局部变量表、操作数栈、动态链接、方法返回地址等信息。在方法调用时,会创建一个新的栈帧压入虚拟机栈,并在方法返回时将栈帧出栈。
4. 本地方法栈(Nactive Method Stack):类似于虚拟机栈,用于支持本地方法(Native Method)的调用
5. 程序计数器(Program Counter Register):是一块较小的内存空间,可以看作时当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有一个独立的程序计数器,用于唇齿当前线程正在执行的指令地址。
2.方法区(Method Area)—线程共享
在进行类加载时,方法区会存储类的元数据信息。方法区内的类卸载很苛刻,所以正常是认为方法区内没有垃圾回收的。
类的卸载通常需要满足一定的条件,包括:
- 类实例的引用数量为零:即没有任何类的实例被引用,包括静态变量对该类的引用。
- 类的ClassLoader被卸载:ClassLoader负责加载类到内存中,当ClassLoader被卸载时,它加载的类也会被卸载。
- 没有被其他类引用:即没有其他类通过反射等方式引用该类。
在方法区,还有一个空间,运行时常量池(Runtime Constant Pool):是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
3.堆(Heap)—线程共享
堆(Heap)是Java虚拟机管理机管理的最大一块内存区域,用于存储对象实例。堆内存是所有线程共享的内存区域,在JVM启动时就被创建。在堆内存中,会被动态地分配内存,用于存储新创建的对象。
- 堆内存通常被划分为几个不同的区域,包括:
- 新生代(Young Generation):新创建的对象会被分配到新生代中。新生代通常使用复制算法(Copying),将内存分为两块,一块为Eden区,另一块有两个 Survivor 区,通常称为 Survivor0 和 Survivor1(或者叫做 From 区和 To 区)。
Survivor两个区是对称的,没有先后关系,所以在新生代的垃圾回收过程中,对象会从 Eden 区复制到其中一个 Survivor 区,然后经过多次垃圾回收后仍然存活的对象会被移动到另一个 Survivor 区。最终,经过多次垃圾回收后仍然存活的对象会被移动到老年代。因此,同一个 Survivor 区可能同时存在来自 Eden 区和另一个 Survivor 区的对象,而复制到老年代的对象只有从第一个 Survivor 区过来的对象。 - 老年代(Old Generation):老年代主要存储一些较大的、存活时间较长的对象。在新生代经过多次垃圾回收后仍然存活的对象会被移动到老年代。老年代通常使用标记-清除(Mark-Sweep)算法或标记-整理(Mark-Compact)算法进行垃圾回收。
- 新生代(Young Generation):新创建的对象会被分配到新生代中。新生代通常使用复制算法(Copying),将内存分为两块,一块为Eden区,另一块有两个 Survivor 区,通常称为 Survivor0 和 Survivor1(或者叫做 From 区和 To 区)。
-
在堆中的字符串常量池(String Pool)
字符串常量池用于存储字符串常量。在Java中,字符串常量通常在编译器就确定了其值,并且会被放入到字符串常量池中。当程序中需要使用相同内容的字符串常量时,虚拟机会直接从字符串常量池中获取,而不是创建一个新的字符串对象。
举个例子,对于JVM底层,String str = new String(“test”)创建对象流程:- “test”字符串:JVM会首先检查字符串常量池是否已经存在相同的字符串,如果存在则直接返回该对象的引用,否则就在字符串常量池中创建一个新的字符串对象。
- ‘new String(“test”)’这部分代码会在堆内存中创建一个新的字符串对象,即使字符串常量池已经存在了“test”的字符串常量。但因为使用了‘new’关键字,明确要求创建一个新的对象,而不是直接使用常量池中已有的对象。
因此,这一行代码会生成两个对象。而日常我们创建字符串常量值,直接使用String bb=“bbb”,这种创建方式只会在字符串常量池创建一个对象。
它和普通的堆内存有一些区别,例如,字符串常量池中的字符串对象是不可变的,而且具有一定的缓存机制,以提高字符串对象的复用率和性能。
4.JAVA虚拟机栈(VM Stack)—线程私有
首先知道:栈后进先出
虚拟机栈用于存储方法调用的信息,包括局部变量表、操作数栈、动态链接、方法返回地址等。
-
方法调用:每次方法调用时,都会创建一个新的栈帧压入虚拟机栈,方法执行结束后,对应的栈帧会被弹出。
public static void main(String[] args) { int a = 1; int b = 2; int sum = add(a, b); System.out.println("sum: " + sum); } public static int add(int a, int b) { return a + b; }
mian方法在jvm中的执行可以用以下步骤来描述:
- 调用 main 方法,创建 main 方法的栈帧。
- 在 main 方法中声明局部变量 a 和 b,并将值赋给它们。
- 调用 add 方法,创建 add 方法的栈帧。
- 在 add 方法中将 a 和 b 相加,并将结果返回。
- add 方法执行结束,其栈帧被弹出,返回到 main 方法。
- 在 main 方法中将 add 方法返回的结果存储到 sum 变量中。
- 打印 sum 的值。
- main 方法执行结束,其栈帧被弹出,程序结束执行。
-
局部变量:每个栈帧中都包含一个局部变量表,用于存储方法中的局部变量。局部变量表中存储的是基本数据类型和对象引用,不存储对象本身。
在 Java 中,对象的引用通常是存储在栈上的,而对象实例则存储在堆上。当我们创建一个对象时,实际上是在堆上分配了一块内存来存储对象的实例数据,并返回一个引用指向这个对象。这个引用会被存储在栈上,作为对堆中对象的引用。 -
操作数栈:每个栈帧都包含一个操作数栈,用于存储方法执行过程中的操作数。操作数栈用于执行方法的运算操作,如加减乘除等。
-
异常处理:虚拟机栈也用于异常处理。当方法出现异常时,虚拟机会查找虚拟机栈中的异常处理器,以确定如何处理异常。
虚拟机栈的大小可以通过 JVM 的启动参数来指定,例如 -Xss 参数用于指定每个线程的栈大小。虚拟机栈的大小会影响方法调用的深度,如果方法调用的层级过深,可能会导致栈溢出异常。
5. 本地方法栈(Native Method Stack)—线程私有
本地方法栈与虚拟机栈类似,用于支持Java调用本地方法(Native Method)的过程。本地方法栈也是线程私有的,每个线程在调用本地方法时都会创建一个对应的本地方法栈。
本地方法栈与虚拟机栈区别在于,虚拟机栈用于执行Java方法的Java字节码,而本地方法栈用于执行本地方法的机器码(Native Code)。本地方法是使用本地语言(如C、C++)编写的方法,通过Java的本地接口(JNI)调用。
本地方法栈与虚拟机栈的大小可以分别设置,并且本地方法栈的大小可能会影响到本地方法的调用。如果本地方法栈空间不足,可能会导致栈溢出异常。
在Java中,Native指的是使用其他编程语言(如C、C++)等编写的方法,这些代码通过Java的本地接口(JNI、Java Native Interface)来与Java代码进行交互。通过使用Native方法,Java程序可以调用本地系统的功能或者特定的硬件设备,从而实现更高级的功能和性能优化。
使用 Native 方法需要在 Java 中声明 Native 方法,并使用 native 关键字修饰,然后通过 JNI 在本地代码中实现这些 Native 方法。在运行时,Java 虚拟机会加载本地库,并通过 JNI 调用本地方法。需要注意的是,使用 Native 方法会降低程序的可移植性,并增加程序的复杂度,因此应该谨慎使用。
6. 程序计数器(Program Counter Register)—线程私有
程序计数器是一块较小的内存空间,也可以称为寄存器,在Java虚拟机中用于存储当前正在执行的字节码指令的地址或索引。在多线程环境下,每个线程都有一个独立的程序计数器,用于指示当前线程执行的位置。
在Java虚拟机中,程序计数器不会发生内存溢出(OutOfMemoryError)的情况,因为它只是一个指示器,并不会存储任何对象或数据。
程序计数器在不同情况下记录的内容略有不同:
-
执行Java方法时:程序计数器记录的是正在执行的虚拟机字节码指令的地址,即程序计数器存储的是下一条要执行的指令在方法内的偏移量。这样,当线程中断或切换后再恢复执行时,可以准确地知道接下来应该执行哪条指令。
举例:public class Example { public static void main(String[] args) { int a = 1; int b = 2; int sum = add(a, b); System.out.println("sum: " + sum); } public static int add(int a, int b) { return a + b; } }
假设 add 方法的字节码指令如下(简化表示):
- 将局部变量 a 压入操作数栈
- 将局部变量 b 压入操作数栈
- 执行加法操作
- 将结果存入局部变量表
- 返回结果
当程序执行到 add 方法时,程序计数器会记录当前执行的位置,比如可能会记录为第一个字节码指令的地址。随着方法的执行,程序计数器会逐步指向下一条要执行的字节码指令,以便虚拟机可以准确地控制方法的执行流程。
-
执行Native方法时:由于Native方法是由本地语言编写地,不是Java字节码,因此程序计数器通常是空的或者。
7.小结
JVM的内存结构只是JVM中很基础的一部分,但了解JVM的结构,对于我们学习JAVA的基础类很有帮助,比如说引用对象(栈)和实例对象(堆)的存储区域、String在JVM的存特殊设计(字符串常量池)、JAVA类初始化元数据加载的空间(方法区)、基本数据类型存储空间(栈)、静态变量(方法区)等等。
下面是jvm结构的总结: