JVM内存模型

简介: JAVA的主旨是它著名的WOTA:“一次编写,随处运行”。为了应用它,Sun Microsystems创建了Java虚拟机,这是解释已编译的Java代码的基础操作系统的抽象。**JVM**是JRE(Java运行时环境)的核心组件,是为运行Java代码而创建的,但现在被其他语言(Scala,Groovy,JRuby,Closure...)使用。在本文中,我将重点介绍 JVM 规范中描述的**运行时数据区域**。这些区域旨在存储程序或 JVM 本身使用的数据。我将首先介绍JVM的概述,然后是字节码是什么,并以不同的数据区域结束。# 全球概况JVM 是底层操作系统的抽象。它确保相同的代码

JAVA的主旨是它著名的WOTA:“一次编写,随处运行”。为了应用它,Sun Microsystems创建了Java虚拟机,这是解释已编译的Java代码的基础操作系统的抽象。JVM是JRE(Java运行时环境)的核心组件,是为运行Java代码而创建的,但现在被其他语言(Scala,Groovy,JRuby,Closure...)使用。

在本文中,我将重点介绍 JVM 规范中描述的运行时数据区域。这些区域旨在存储程序或 JVM 本身使用的数据。我将首先介绍JVM的概述,然后是字节码是什么,并以不同的数据区域结束。

全球概况

JVM 是底层操作系统的抽象。它确保相同的代码将以相同的行为运行,无论JVM在什么硬件或操作系统上运行。例如:

  • 无论 JVM 是否在 16 位/32 位/64 位操作系统上运行,基元类型 int 的大小将始终为从 -2^31 到 2^31-1 的 32 位有符号整数。
  • 每个 JVM 都以大端顺序(其中高字节优先)在内存中存储和使用数据,无论底层操作系统/硬件是大端还是小端序。

注意:有时,JVM 实现的行为与另一个 JVM 实现不同,但通常是相同的。



下图给出了 JVM 的概述:

  • JVM 解释由编译类的源代码生成的字节码。虽然术语JVM代表“Java虚拟机”,但它可以运行其他语言,如scala或groovy,只要它们可以编译成java字节码。
  • 为了避免磁盘 I/O,字节码由其中一个运行时数据区域中的类装入器加载到 JVM 中。此代码将保留在内存中,直到 JVM 停止或类装入器(装入它)被销毁。
  • 然后,加载的代码执行引擎解释和执行
  • 执行引擎需要存储数据,就像指向正在执行的代码的指针一样。它还需要存储开发人员代码中处理的数据。
  • 执行引擎还负责处理底层操作系统。

注意:许多 JVM 实现的执行引擎不会总是解释字节码,而是将字节码编译为本机代码(如果经常使用)。它被称为Just In Time(JIT)编译,大大加快了JVM的速度。编译的代码临时保存在通常称为代码缓存的区域中。由于该区域不在 JVM 规范中,因此在本文的其余部分我不会讨论它。


基于堆栈的架构

JVM 使用基于堆栈的体系结构。虽然它对开发人员来说是不可见的,但它对生成的字节码和JVM架构有巨大的影响,这就是为什么我将简要解释这个概念。

JVM通过执行Java字节码中描述的基本操作来执行开发人员的代码(我们将在下一章中看到它)。操作数是指令操作的值。根据 JVM 规范,这些操作要求通过称为操作数堆栈的堆栈传递参数。

例如,让我们取 2 个整数的基本相加法。此操作称为 iadd(对于 integer addition)。如果要在字节码中添加 3 和 4:

  • 他首先在操作数堆栈中推送 3 和 4。
  • 然后调用 iadd 指令。
  • iadd 将从操作数堆栈中弹出最后 2 个值。
  • int 结果 (3 + 4) 被推送到操作数堆栈中,以便其他操作使用。

这种工作方式称为基于堆栈的体系结构。还有其他方法可以处理基本操作,例如,基于寄存器的体系结构将操作数存储在小型寄存器中,而不是堆栈中。这种基于寄存器的架构由桌面/服务器(x86)处理器和以前的Android虚拟机Dalvik使用。


字节码

由于JVM解释字节码,因此在深入研究之前了解它是什么很有用。

java字节码是转换为一组基本操作的java源代码。每个操作由一个表示要执行的指令的字节(称为操作码操作代码)以及零个或多个用于传递参数的字节组成(但大多数操作使用操作数堆栈来传递参数)。在 256 个可能的一字节长的操作码(从值 0x00 到十六进制的 0xFF)中,有 204 个目前在 java8 规范中使用。

下面是不同类别的字节码操作的列表。对于每个类别,我添加了一个小描述和操作代码的十六进制范围:

  • 常量:用于将值从常量池(我们稍后会看到它)或从已知值推送到操作数堆栈中。从价值0x00到0x14
  • 加载:用于将值从局部变量加载到操作数堆栈中。从价值0x15到0x35
  • 存储:用于将操作数堆栈存储到局部变量中。从价值0x36到0x56
  • 堆栈:用于处理操作数堆栈。从价值0x57到0x5f
  • Math:用于对操作数堆栈中的值进行基本数学运算。从价值0x60到0x84
  • 转换:用于从一种类型转换为另一种类型。从价值0x85到0x93
  • 比较:用于两个值之间的基本比较。从价值0x94到0xa6
  • 控制:基本操作,如转到,返回,...允许更高级的操作,如返回值的循环或函数。从价值0xa7到0xb1
  • 引用:用于分配对象或数组,获取或检查对对象,方法或静态方法的引用。还用于调用(静态)方法。从价值0xb2到0xc3
  • 扩展:之后添加的其他类别中的操作。从价值0xc4到0xc9
  • 保留:供每个 Java 虚拟机实现内部使用。3 个值:0xca、0xfe和0xff。

这 204 个操作非常简单,例如:

  • 操作数 ifeq (0x99 ) 检查 2 个值是否相等
  • 操作数 iadd (0x60) 添加 2 个值
  • 操作数 i2l (0x85) 将整数转换为长整型
  • 操作数数组长度 (0xbe) 给出数组的大小
  • 操作数 pop (0x57) 从操作数堆栈中弹出第一个值

要创建字节码,需要一个编译器,JDK中包含的标准Java编译器是javac

让我们看一个简单的添加:

publicclassTest {

 publicstaticvoidmain(String[] args) {

   inta=1;

   intb=15;

   intresult=add(a,b);

 }

 

 publicstaticintadd(inta, intb){

   intresult=a+b;

   returnresult;

 }

}

“javac Test.java”命令在 Test.class 中生成一个字节码。由于java字节码是二进制代码,因此人类无法读取它。Oracle在其JDK中提供了一个工具javap,该工具将二进制字节码转换为JVM规范中人类可读的标记操作代码集。

命令 “javap -verbose Test.class” 给出以下结果:

Classfile/C:/TMP/Test.class

 Lastmodified1avr. 2015; size367bytes

 MD5checksumadb9ff75f12fc6ce1cdde22a9c4c7426

 Compiledfrom"Test.java"

publicclasscom.codinggeek.jvm.Test

 SourceFile: "Test.java"

 minorversion: 0

 majorversion: 51

 flags: ACC_PUBLIC, ACC_SUPER

Constantpool:

  #1=Methodref          #4.#15         //  java/lang/Object."<init>":()V

  #2=Methodref          #3.#16         //  com/codinggeek/jvm/Test.add:(II)I

  #3=Class              #17            //  com/codinggeek/jvm/Test

  #4=Class              #18            //  java/lang/Object

  #5=Utf8               <init>

  #6=Utf8               ()V

  #7=Utf8               Code

  #8=Utf8               LineNumberTable

  #9=Utf8               main

 #10=Utf8               ([Ljava/lang/String;)V

 #11=Utf8               add

 #12=Utf8               (II)I

 #13=Utf8               SourceFile

 #14=Utf8               Test.java

 #15=NameAndType        #5:#6          //  "<init>":()V

 #16=NameAndType        #11:#12        //  add:(II)I

 #17=Utf8               com/codinggeek/jvm/Test

 #18=Utf8               java/lang/Object

{

 publiccom.codinggeek.jvm.Test();

   flags: ACC_PUBLIC

   Code:

     stack=1, locals=1, args_size=1

        0: aload_0

        1: invokespecial#1                  // Method java/lang/Object."<init>":()V

        4: return

     LineNumberTable:

       line3: 0

 

 publicstaticvoidmain(java.lang.String[]);

   flags: ACC_PUBLIC, ACC_STATIC

   Code:

     stack=2, locals=4, args_size=1

        0: iconst_1

        1: istore_1

        2: bipush        15

        4: istore_2

        5: iload_1

        6: iload_2

        7: invokestatic  #2                  // Method add:(II)I

       10: istore_3

       11: return

     LineNumberTable:

       line6: 0

       line7: 2

       line8: 5

       line9: 11

 

 publicstaticintadd(int, int);

   flags: ACC_PUBLIC, ACC_STATIC

   Code:

     stack=2, locals=3, args_size=2

        0: iload_0

        1: iload_1

        2: iadd

        3: istore_2

        4: iload_2

        5: ireturn

     LineNumberTable:

       line12: 0

       line13: 4

}

可读.class表明字节码包含的不仅仅是java源代码的简单转录。它包含:

  • 类的常量池的描述。常量池是JVM的数据区域之一,它存储有关类的元数据,例如方法的名称,参数......当一个类在JVM中加载时,这部分进入常量池。
  • 像 LineNumberTable 或 LocalVariableTable 这样的信息,用于指定函数的位置(以字节为单位)及其变量在字节码中的位置。
  • 开发人员的 java 代码(加上隐藏构造函数)的字节码中的转录。
  • 处理操作数堆栈的特定操作,更广泛地说是处理传递和获取参数的方式。

仅供参考,以下是存储在.class文件中的信息的简要说明:

ClassFile {

 u4 magic;

 u2 minor_version;

 u2 major_version;

 u2 constant_pool_count;

 cp_info constant_pool[constant_pool_count-1];

 u2 access_flags;

 u2 this_class;

 u2 super_class;

 u2 interfaces_count;

 u2 interfaces[interfaces_count];

 u2 fields_count;

 field_info fields[fields_count];

 u2 methods_count;

 method_info methods[methods_count];

 u2 attributes_count;

 attribute_info attributes[attributes_count];

}


运行时数据区域

运行时数据区域是用于存储数据的内存中区域。这些数据由开发人员的程序或JVM用于其内部工作。

此图显示了 JVM 中不同运行时数据区域的概述。某些区域是每个线程的其他区域所独有的。


堆是所有 Java 虚拟机线程之间共享的内存区域。它是在虚拟机启动时创建的。所有类实例数组都在堆中分配(使用 new 运算符)。

MyClassmyVariable=newMyClass();

MyClass[] myArrayClass=newMyClass[1024];

此区域必须由垃圾回收器管理,以便在不再使用开发人员分配的实例时将其删除。清理内存的策略取决于 JVM 实现(例如,Oracle Hotspot 提供了多种算法)。

堆可以动态扩展或收缩,并且可以具有固定的最小和最大大小。例如,在Oracle Hotspot中,用户可以通过以下方式使用Xms和Xmx参数指定堆的最小大小“java -Xms=512m -Xmx=1024m...”


注意:堆不能超过的最大大小。如果超过此限制,JVM 将抛出一个 OutOfMemoryError。

方法区域

方法区域是所有 Java 虚拟机线程之间共享的内存。它是在虚拟机启动时创建的,由类装入器从字节码装入。只要加载方法区域中的类装入器处于活动状态,它们就会保留在内存中。

方法区域存储:

  • 类信息(字段/方法数、超类名、接口名、版本等)
  • 方法和构造函数的字节码。
  • 每个装入的类的运行时常量池。

规范不会强制在堆中实现方法区域。例如,在JAVA7之前,Oracle HotSpot使用一个名为PermGen的区域来存储方法区域。这个PermGen与Java堆(以及像堆一样由JVM管理的内存)是连续的,并且被限制为默认空间64Mo(由参数-XX:MaxPermSize修改)。从Java 8开始,HotSpot现在将方法区域存储在称为Metaspace的单独本机内存空间中,最大可用空间是总可用系统内存。


注意:方法区域不能超过的最大大小。如果超过此限制,JVM 将抛出一个 OutOfMemoryError。

运行时常量池

此池是方法区域的子部分。由于它是元数据的重要组成部分,因此 Oracle 规范除了“方法区域”之外,还描述了运行时常量池。对于每个加载的类/接口,此常量池都会增加。这个池就像传统编程语言的符号表。换句话说,当引用类、方法或字段时,JVM 通过使用运行时常量池搜索内存中的实际地址。它还包含常量值,如字符串 litteral 或常量基元。

StringmyString1=“Thisisastringlitteral”;

staticfinalintMY_CONSTANT=2;


PC 寄存器(每个线程)

每个线程都有自己的 pc(程序计数器)寄存器,与线程同时创建。在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法。pc 寄存器包含当前正在执行的 Java 虚拟机指令(在方法区域中)的地址。

注: 如果线程当前正在执行的方法是本机的,则 Java 虚拟机的 pc 寄存器的值是未定义的。Java 虚拟机的 pc 寄存器足够宽,可以在特定平台上保存 returnAddress 或本机指针。


Java 虚拟机堆栈(每个线程)

堆栈区域存储多个帧,因此在讨论堆栈之前,我将介绍这些帧。

框架

帧是一种数据结构,它包含多个数据,这些数据表示当前方法(被调用的方法)中线程的状态:

  • 操作数堆栈:我已经在关于基于堆栈的体系结构的章节中介绍了操作数堆栈。此堆栈由字节码指令用于处理参数。此堆栈还用于在 (java) 方法调用中传递参数,并在调用方法的堆栈顶部获取被调用方法的结果。
  • 局部变量数组:此数组包含当前方法范围内的所有局部变量。此数组可以保存基元类型、引用或返回地址的值。此数组的大小是在编译时计算的。Java虚拟机使用局部变量在方法调用时传递参数,被调用方法的数组是从调用方法的操作数堆栈创建的。
  • 运行时常量池引用:对正在执行的当前方法**的当前类**的常量池的引用。JVM 使用它来将符号方法/变量引用(例如:myInstance.method())转换为实际内存引用。

每个 Java 虚拟机线程都有一个私有 Java 虚拟机堆栈,与该线程同时创建。Java 虚拟机堆栈存储帧。每次调用方法时,都会创建一个新帧并将其放入堆栈中。当帧的方法调用完成时,无论该完成是正常还是突然(它会引发未捕获的异常),帧都会被销毁。

只有一个帧(执行方法的帧)在给定线程中的任何点处于活动状态。此帧称为当前帧*,其方法称为**当前方法*。在其中定义当前方法的类是*当前类***。对局部变量和操作数堆栈的操作通常参考当前帧。


让我们看看下面的例子,这是一个简单的加法

publicintadd(inta, intb){

 returna+b;

}

 

publicvoidfunctionA(){

// some code without function call

 intresult=add(2,3); //call to function B

// some code without function call

}

以下是当函数A()运行时它在JVM中的工作方式:


内部函数A() 帧 A 是堆栈帧的顶部,是当前帧。在内部调用添加 () 时,一个新帧(帧 B)被放置在堆栈中。帧 B 成为当前帧。帧 B 的局部变量数组是通过弹出帧 A 的操作数堆栈来填充的。当 add() 完成后,帧 B 将被销毁,帧 A 再次成为当前帧。add() 的结果放在 Frame A 的操作数堆栈上,以便 functionA() 可以通过弹出其操作数堆栈来使用它。


注意:这个堆栈的功能使它动态可扩展和收缩。存在堆栈不能超过的最大大小,这会限制递归调用的数量。如果超过此限制,JVM 将抛出一个 StackOverflowError

使用 Oracle HotSpot,您可以使用参数 -Xss 指定此限制。

本机方法堆栈(每线程)

这是一个用Java以外的语言编写的本机代码的堆栈,并通过JNI(Java本机接口)调用。由于它是一个“本机”堆栈,因此此堆栈的行为完全依赖于底层操作系统。


结论

我希望本文能帮助您更好地了解 JVM。在我看来,最棘手的部分是JVM堆栈,因为它与JVM的内部功能密切相关。


目录
相关文章
|
28天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
232 1
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
43 4
|
18天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
27天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
28天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
21 3
|
28天前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
45 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
20 1
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
80 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!