JVM内存模型剖析与参数设置

目录

Java语言的跨平台特性

JVM 的主要组成部分及其作用是什么?

JVM整体结构及内存模型

线程栈(Machine Stack)

局部变量表(Local Variable Table)

操作数栈(Operand Stack)

程序计数器(Program Counter Register)

动态链接(Dynamic Linking)

方法出口(Method Exit)

堆(Heap)

栈(Stack)

本地方法栈(Native Method Stack)

方法区(Method Area)

在minor gc过程中对象挪动后,引用如何修改?

JVM内存参数设置


Java语言的跨平台特性

Java语言具有跨平台特性,这意味着可以编写一次Java代码,并在不同的操作系统和硬件平台上运行,而无需进行大量的修改或重新编译。

  1. Java虚拟机(Java Virtual Machine):Java程序在运行时被编译为字节码(bytecode),而不是特定于某个平台的机器代码。字节码可以在任何支持Java虚拟机的平台上运行,因此Java程序可以在Windows、Mac、Linux等各种操作系统上执行。
  2. Write Once, Run Anywhere(一次编写,到处运行):通过Java的跨平台特性,可以编写一次Java代码,然后在不同的平台上运行。只需将编译后的字节码文件(.class文件)拷贝到目标平台,并确保目标平台上有合适版本的Java虚拟机,就可以在该平台上运行程序。
  3. 平台无关的标准库:Java提供了丰富的标准库,涵盖了各种常用功能和API。这些库是与平台无关的,因此可以在任何支持Java虚拟机的平台上使用。
  4. 自动内存管理:Java通过垃圾回收器(Garbage Collector)自动管理内存,开发人员无需手动分配和释放内存。这有助于减少与特定平台相关的内存管理问题。
  5. 安全性:Java具有内置的安全机制,可以防止恶意代码对系统的损害。Java的安全性模型包括类加载器、安全管理器和沙箱环境,可以确保代码在不同平台上以安全的方式运行。

JVM 的主要组成部分及其作用是什么?

1、类加载器(Class Loader):类加载器负责将字节码文件(.class文件)加载到内存中,并将其转换为可执行的Java类,并在运行时动态链接这些类。它根据类的全限定名查找和加载字节码文件,并生成类的运行时数据结构,供JVM使用。

它分为三个主要的层次:

  • Bootstrap Class Loader
  • Application Class Loader
  • Extension Class Loader

2、执行引擎(Execution Engine):执行引擎负责解释和执行Java字节码指令。它将字节码转换为可执行的机器指令,并执行这些指令。

它包含两个主要的部分:

  • 解释器(Interpreter):逐行解释执行字节码指令,实现跨平台的特性,但执行速度相对较慢。
  • 即时编译器(Just-In-Time Compiler,JIT):将字节码转换为本地机器码,这样可以在后续的执行中直接运行本地机器码,提高执行速度。

3、运行时数据区(Runtime Data Area):运行时数据区是JVM中的内存区域,用于存储各种数据和程序运行时所需的数据。主要包括方法区、堆、栈、程序计数器和本地方法栈等。

  • 方法区(Method Area):方法区用于存储类的结构信息、常量、静态变量和字节码等。它是被所有线程共享的内存区域。
  • 垃圾回收器(Garbage Collector):垃圾回收器负责自动回收不再被使用的对象,并释放它们占用的内存空间。它通过检测和标记不可达对象,并回收它们的内存,以供后续的对象分配使用。
  • 本地方法栈(Native Method Stack):本地方法栈用于执行本地方法(Native Method)。本地方法是使用其他编程语言(如C、C++)编写的方法,通过Java本地接口(JNI)与Java代码进行交互。
    • 本地接口(Native Interface):允许Java代码调用本地库中的方法,也允许本地库调用Java代码。这样可以在Java中使用其他语言编写的库,实现与底层系统的交互。
  • 程序计数器(Program Counter Register):程序计数器用于存储当前线程正在执行的字节码指令的地址或索引。它在方法调用、执行和返回过程中发挥重要作用。
  • 栈(Stack):栈用于管理方法的调用和执行过程。每个线程在执行时都会有一个栈,用于存储局部变量、方法调用、返回信息和操作数栈等。
  • 堆(Heap):堆用于存储对象实例和数组。在Java中,所有通过new关键字创建的对象都存储在堆上,堆是Java动态分配内存的主要区域。也是垃圾回收的主要区域。

JVM的主要作用是提供一个可移植、安全和高性能的运行环境,使得Java程序可以在不同的操作系统和硬件平台上运行。它负责加载、解释和执行Java字节码,提供内存管理和垃圾回收、线程管理、异常处理、安全检查等功能。JVM的设计和实现使得Java具备了跨平台性、可移植性和安全性的优势。

JVM整体结构及内存模型

线程栈(Machine Stack)

每个线程在执行时都有自己的线程栈,线程栈独立于其他线程,互不共享。

线程栈的主要作用如下:

  1. 方法调用和执行:线程栈用于管理方法的调用和执行过程。每当一个方法被调用时,JVM会在线程栈上创建一个新的栈帧(Stack Frame),栈帧用于存储方法的参数、局部变量和中间结果。方法执行期间,栈帧会保持在栈顶,直到方法执行完毕。
  2. 局部变量存储:线程栈用于存储方法的局部变量。每个方法在执行时,会分配一定的栈空间用于存储方法的参数和局部变量。局部变量包括方法的参数和方法内部声明的变量,它们的生命周期仅限于方法的执行期间。
  3. 递归调用支持:线程栈支持方法的递归调用。当一个方法在执行过程中调用自身或其他方法时,每次调用都会在线程栈上创建一个新的栈帧,用于保存方法调用的上下文信息。
//运行main方法时,创建线程,Java虚拟机为每个线程创建的一块内存区域,用于支持线程的方法调用和执行。
//main方法在线程栈内开辟属于自己的独立内存空间(栈帧),存放math;执行compute方法时,会另外开辟一块compute的独立内存空间(栈帧),存放a、b、c。如上图所示
//栈采用后进先出(LIFO)的数据结构,所以后进栈的compute方法会先执行完成(出栈释放内存)
package com.bubble.jvm;
public class Math {
    public static final int initData = 666;
    public static User user = new User();

    public int compute() {  //一个方法对应一块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        System.out.println("test");
    }
}

生成反汇编文件的指令: javap -c Math.class > Math.txt

Compiled from "Math.java"
public class com.bubble.jvm.Math {
  public static final int initData;

  public static com.bubble.jvm.User user;

  public com.bubble.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int compute();
    Code:
       0: iconst_1     //将int类型常量1压入操作数栈
       1: istore_1     //将int类型值存入局部变量1
       2: iconst_2     //将int类型常量2压入操作数栈
       3: istore_2     //将int类型值存入局部变量2
       4: iload_1      //从局部变量1中装载int类型值 
       5: iload_2      //从局部变量2中装载int类型值
       6: iadd         //执行int类型的加法
       7: bipush        10    //将一个8位带符号整数压入栈(将10压入栈)
       9: imul         //执行int类型的乘法
      10: istore_3     //将int类型值存入局部变量3
      11: iload_3      //从局部变量3中装载int类型值
      12: ireturn      //从方法中返回int类型的数据

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/bubble/jvm/Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: ldc           #6                  // String test
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return

  static {};
    Code:
       0: new           #8                  // class com/bubble/jvm/User
       3: dup
       4: invokespecial #9                  // Method com/bubble/jvm/User."<init>":()V
       7: putstatic     #10                 // Field user:Lcom/bubble/jvm/User;
      10: return
}

局部变量表(Local Variable Table)

JVM运行时数据区中的一个部分,用于存储方法中的局部变量和方法参数的值。每个方法在执行时都会创建一个局部变量表。

操作数栈(Operand Stack)

是JVM中的一块内存区域,用于存储方法执行过程中操作数和中间结果的内存区域。每个线程在执行方法时都会拥有自己的操作数栈。

操作数栈采用后进先出(LIFO)的数据结构,它可以执行一些基本的算术和逻辑操作。

操作数栈的主要作用如下:

  1. 存储方法执行过程中的操作数:当方法需要进行计算时,操作数栈用于存储方法所需的操作数,如整数、浮点数、对象引用等。
  2. 保存中间结果:在方法执行过程中,操作数栈可以保存计算的中间结果,以便后续的操作使用。
  3. 方法调用的参数传递:在方法调用时,参数会被压入操作数栈中,供被调用方法使用。
  4. 支持方法调用和返回:操作数栈在方法调用和返回过程中发挥重要作用。当方法调用其他方法时,当前方法的操作数栈可以用来传递参数;而在方法返回时,返回值会被压入操作数栈中。

操作数栈的大小是由编译器在编译阶段根据字节码指令的需求进行计算,并在方法运行时创建。栈帧(Stack Frame)中的局部变量和操作数栈共同组成了方法的运行时数据区。

程序计数器(Program Counter Register)

程序计数器是JVM中的一块内存空间,它是线程私有的,即每个线程都有自己独立的程序计数器。

作用:用来存储当前线程正在执行的字节码指令的地址或索引。每个线程都有自己的程序计数器,它们独立地记录各自线程的执行位置。

  1. 字节码解释器工作的依据:解释器通过不断地读取程序计数器的值,获取下一条需要执行的字节码指令。这样可以实现字节码的逐行解释执行。
  2. 线程切换的恢复点:由于每个线程都有自己的程序计数器,当线程切换时,JVM可以通过程序计数器的值恢复到上次执行的位置,以便继续执行。
  3. 异常处理的依据:在异常处理过程中,程序计数器能够记录异常处理器的返回地址,以确保异常处理后能够正确返回到原来的执行点。
  4. 方法间的跳转依据:当方法调用其他方法时,程序计数器可以用来保存调用点的信息,以便方法执行完成后能够返回到正确的调用位置。

动态链接(Dynamic Linking)

动态链接是通过类加载器(Class Loader)和运行时的链接过程实现的,它实现了类、字段、方法等符号引用与具体内存地址的绑定,并在程序的执行期间动态进行。当Java程序在JVM中执行时,类加载器负责将字节码文件(.class文件)加载到内存中,并在需要时进行链接操作。

动态链接在Java中的实现方式如下:

  1. 类加载过程:类加载器负责将字节码文件加载到内存中,并对类进行初始化。这个过程包括了加载、验证、准备、解析和初始化等步骤。在加载阶段,类加载器会根据类的全限定名找到对应的字节码文件,并将其加载到内存中。
  2. 链接过程:
    1. 验证(Verification):验证阶段确保加载的字节码符合Java虚拟机规范,不会造成安全漏洞和不一致性。
    2. 准备(Preparation):准备阶段为类的静态变量分配内存空间,并设置默认初始值。
    3. 解析(Resolution):解析阶段将符号引用转换为直接引用,即将类、字段、方法等符号引用解析为具体的内存地址。
  3. 运行时链接:在Java程序执行过程中,JVM会动态地将类、字段、方法等符号引用与具体的内存地址进行绑定。这个过程发生在程序的执行期间,根据需要进行动态链接。

Java的动态链接具有以下特点:

  • 延迟加载:类的加载是在需要时进行的,避免了不必要的加载和初始化过程。
  • 运行时绑定:方法的绑定是在程序执行期间动态进行的,根据实际的类和对象进行绑定,实现多态特性。
  • 灵活性:由于动态链接是在运行时进行的,可以根据运行环境和需求进行动态替换和更新,提供了更高的灵活性和可扩展性。

方法出口(Method Exit)

方法出口指的是方法执行结束后的返回点,即方法返回时程序要继续执行的位置。可以是方法返回指令的下一条指令或调用方(方法调用者)的位置。取决于方法的正常返回、异常抛出或非正常返回情况。

方法出口由以下几种情况确定

  1. 方法正常返回:当方法执行完毕并顺利返回时,程序将继续执行调用方的下一条指令。方法返回指令(return)将方法出口指定为调用方的位置
  2. 方法抛出异常:如果方法在执行过程中抛出异常,并且没有被当前方法处理,则异常将传播到调用方。此时,方法出口也是指向调用方的位置。
  3. 方法非正常返回:在某些情况下,方法可能会发生非正常的返回,比如遇到了异常、使用了非局部跳转指令(如goto)等。这种情况下,方法出口的位置取决于具体的非正常返回情况。

注意:方法出口并不一定是方法的末尾,它可以在方法的任意位置,因为方法可能会在中间通过返回指令或异常抛出来提前结束。

堆(Heap)

堆是一块用于动态分配对象的运行时数据区域。它是Java程序运行时内存管理的重要组成部分。

堆是用来存放对象和数组的内存空间,几乎所有的Java对象、数组都存储在JVM的堆内存中。当我们使用new关键字创建对象时,对象实例就会被分配在堆上。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,便于后续的执行。

Java堆具有以下特点:

  1. 动态分配:堆的内存空间在程序运行期间动态分配和扩展。当我们创建对象时,JVM会根据对象的大小动态分配堆中的内存空间,并保证对象之间没有碎片化的空闲空间。
  2. 垃圾回收:由于堆存储了所有的Java对象实例和数组,Java堆也是垃圾回收器(Garbage Collector)的主要工作区域。当对象不再被引用时,垃圾回收器会自动回收这些对象占用的内存空间,以便后续的对象分配使用。
  3. 对象生命周期:堆中的对象生命周期不受方法的作用域限制,即对象可以在不同的方法中被引用和操作。对象的生命周期由程序的逻辑控制和引用关系决定。

通过调整JVM参数,可以控制Java堆的大小。常见的参数包括-Xms(初始堆大小)和-Xmx(最大堆大小)等。

注意:堆是Java虚拟机管理的内存区域,与操作系统的堆内存并不完全一样。Java堆只是虚拟机规范中定义的概念,并不一定直接对应操作系统堆内存。

栈(Stack)

栈是用于存储方法调用和局部变量的一块内存区域。每个线程在执行方法时都会拥有自己的栈。

栈采用后进先出(LIFO)的数据结构,用于管理方法的调用和执行过程。每当一个方法被调用时,JVM会在栈上创建一个栈帧(Stack Frame),栈帧用于存储方法的局部变量、操作数栈、动态链接和返回地址等信息。栈在方法执行期间动态分配和释放内存,是实现方法调用和局部变量存储的关键组成部分。

栈的主要作用如下:

  1. 方法调用和返回:栈用于管理方法的调用和返回过程。当一个方法被调用时,JVM会在栈上创建一个新的栈帧,将方法的参数、局部变量等信息存储在栈帧中。方法执行期间,栈帧会保持在栈顶,用于存储方法执行所需的局部变量和中间结果。当方法执行完毕后,栈帧被销毁,返回到上一个方法的执行点。
  2. 局部变量存储:栈用于存储方法的局部变量。每个方法都有自己的局部变量空间,用于存储方法中定义的局部变量。局部变量包括方法参数和方法内部声明的变量,它们的生命周期仅限于方法的执行期间。
  3. 操作数栈:栈也用于存储方法执行过程中的操作数栈(Operand Stack)。操作数栈用于执行方法中的运算操作,如算术运算、逻辑运算等。方法执行时,操作数栈存储方法所需的操作数和中间结果。

注意:栈的大小是有限的,并且在JVM启动时就被预先设置好了。当栈空间不足时,会抛出StackOverflowError异常。

本地方法栈(Native Method Stack)

本地方法栈是一块用于执行本地方法(Native Method)的内存区域。它用于调用本地方法、分配本地方法的局部变量和管理本地方法执行过程。本地方法是使用其他编程语言(如C、C++)编写的方法,通过Java本地接口(Java Native Interface,JNI)与Java代码进行交互。

与Java虚拟机栈类似,本地方法栈采用后进先出(LIFO)的数据结构。每当一个本地方法被调用时,JVM会在本地方法栈上创建一个新的栈帧,用于存储本地方法的参数和局部变量。本地方法执行期间,栈帧会保持在栈顶。每个线程在执行本地方法时都会拥有自己的本地方法栈。

本地方法栈的主要作用是为本地方法的执行提供支持,包括以下功能:

  1. 调用本地方法:当Java代码调用本地方法时,JVM会将本地方法的参数传递到本地方法栈中,准备进行本地方法的执行。
  2. 分配本地方法的局部变量:本地方法栈用于分配本地方法所需的局部变量空间,存储本地方法中定义的局部变量。
  3. 本地方法执行过程的管理:本地方法栈负责管理本地方法的调用和执行过程。它保存了本地方法的返回地址、动态链接和异常处理信息等。

注意:不同于Java虚拟机栈,它是Java虚拟机为了支持本地方法的调用而提供的专门内存区域。

方法区(Method Area)

在Java虚拟机(JVM)中,方法区是一块用于存储类信息、静态变量、常量、方法字节码等数据的内存区域。它是Java程序运行时内存管理的一部分。

方法区是被所有线程共享的内存区域,存储了加载的类信息和类的运行时常量池。它与堆不同,方法区不存储对象实例,而是存储类级别的数据。

方法区的主要作用如下:

  1. 存储类信息:方法区存储加载的类的结构信息,包括类的字段、方法、构造函数、父类、接口等。这些信息在类加载过程中被加载到方法区,并被JVM用于类的验证、准备、解析等操作。
  2. 运行时常量池:方法区中有一个运行时常量池(Runtime Constant Pool),它是每个类或接口在编译期间生成的常量池的运行时表示。运行时常量池包含类中的常量、字面量、符号引用等。方法区中的运行时常量池在类加载时被创建,存储了编译期生成的常量信息,供JVM在运行时使用。
  3. 静态变量和静态方法:方法区存储了类的静态变量(static variable)和静态方法(static method)。静态变量是在类加载时初始化的,并且在整个程序执行期间都存在于方法区中。

注意:方法区的大小在JVM启动时就被固定下来,并且通常比堆要小。方法区的大小可以通过JVM参数进行调整,如-XX:MaxMetaspaceSize。在Java 8及之前的版本,方法区被实现为永久代(Permanent Generation)。但在Java 8及以后的版本,永久代被元数据区(Metaspace)取代,元数据区是一块位于本地内存中的区域,与堆内存分开。

在minor gc过程中对象挪动后,引用如何修改?

对象在堆内部挪动的过程其实是复制,原有区域对象还在,一般不直接清理,JVM内部清理过程只是将对象分配指针移动到区域的头位置即可,比如扫描s0区域,扫到gcroot引用的非垃圾对象是将这些对象复制到s1或老年代,最后扫描完了将s0区域的对象分配指针移动到区域的起始位置即可,s0区域之前对象并不直接清理,当有新对象分配了,原有区域里的对象也就被清除了。

minor gc在根扫描过程中会记录所有被扫描到的对象引用(在年轻代这些引用很少,因为大部分都是垃圾对象不会扫描到),如果引用的对象被复制到新地址了,最后会一并更新引用指向新地址。

JVM内存参数设置

#Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

-Xss:每个线程的栈大小

-Xms:设置堆的初始可用大小,默认物理内存的1/64

-Xmx:设置堆的最大可用大小,默认物理内存的1/4

-Xmn:新生代大小

-XX:NewRatio:默认2表示新生代占老年代的1/2,占整个堆内存的1/3。

-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。

关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N

-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。

-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。

由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

StackOverflowError示例:

// JVM设置  -Xss128k(默认1M),
public class StackOverflowTest {
    static int count = 0;
    static void redo() {
        count++;
        redo();
    }
    
    public static void main(String[] args) {
        try {
            redo();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println(count);
        }
    }
}

//运行结果:
java.lang.StackOverflowError
    at com.bubble.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)
    at com.bubble.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
    at com.bubble.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
   ......

结论:

-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多

尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值