• 首页
  • 博客
  • 网站
  • 分享
  • 数码

当前位置:首页 » 个人博客 » 正文

JAVA的主旨是其著名的WOTA:“编写一次,在任何地方运行”。为了应用它,Sun Microsystems创建了java虚拟机,它是解释编译后的Java代码的底层操作系统的抽象。这虚拟机(Java Virtual Machine的缩写)是JRE (Java Runtime Environment)的核心组件,被创建来运行Java代码,但现在被其他语言(Scala、Groovy、JRuby、Closure……)使用。

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

 

全球概览

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

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

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

6666jvm_overview.jpg

此图给出了JVM的概述:

  • JVM

    解释

    字节码

    产生

    由汇编的一个类的源代码。尽管术语JVM代表“java虚拟机”,但它也运行其他语言,如scala或groovy,只要它们能被编译成Java字节码。
  • 为了避免磁盘I/O,字节码通过

    类别载入器

    在其中一个运行时数据区域中。这段代码会一直保存在内存中,直到JVM停止或者类加载器(加载它的那个)被销毁。
  • 然后加载的代码是

    解释

    并由

    执行引擎.

  • 执行引擎需要存储数据,比如指向正在执行的代码行的指针。它还需要存储开发人员代码中处理的数据。
  • 执行引擎还负责处理底层操作系统。

注意:许多JVM实现的执行引擎并不总是解释字节码,而是将字节码编译成本机代码(如果经常使用的话)。这就是所谓的及时(吉特舞乐)编译,并大大加快了JVM的速度。编译后的代码临时保存在一个通常称为代码缓存。因为该区域不在JVM规范中,所以在本文的剩余部分我不会讨论它。

 

基于堆栈的架构

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

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

举个例子,我们来看两个整数的基本加法。这个操作被称为iadd(对于i恩特格尔增加tion)。如果想在字节码中添加3和4:

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

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

 

字节码

因为JVM解释字节码,所以在深入之前理解它是什么是有用的。

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

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

  • 常数

    :用于将常量池中的值(我们稍后会看到)或已知值推入操作数堆栈。从值0x00到0x14
  • 负荷

    :用于将值从局部变量加载到操作数堆栈中。从值0x15到0x35
  • 商店

    :用于从操作数堆栈存储到局部变量。从值0x36到0x56
  • :用于处理操作数堆栈。从值0x57到0x5f
  • 数学

    :用于对操作数堆栈中的值进行基本数学运算。从值0x60到0x84
  • 转换策略

    :用于从一种类型转换到另一种类型。从值0x85到0x93
  • 比较

    :用于两个值之间的基本比较。从值0x94到0xa6
  • 控制

    :goto、return等基本操作,允许更高级的操作,如循环或返回值的函数。从值0xa7到0xb1
  • 参考

    :用于分配对象或数组,获取或检查对对象、方法或静态方法的引用。也用于调用(静态)方法。从值0xb2到0xc3
  • 延长

    :之后添加的其他类别的操作。从值0xc4到0xc9
  • 内向的; 寡言少语的; 矜持的

    :供每个Java虚拟机实现内部使用。3个值:0xca、0xfe和0xff。

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

  • 操作数

    ifeq

    (0x99)检查两个值是否相等
  • 操作数

    iadd

    (0x60)将2个值相加
  • 操作数

    i2l

    (0x85)将整数转换为长整型
  • 操作数

    数组长度

    (0xbe)给出数组的大小
  • 操作数

    流行音乐

    (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中生成一个字节码。Oracle在其JDK中提供了一个工具,javap,它将二进制字节码转换成JVM规范中的一组人类可读的带标签的操作码。

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

Classfile /C:/TMP/Test.class
  Last modified 1 avr. 2015; size 367 bytes
  MD5 checksum adb9ff75f12fc6ce1cdde22a9c4c7426
  Compiled from "Test.java"
public class com.codinggeek.jvm.Test
  SourceFile: "Test.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #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
{
  public com.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:
        line 3: 0

  public static void main(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:
        line 6: 0
        line 7: 2
        line 8: 5
        line 9: 11

  public static int add(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:
        line 12: 0
        line 13: 4
}

可读的。类表明字节码包含的不仅仅是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_memory_overview.jpg

该图显示了JVM中不同运行时数据区域的概况。有些区域是独一无二的,有些是每个线程独有的。

 

许多

堆是所有Java虚拟机线程共享的内存区域。它是在虚拟机启动时创建的。所有类别例子数组分配的在堆中(与新的操作员)。

 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热点使用一个名为PermGen的区域来存储方法区域。这佩尔姆根与Java堆(以及像堆一样由JVM管理的内存)相邻,并且被限制为64Mo的默认空间(由参数-XX:MaxPermSize修改)。从Java 8开始,HotSpot现在将方法区域存储在一个单独的本机内存空间中,称为元空间,最大可用空间是总的可用系统内存。

 

注意:有一个方法区域不能超过的最大大小。如果超过这个限制,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寄存器足够宽,可以容纳特定平台上的返回地址或本机指针。

 

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 call
  int result = add(2,3); //call to function B
// some code without function call
}

下面是当functionA()在JVM中运行时它是如何工作的: 

state_of_jvm_method_stack.jpg

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

 

注意:这个堆栈的功能使它可以动态扩展和收缩。有一个堆栈不能超过的最大大小,这限制了递归调用的数量。如果超过了这个限制,JVM就会抛出一个stack over flower error.

对于Oracle HotSpot,您可以使用参数-Xss来指定这个限制。

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

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

 

结论

我希望这篇文章能帮助你更好地理解JVM。在我看来,最棘手的部分是JVM栈,因为它与JVM的内部功能紧密相连。

投稿内容不要求原创性,所指观点属原作者所有!

本文链接:http://www.ziti66.com/net/html/165.html

本文标签:博客    

微信公众号:升级中

<< 上一篇下一篇 >>
为祖国加油
疫情防护,人人有责。祖国加油...
为祖国加油
疫情防护,人人有责。祖国加油...

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

搜索

网站分类

Tags列表

最新留言

++发现更多精彩++

    祖国加油!!!

Copyright ziti66.com on 2022. Some Rights Reserved.