JAVA基于其著名的WOTA:“WriteOnce,RunAnywhere”。为了应用它,SunMicrosystems创建了Java虚拟机,这是一种解释已编译Java代码的底层操作系统的抽象。JVM是JRE(JavaRuntimeEnvironment)的核心组件,它是为运行Java代码而创建的,但现在被其他语言(Scala、Groovy、JRuby、Closure...)使用。在本文中,我将重点关注JVM规范中描述的运行时数据区域。这些区域旨在存储程序或JVM本身使用的数据。我将从JVM的概述开始,然后是字节码,最后是不同的数据区域。目录[显示]全局概述JVM是底层操作系统的抽象。它确保无论JVM在什么硬件或操作系统上运行,相同的代码都将以相同的行为运行。例如:无论JVM是否运行在16位/32位/64位操作系统上,原始类型int的大小始终是从-2^31到2^31-的32位有符号整数1.无论底层操作系统/硬件是大端还是小端,每个JVM都以大端顺序(高字节在前)在内存中存储和使用数据。注意:有时JVM实现的行为与另一个不同,但通常是相同的。此图给出了JVM的概述:JVM解释通过编译类的源代码生成的字节码。虽然JVM这个词代表的是“Java虚拟机”,但是它可以运行其他语言,比如scala或者groovy,只要能编译成java字节码。为了避免磁盘I/O,字节码由一个运行时数据区域中的类加载器加载到JVM中。此代码保留在内存中,直到JVM停止或类加载器(加载它的)被销毁。加载的代码然后由执行引擎解释和执行。执行引擎需要存储数据,例如指向正在执行的代码行的指针。它还需要存储在开发人员代码中处理的数据。执行引擎还负责处理底层操作系统。注意:许多JVM实现都有执行引擎,如果经常使用,这些引擎会将字节码编译为本机机器码,而不是总是解释字节码。它称为即时(JIT)编译,可大大加快JVM的速度。编译后的代码暂时保存在通常称为代码缓存的区域中。由于这个区域不在JVM规范中,我不会在本文的其余部分讨论它。基于堆栈的体系结构JVM使用基于堆栈的体系结构。虽然它对开发者来说是不可见的,但它对生成的字节码和JVM架构有着巨大的影响,这也是我将简要解释这个概念的原因。JVM通过执行Java字节码中描述的基本操作来执行开发人员的代码(我们将在下一章中看到)。操作数是指令操作的值。根据JVM规范,这些操作需要通过称为操作数栈的栈传递参数。例如,我们以2个整数的基本加法为例。此操作称为iadd(用于整数加法)。如果要在字节码中添加3和4:他先将3和4压入操作数栈。然后调用iadd指令。iadd会将最后两个值弹出操作数栈。int结果(3+4)被压入操作数栈以供其他操作使用。这种操作方式称为基于堆栈的架构。还有其他处理基本操作的方法,例如将操作数存储在小寄存器而不是堆栈中的基于寄存器的体系结构。桌面/服务器(x86)处理器和旧的android虚拟机Dalvik使用这种基于寄存器的架构。字节码由于JVM解释字节码,因此在深入研究之前了解它很有用。Java字节码是将java源代码转换成一组基本操作。每个操作都包含一个表示要执行的指令的字节(称为操作码或操作码),以及零个或多个用于传递参数的字节(但大多数操作使用操作数堆栈来传递参数)。在256个可能的1字节长操作码中,204个当前用于java8规范。这是不同类别的字节码操作的列表。对于每个类别,我添加了一个小描述和一个十六进制范围的操作码:常量:用于将值从常量池(我们稍后会看到)或从已知值压入操作数堆栈。Fromvalues0x00to0x14Loads:用于将值从局部变量加载到操作数栈中。从值0x15到0x35Stores:用于从操作数栈存储到局部变量中。从值0x36到0x56Stack:用于处理操作数堆栈。Valuesfrom0x57to0x5fMath:用于对操作数栈中的值进行基本的数学运算。Conversionfromvalue0x60to0x84:用于从一种类型转换为另一种类型。从值0x85到0x93进行比较:用于两个值之间的基本比较。从值0x94到0xa6的控制:基本操作,如goto、return...允许更高级的操作,如返回值的循环或函数。从值0xa7到0xb1的引用:用于分配对象或数组,获取或检查对对象、方法或静态方法的引用。也用于调用(静态)方法。从值0xb2到0xc3Extended:以后添加的其他动作类。值0xc4到0xc9是保留值:供每个Java虚拟机实现内部使用。3个值:0xca、0xfe和0xff。这204个操作很简单,例如:操作数ifeq(0x99)检查2个值是否相等操作数iadd(0x60)将2个值相加操作数i2l(0x85)将整数转换为长操作数arraylength(0xbe)给出了sizeofthearrayOperandpop(0x57)从操作数栈弹出第一个值创建字节码需要编译器,JDK中包含的标准java编译器是javac。让我们看一个简单的加法:publicclassTest{publicstaticvoidmain(String[]args){inta=1;整数b=15;int结果=添加(a,b);}publicstaticintadd(inta,intb){intresult=a+b;返回结果;}}“javacTest.java”命令在Test.class中生成一个字节码。由于java字节码是二进制代码,它不是人类可读的。Oracle在其JDK中提供了一个工具javap,可将二进制字节码转换为一组人类可读的JVM规范标记操作码。命令“javap-verboseTest.class”给出以下结果:Classfile/C:/TMP/Test.classLastmodified1avr.2015;大小367字节MD5校验和adb9ff75f12fc6ce1cdde22a9c4c7426从“Test.java”公共类com.codinggeek.jvm.Test源文件编译:“Test.java”次要版本:0主要版本:51标志:ACC_PUBLIC,ACC_SUPER常量池:#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#6=Utf8()V#7=Utf8Code#8=Utf8LineNumberTable#9=Utf8main#10=Utf8([Ljava/lang/String;)V#11=Utf8add#12=Utf8(II)I#13=Utf8SourceFile#14=Utf8Test.java#15=NameAndType#5:#6//"":()V#16=NameAndType#11:#12//add:(II)I#17=Utf8com/codinggeek/jvm/Test#18=Utf8java/lang/Object{publiccom.codinggeek.jvm.Test();flags:ACC_PUBLICCode:stack=1,locals=1,args_size=10:aload_01:invokespecial#1//Methodjava/lang/Object."":()V4:returnLineNumberTable:第3行:0publicstaticvoidmain(java.lang.String[]);标志:ACC_PUBLIC,ACC_STATIC代码:stack=2,locals=4,args_size=10:iconst_11:istore_12:bipush154:istore_25:iload_16:iload_27:invokestatic#2//方法添加:(II)I10:istore_311:returnLineNumberTable:line6:0line7:2line8:5line9:11publicstaticintadd(int,int);标志:ACC_PUBLIC,ACC_STATIC代码:stack=2,locals=3,args_size=20:iload_01:iload_12:iadd3:istore_24:iload_25:ireturnLineNumberTable:第12行:0第13行:4}可读.class表示字节码包含的不仅仅是它包含的java源代码的简单转录:类的常量池的描述。常量池是JVM的数据区之一,里面存放着类的元数据,比如方法的名字,方法的参数……当类在JVM中加载的时候,这部分就进入到常量池中。诸如LineNumberTable或LocalVariableTable之类的信息,它们指定函数及其变量在字节码中的位置(以字节为单位)。开发人员的java代码(加上隐藏的构造函数)的字节码转录。处理操作数堆栈上的特定操作,更一般地处理参数传递和检索的方式。作为参考,下面是对.class文件中存储的信息的简要说明:ClassFile{u4magic;u2次要版本;u2主要版本;u2constant_pool_count;cp_infoconstant_pool[constant_pool_count-1];u2访问标志;u2这个类;u2超类;u2接口数;u2接口[interfaces_count];u2字段数;field_info字段[fields_count];u2方法计数;method_info方法[methods_count];u2属性计数;数据的内存区域。此数据由开发人员的程序或JVM用于其内部工作。此图显示了JVM中不同运行时数据区域的概览。有些区域是唯一的,其他区域是每个线程。堆堆是所有Java虚拟机线程之间共享的一块内存区域。它是在虚拟机启动时创建的。所有类实例和数组都分配在堆上(使用new运算符)。MyClassmyVariable=newMyClass();MyClass[]myArrayClass=newMyClass[1024];该区域必须由垃圾收集器管理,以便在不再使用时删除开发人员分配的实例。清理内存的策略取决于JVM实现(例如,OracleHotspot提供了几种算法)。堆可以动态扩展或收缩,并且可以具有固定的最小和最大大小。例如,在OracleHotspot中,用户可以使用Xms和Xmx参数指定堆的最小大小,方法如下“java-Xms=512m-Xmx=1024m...”注意:堆不能超过最大大小.如果超过此限制,JVM将抛出OutOfMemoryError。方法区方法区是所有Java虚拟机线程之间共享的内存。它在虚拟机启动时创建,并由类加载器从字节码加载。只要加载它们的类加载器还活着,方法区中的数据就会保留在内存中。方法区存储:类信息(字段/方法的数量、超类名称、接口名称、版本...)方法和构造函数的字节码。每个加载的类都有一个运行时常量池。规范没有强制要求方法区在堆上实现。比如在JAVA7之前,OracleHotSpot使用一个叫做PermGen的区域来存放方法区。这个PermGen与Java堆(以及像堆一样由JVM管理的内存)连续,并且被限制为默认空间64Mo(由参数-XX:MaxPermSize修改)。从Java8开始,HotSpot现在将方法区存储在一个单独的本机内存空间中,称为Metaspace,最大可用系统内存总量。注意:方法区不能超过最大大小。如果超过此限制,JVM将抛出OutOfMemoryError。运行时常量池这个池是方法区的一个子区。因为它是元数据的重要组成部分,所以Oracle规范将运行时常量池与方法分开描述。每个加载的类/接口都会增加这个常量池。这个池就像传统编程语言的符号表。换句话说,当一个类、方法或字段被引用时,JVM通过运行时常量池在内存中搜索实际地址。它还包含常量值,例如字符串文字或常量基元。StringmyString1="这是一个字符串乱码";静态最终intMY_CONSTANT=2;pc寄存器(每个线程)每个线程都有自己的pc(程序计数器)寄存器,与线程同时创建。在任何时候,每个Java虚拟机线程都在执行一个方法的代码,即线程的当前方法。pc寄存器包含当前正在执行的Java虚拟机指令的地址(在方法区)。注意:如果线程当前正在执行的方法是native,那么Java虚拟机的pc寄存器的值是undefined。Java虚拟机的pc寄存器足够宽,可以容纳一个returnAddress或特定平台上的本机指针。Java虚拟机堆栈(每个线程)堆栈区域存储多个帧,因此我将在讨论堆栈之前介绍这些帧。帧帧是一种数据结构,包含表示当前方法(被调用方法)中线程状态的几段数据:操作数栈:我已经在基于栈的体系结构一章中介绍了操作数栈。字节码指令使用这个堆栈来处理参数。该堆栈还用于在(java)方法调用中传递参数,并在调用方法的堆栈顶部获取被调用方法的结果。局部变量数组:该数组包含当前方法范围内的所有局部变量。数组可以保存基本类型、引用或returnAddress的值。这个数组的大小是在编译时计算的。Java虚拟机在调用方法时使用局部变量传递参数,被调用方法的数组是从调用方法的操作数栈中创建的。RuntimeConstantPoolReference:为当前正在执行的方法引用当前类的常量池。JVM使用它来将符号方法/变量引用(例如:myInstance.method())转换为实际的内存引用。堆每个Java虚拟机线程都有一个私有的Java虚拟机栈,与线程同时创建。Java虚拟机堆栈存储帧。每次调用方法时,都会创建一个新框架并将其放入堆栈。框架在其方法调用完成时被销毁,无论该完成是正常的还是突然的(它抛出未捕获的异常)。只有一个框架,即执行该方法的框架,在给定线程中的任何时候处于活动状态。这个框架称为当前框架,它的方法称为当前方法。定义当前方法的类是当前类。对局部变量和操作数栈的操作一般是指当前帧。让我们看看下面的例子,它是一个简单的加法publicintadd(inta,intb){returna+b;}publicvoidfunctionA(){//一些没有函数调用的代码intresult=add(2,3);//调用函数B//一些没有函数调用的代码}下面是运行functionA()时在JVM中的工作方式:在functionA()中,帧A是堆栈帧的顶部,也是当前帧。在对add()的内部调用开始时,一个新帧(帧B)被放入堆栈。帧B成为当前帧。框架B的局部变量数组由弹出框架A的操作数堆栈来填充。当add()完成时,框架B被销毁,框架A再次成为当前框架。add()的结果放在FrameA的操作数栈中,以便functionA()可以通过弹出其操作数栈来使用它。注意:此堆栈的功能允许它动态扩展和收缩。堆栈不能超过最大大小,这限制了递归调用的次数。如果超过此限制,JVM将抛出StackOverflowError。对于OracleHotSpot,您可以使用参数-Xss指定此限制。本机方法堆栈(每个线程)这是用Java以外的语言编写并通过JNI(Java本机接口)调用的本机代码堆栈。由于它是一个“本机”堆栈,因此该堆栈的行为完全取决于底层操作系统。填写。当add()完成时,框架B被销毁,框架A再次成为当前框架。add()的结果放在FrameA的操作数栈中,以便functionA()可以通过弹出其操作数栈来使用它。注意:此堆栈的功能允许它动态扩展和收缩。堆栈不能超过最大大小,这限制了递归调用的次数。如果超过此限制,JVM将抛出StackOverflowError。对于OracleHotSpot,您可以使用参数-Xss指定此限制。本机方法堆栈(每个线程)这是用Java以外的语言编写并通过JNI(Java本机接口)调用的本机代码堆栈。由于它是一个“本机”堆栈,因此该堆栈的行为完全取决于底层操作系统。你了解过JVM内存模型吗?