欢迎访问 生活随笔!

凯发k8官方网

当前位置: 凯发k8官方网 > 编程资源 > 编程问答 >内容正文

编程问答

详细分析jvm内存模型 -凯发k8官方网

发布时间:2025/1/21 编程问答 20 豆豆
凯发k8官方网 收集整理的这篇文章主要介绍了 详细分析jvm内存模型 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

jvm内存模型

java的主要特点是其著名的wota(write once, run anywhere):“编写一次,随处运行”。为了应用它,sun microsystems创建了java虚拟机,这是对底层os的抽象,它解释了编译的java代码。该jvm是jre(java运行环境)的核心组件,创建运行java代码,但现在所使用的其他语言有(scala、groovy、jruby)。

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

jvm整体概况

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

  • 无论jvm是在16位/ 32位/ 64位os上运行的,原始类型int的大小始终是从-2 ^ 31到2 ^ 31-1的32位有符号整数。

  • 无论底层的os /硬件是big-endian还是little-endian,每个jvm都以大端顺序(高字节在前)存储和使用内存中的数据。
    注意:有时,一个jvm实现的行为与另一个不同,但通常是相同的。

    该图概述了jvm:

  • jvm 解释由编译类的源代码产生的字节码。尽管术语jvm代表“ java虚拟机”,但它可以运行其他语言,例如scala或groovy,只要它们可以编译成java字节码即可。

  • 为了避免磁盘i / o,字节码由运行时数据区之一中的类加载器加载到jvm 中。此代码将保留在内存中,直到jvm停止运行或销毁(加载了它的)类加载器为止。

  • 然后,加载的代码由执行引擎解释并执行。

  • 执行引擎需要存储数据,例如指向正在执行的代码的指针的指针。它还需要存储- 在开发人员代码中处理的数据。

  • 执行引擎还负责处理底层操作系统。

注意:许多jvm实现的执行引擎不是总是解释字节码,而是将字节码编译为本地代码(如果经常使用的话)。它称为即时(jit)编译,可大大加快jvm的速度。编译后的代码是临时保存在通常称为“代码缓存”的区域中的 。由于该区域不在jvm规范中,因此在本文的其余部分中将不再讨论。

基于堆栈的架构

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

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

例如,让我们对2个整数进行基本加法。该操作被称为add。如果要在字节码中添加3和4:

  • 他首先在操作数堆栈中压入3和4。
  • 然后调用add指令。
  • add将弹出操作数堆栈的最后2个值。
  • 将int结果(3 4)压入操作数堆栈,以供其他操作使用。

这种功能方式称为基于堆栈的体系结构。还有其他处理基本操作的方法,例如,基于寄存器的体系结构将操作数存储在较小的寄存器中,而不是堆栈中。台式机/服务器(x86)处理器和以前的android虚拟机dalvik使用基于寄存器的体系结构。

字节码

由于jvm会解释字节码,因此在深入了解字节码之前很有用。

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

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

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

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

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

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

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

public class test {public static void main(string[] args) {int a =1;int b = 15;int result = add(a,b);}public static int add(int a, int b){int result = a b;return result;} }

“ javac test.java”命令在test.class中生成一个字节码。由于java字节码是二进制代码,因此人类无法读取。oracle在其jdk javap中提供了一个工具,该工具可以将二进制字节码转换为jvm规范中易于阅读的带有 标签的操作码集。

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

classfile /c:/tmp/test.classlast modified 1 avr. 2015; size 367 bytesmd5 checksum adb9ff75f12fc6ce1cdde22a9c4c7426compiled from "test.java" public class com.codinggeek.jvm.testsourcefile: "test.java"minor version: 0major version: 51flags: acc_public, acc_super constant pool:#1 = methodref #4.#15 // java/lang/object."":()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 // "":()v#16 = nameandtype #11:#12 // add:(ii)i#17 = utf8 com/codinggeek/jvm/test#18 = utf8 java/lang/object {public com.codinggeek.jvm.test();flags: acc_publiccode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // method java/lang/object."":()v4: returnlinenumbertable:line 3: 0public static void main(java.lang.string[]);flags: acc_public, acc_staticcode:stack=2, locals=4, args_size=10: iconst_11: istore_12: bipush 154: istore_25: iload_16: iload_27: invokestatic #2 // method add:(ii)i10: istore_311: returnlinenumbertable:line 6: 0line 7: 2line 8: 5line 9: 11public static int add(int, int);flags: acc_public, acc_staticcode:stack=2, locals=3, args_size=20: iload_01: iload_12: iadd3: istore_24: iload_25: ireturnlinenumbertable:line 12: 0line 13: 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运算符)。

myclass myvariable = new myclass(); myclass[] myarrayclass = new myclass[1024];

当不再使用开发人员分配的实例时,必须由垃圾收集器 来管理该区域。清理内存的策略取决于jvm实现(例如,oracle hotspot提供了多种算法)。

堆可以动态扩展或收缩,并且可以具有固定的最小和最大大小。例如,在oracle hotspot中,用户可以通过以下方式用xms和xmx参数指定堆的最小大小:“ java -xms = 512m -xmx = 1024m…”。

注意:堆不能超过最大限制。如果超出此限制,jvm将抛出outofmemoryerror。

方法范围

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

方法区域存储:

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

规范不强制在堆中实现方法区域。例如,在java7之前,oracle hotspot使用一个名为permgen的区域来存储“方法区域”。该permgen与java堆(以及由jvm像堆一样由jvm管理的内存)是连续的,并且被限制为默认空间64m(由参数-xx:maxpermsize修改)。从java 8开始,hotspot现在将“方法区域”存储在称为metaspace的分离的本机内存空间中,最大可用空间是总可用系统内存。

注意:方法区域不能超过最大限制。如果超出此限制,jvm将抛出outofmemoryerror。

运行时常量池

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

string mystring1 = “this is a string litteral”; static final int my_constant=2;

pc寄存器(程序计数器)

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

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

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

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

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

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

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

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

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

public int add(int a, int b){return a b; }public void functiona(){ // some code without function callint result = add(2,3); //call to function b // some code without function call }

当functiona()在其上运行时,这是它在jvm内部的工作方式:

在functiona()内部,框架a是堆栈框架的顶部,并且是当前框架。在内部调用add()的开始处,将新框架(框架b)放入堆栈中。帧b成为当前帧。通过弹出帧a的操作数堆栈来填充帧b的局部变量数组。add()完成后,帧b被销毁,帧a再次成为当前帧。add()的结果放在框架a的操作数堆栈上,以便functiona()可以通过弹出其操作数堆栈来使用它。

注意:此堆栈的功能使其可以动态扩展和收缩。有一个堆栈不能超过的最大大小,这限制了递归调用的数量。如果超出此限制,则jvm抛出 stackoverflowerror。

使用oracle hotspot,可以使用参数-xss指定此限制。

本机方法堆栈(每个线程)

这是用非java语言编写并通过jni(java本机接口)调用的本机代码的堆栈。由于它是一个“本机”堆栈,因此该堆栈的行为完全取决于基础操作系统。

结论

我希望本文能帮助您更好地了解jvm。我认为,最棘手的部分是jvm堆栈,因为它与jvm的内部功能紧密相关。
本文来源:jvm内存模型

总结

以上是凯发k8官方网为你收集整理的详细分析jvm内存模型的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得凯发k8官方网网站内容还不错,欢迎将凯发k8官方网推荐给好友。

  • 上一篇:
  • 下一篇:
网站地图