资讯详情

Java虚拟机详解

1、什么是JDK

jdk全称“Java Development Kit”,指的是Java语言软件开发工具包主要用于移动设备和嵌入式设备java开发应用程序。jdk是java发展的核心, 包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基本类库(即Java API 包括rt.jar)。是SunMicrosystems针对Java是开发商的产品 Java 语言软件开发工具包主要用于移动设备和嵌入式设备java应用程序。

最主流的JDK是Sun公司发布的JDK,除了Sun此外,许多公司和组织都开发了自己的产品JDK。IBM公司开发了自己的公司JDK,国内淘宝也开发了自己的淘宝JDK,各组织开发自己的JDK都是为了在某些方面得到一些改进,以适应自己的需要。

JDK用于构建 Java 平台上发布的应用程序、applet 以及组件的开发环境。它不提供具体的开发软件,无论你使用什么开发软件Java必须使用的类库和程序Java语言规范。

JDK基本组件包括:

javac – 编译器将源程序转换为字节码 jar – 包装工具将相关文件包装成文件 javadoc – 文档生成器,从源码注释中提取文档 jdb – debugger,查错工具 java – 编译后的操作java程序(.class后缀的) appletviewer:小程序浏览器,执行HTML文件上的Java小程序的Java浏览器。 Javah:可调用生产JavaC过程的过程,或可以建立的过程JavaC过程调用的头文件。 Javap:Java编译文件中的可访问功能和数据,以及字节代码的含义。 Jconsole: Java系统调试和监控的工具

JDK:把Java编程语言,Java虚拟机、Java这三部分统称为类库JDK(JavaDevelopment Kit),JDK是用于支持Java最小的程序开发环境。

JRE:把Java类库API中的Java SE API子集和Java这两部分统称为虚拟机JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境。 在这里插入图片描述 Java虚拟机家族 HotSpot: 是Sun/OracleJDK和OpenJDK中默认的java虚拟机,也是目前使用范围最广的虚拟机。

2、内存管理

2.1.运行时数据区内容

Java虚拟机正在执行Java将其管理的内存划分为几个不同的数据区域。《Java《虚拟机规范》规定,Java虚拟机管理的内存将包括以下操作数据区域。

2.2.程序计数器

2.2.1、主要内容

程序计数器(Program Counter Register)它是一个较小的内存空间,可以看作是当前线程执行的字节码的行号指示器。 在Java在虚拟机的概念模型中,字节码解释器通过改变计数器的值来选择下一个需要执行的字节码指令。它是程序控制流的指示器,需要依靠计数器完成分支、循环、跳转、异常处理、线程恢复等基本功能。 由于Java虚拟机的多线程是通过轮流切换和分配处理器执行时间来实现的。在任何确定的时刻,处理器(多核处理器的内核)只执行一个线程中的指令。因此,为了在线程切换后恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,不影响每个线程之间的计数器,并独立存储。我们称这种内存区域为线程私有内存。 如果线程执行是一个Java该计数器记录虚拟机字节码指令正在执行的地址;如果是本地的(Native)该计数器值为空(Undefined)。这里唯一的内存区域《Java虚拟机规范中没有规定OutOfMemoryError情况区。

2.2.2.程序计数器详解

简介 (Program Counter Register)它是一个小的内存空间,可以看作是当前线程执行的字节码的行号指示器。(在概念模型中:字节码解释器通过改变计数器的值来选择下一个需要执行的字节码指令) 线程私有:多线程是通过轮流切换和分配处理器执行时间来实现的。为了在线程切换后恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,它不会相互影响,并独立存储。 状态:

①执行java计数器记录虚拟机字节码指令的地址; ②执行native方法,计数器为空(undefined) 唯一一个再java虚拟机规范中没有规定OutOfMemoryError的区域。

特点及分析 ①占用内存小,唯一不会OutOfMemoryError内存空间可根据编译时确定线程代码的最大偏移值:A字节;或者JVM已确定程序计数器的大小:X字节。A字节和X字节都保证了程序计数器的最大偏移≤最大空间值。另外,在线程运行时,只改变程序计数器的值,不涉及空间的扩展,因此不会有空间不足) ②私有线程(因为JVM里多线程运行时(至少会有main, gc),它通过轮流切换和分配处理器执行时间来运行,在确定的某个时刻,一个CPU(或内核)只有一个线程运行(一个线程中的程序计数器更改),需要保证线程切换后能够恢复到正确的执行位置——每个线程都有一个程序计数器,相互独立,不相互干扰) ③对java该方法是字节码偏移量native方法是undefined(native是非java比如编写代码C,C , 它们无法在java编译时生成字节码,即JVM获取不到native只能通过系统指令调用native方法) Java线程总是需要以某种形式映射OS线程上。映射模型可为1:1(原线程模型)n:1(绿色线程 / 用户态线程模型),m:n(混合模型)。以HotSpot VM例如,它目前在大多数平台上使用1:1模型,即每个平台Java线程直接映射到一个OS线程执行。此时,native该方法由原始平台直接执行,不需要注意抽象JVM层面上的“pc寄存器概念-原生CPU上真正的PC寄存器是什么样。就像使用C或C 写多线程序,它在线程切换时是什么,Java的native方法是什么。

执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C 库,可以近似地认为native方法相当于C/C 暴露给java接口,java通过调用这个接口来调用C/C 方法。因为这种方法是通过的C/C 而不是java进行实现。自然不能产生相应的字节码,C/C 内存分配是由自己的语言决定的,而不是由自己的语言决定的JVM决定的。

2.3、Java虚拟机栈

2.3.2.堆栈的储存单位

每个线程都有自己的栈,栈中的数据都是栈帧(Stack Frame)格式存在。在这个线程中执行的每一种方法都对应于栈帧(Stack Frame)。?栈帧是一个内存块,过程中各种数据信息的内存块和数据集。 

2.3.三、栈运行原理

JVM直接对栈的操作只有两种,即栈帧进出栈,遵循先进后出/后进先出的原则。在活动线程中,在一个时间点上,只会有一个活动栈帧,即只有目前正在实施的栈帧(栈顶栈帧)有效,被称为当前栈帧(Current Frame),对应当前栈帧的方法是当前方法(Current Method),定义这种方法的类别是当前类别(Current Class)。所有执行引擎操作的字节码指令只针对当前栈帧。如果在这种方法中调用其他方法,将创建相应的新栈帧,放在栈的顶部,成为新的当前帧。 

注:不同线程中包含的栈帧不允许相互引用,即不可能在一个栈帧中引用另一个栈帧。如果当前方法调用其他方法并返回方法,当前栈帧将将该方法的执行结果传回到前一个栈帧。然后,虚拟机会丢弃当前栈帧,使前一个栈帧再次成为当前栈帧。java返回函数的方法有两种,一种是正常的函数返回,使用return指令,另一种是抛出异常。无论使用哪种方式,栈帧都会弹出。 

2.3.4.栈帧的内部结构

每个栈中存储着: 局部变量表(Local Variables) 操作数栈(Oprand Stack)(或表达式栈) 动态链接(Dynamic Linking)(或引用指向运行时常量池的方法) 方法返回地址(Return Address)(或定义正常退出或异常退出的方法) 附加信息 

2.3.5.局部变量表

介绍  局部变量表也称为局部变量数组或本地变量表 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类本数据类型、对象引用(reference),以及ReturnAddress类型 由于局部变量表是建立在线程栈上的,是线程的私有数据,因此,没有数据安全问题 编译期确定了局部变量表所需的容量大小,并保存在方法中Code属性的maxmum variables数据项中。在方法运行期间是不会改变局部变量表大小的。

2.3.6、关于Slot的理解

局部变量表,最基本的存储单元是Slot(变量槽)
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量
在局部变量表里,32位以内的类型只占用一个Slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
byte、short、char在存储前被转换为int、boolean也被转换为int、0表示false,非0表示true
long和double则占据两个slot
jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每个slot上
如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可。(比如访问long或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象应用this将会存放在index为0的slot处,其余的参数按照参数顺序继续排列。

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

2.3.7、操作数栈

介绍
每一个独立的栈帧中除了包含局部变量表之外,还包含一个后进先出(Last-in-first-out)的操作数栈,也可以称之为表达式栈(Expression Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。
比如:执行复制、交换、求和等操作

图解:将8和15出栈,执行求和操作后再将结果进栈操作。

概念
操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
栈中的任何一个元素都是可以任意的java数据类型
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问
代码追踪

过程解析

2.3.8、栈顶缓存技术

背景
由于操作数是存储在内存中的,因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题,HopSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

2.3.9、 动态链接

如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接

2.3.10、符号引用

我们用命令javap -v Math.class 获取可读的字节码信息,如下,我们类信息中的所有符号都放在了常量池中,如下:
...
public class com.suibibk.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // com/suibibk/jvm/Math
   #2 = Utf8               com/suibibk/jvm/Math
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/suibibk/jvm/Math;
  #14 = Utf8               compute
  #15 = Utf8               ()I
  #16 = Utf8               a
  #17 = Utf8               I
  #18 = Utf8               b
  #19 = Utf8               c
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Methodref          #1.#9          // com/suibibk/jvm/Math."<init>":()V
  #23 = Methodref          #1.#24         // com/suibibk/jvm/Math.compute:()I
  #24 = NameAndType        #14:#15        // compute:()I
  #25 = Utf8               args
  #26 = Utf8               [Ljava/lang/String;
  #27 = Utf8               math
  #28 = Utf8               SourceFile
  #29 = Utf8               Math.java
{ 
        
  public com.suibibk.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/suibibk/jvm/Math;
  public int compute();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/suibibk/jvm/Math;
            2      11     1     a   I
            4       9     2     b   I
           11       2     3     c   I
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #1                  // class com/suibibk/jvm/Math
         3: dup
         4: invokespecial #22                 // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #23                 // Method compute:()I
        12: pop
        13: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/suibibk/jvm/Math;
}
常量池中的就都是符号引用,那方法怎么找到。符号引用转直接引用,我们再看一下我们的代码
public class Math { 
        
    private static Math math = new Math();
    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();
    }
}
在JVM执行到map.compute()时,也就是对于上面的指令
 9: invokevirtual #23                 // Method compute:()I
时,怎么根据符号引用找到compute()的执行指令?
我们通过#23去常量池中找到对应的符号
#23 = Methodref          #1.#24         // com/suibibk/jvm/Math.compute:()I
可以知道时方法引用,其实#23对应也有两个符号#1和#24,这两个符号分别如下
#1 = Class              #2             // com/suibibk/jvm/Math
#24 = NameAndType        #14:#15        // compute:()I
然后#24对应的时#14和#15
#14 = Utf8               compute
#15 = Utf8               ()I
所以#23对应的符号引用就是com/suibibk/jvm/Math.compute:()I
然后JVM会找到该符号引用对应的直接引用,放入栈帧的动态链接中。

补充二:

虚拟机栈概述 虚拟机栈出现的背景

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

初步印象 有不少Java开发人员一提到Java内存结构,就会非常粗粒度地将JVM中的内存区理解为仅有Java堆(heap)和Java栈(stack)?为什么?

内存中的栈与堆 栈是运行时的单位,而堆是存储的单位

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。 堆解决的是数据存储的问题,即数据怎么放,放哪里

虚拟机栈基本内容 Java虚拟机栈是什么? Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。 生命周期: 生命周期和线程一致 作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。 栈的特点:栈是一种快速有效的分配存储方式,访问速度仅次于罹序计数器。 JVM直接对Java栈的操作只有两个: 每个方法执行,伴随着进栈(入栈、压栈) 执行结束后的出栈工作 对于栈来说不存在垃圾回收问题(栈存在溢出的情况)

问题:开发中遇到哪些异常?

栈中可能出现的异常 Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常。

public static void main(String[] args) { 
        
    test();
}
public static void test() { 
        
    test();
}
//抛出异常:Exception in thread"main"java.lang.StackoverflowError
//程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。

设置栈内存大小 我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

public class StackDeepTest{ 
         
    private static int count=0; 
    public static void recursion(){ 
        
        count++; 
        recursion(); 
    }
    public static void main(String args[]){ 
        
        try{ 
        
            recursion();
        } catch (Throwable e){ 
        
            System.out.println("deep of calling="+count); 
            e.printstackTrace();
        }
    }
}

栈的存储单位

栈中存储什么? 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈运行原理

JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。 即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。(JVMStack是线程私有的) 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java方法有两种返回函数的方式:

一种是正常的函数返回,使用return指令; 一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

public class CurrentFrameTest{ 
        
    public void methodA(){ 
        
        system.out.println("当前栈帧对应的方法->methodA");
        methodB();
        system.out.println("当前栈帧对应的方法->methodA");
    }
    public void methodB(){ 
        
        System.out.println("当前栈帧对应的方法->methodB");
    }

栈帧的内部结构

每个栈帧中存储着:

局部变量表(Local Variables) 操作数栈(operand Stack)(或表达式栈) 动态链接(DynamicLinking)(或指向运行时常量池的方法引用) 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义) 一些附加信息

并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的。

局部变量表(Local Variables) 局部变量表也被称之为局部变量数组或本地变量表

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

关于Slot的理解

局部变量表,最基本的存储单元是Slot(变量槽) 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。 byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。 JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或doub1e类型变量) 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

Slot的重复利用

局部变量表用于存放方法参数和方法内部所定义的局部变量。 它的容量是以Slot为最小单位,一个slot可以存放32位以内的数据类型。 虚拟机通过索引定位的方式使用局部变量表,范围为[0,局部变量表的slot的数量]。方法中的参数就会按一定顺序排列在这个局部变量表中,至于怎么排的我们可以先不关心。而为了节省栈帧空间,这些slot是可以复用的,当方法执行位置超过了某个变量,那么这个变量的slot可以被其它变量复用。当然如果需要复用,那我们的垃圾回收自然就不会去动这些内存。

public class SlotTest { 
        
    public void localVarl() { 
        
        int a = 0;
        System.out.println(a);
        int b = 0;
    }
    public void localVar2() { 
        
        { 
        
            int a = 0;
            System.out.println(a);
        }
        //此时的就会复用a的槽位
        int b = 0;
    }
}

如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。 字节码PC计数器就是程序计数器,程序计数器记录当前线程所执行的字节码的偏移地址。如果这个值超出了某个变量的作用域,那么接下来这个变量就不会再被访问到。 slot的复用会影响到系统的垃圾收集行为:

没有回收p所占的内存,因为在执行System.gc()时,变量p还处于作用域内,虚拟机自然不敢回收p的内存。

p的所用域被限制在了块中,执行System.gc()的时候。p已经不能被访问了。可还是没有被回收。

继续修改

这次垃圾被回收了。

静态变量与局部变量的对比

参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。

我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。

和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

public void test(){ 
        
    int i;
    System. out. println(i);
}

这样的代码是错误的,没有赋值不能够使用。

补充说明

在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。 在方法执行时,虚拟机使用局部变量表完成方法的传递。 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈(Operand Stack)

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack) 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop) 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈 比如:执行复制、交换、求和等操作 ————————————————

举个栗子:

public void testAddOperation(){ 
        
    byte i = 15; 
    int j = 8; 
    int k = i + j;
}

字节码指令信息

public void testAddOperation(); 
    Code:
    0: bipush 15
    2: istore_1 
    3: bipush 8
    5: istore_2 
    6:iload_1 
    7:iload_2 
    8:iadd
    9:istore_3 
    10:return

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。

栈中的任何一个元素都是可以任意的Java数据类型

32bit的类型占用一个栈单位深度 64bit的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

代码追踪 举个栗子:

public void testAddOperation() { 
        
    byte i = 15;
    int j = 8;
    int k = i + j;
}

使用javap 命令反编译class文件:javap -v 类名.class

public void testAddoperation(); 
Code:
	0: bipush 15 
	2: istore_1 
	3: bipush 8
	5: istore_2
	6: iload_1
	7: iload_2
	8: iadd
	9: istore_3
    10: return

栈顶缓存技术(Top Of Stack Cashing)技术 前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。

由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接(Dynamic Linking)

动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

为什么需要运行时常量池呢? 常量池的作用:就是为了提供一些符号和常量,便于指令的识别 方法的调用:解析与分配 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

静态链接

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接

动态链接

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。

静态链接和动态链接不是名词,而是动词,这是理解的关键。

对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

早期绑定

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

晚期绑定

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

随着高级语言的横空出世,类似于Java一样的基于面

标签: aas压力变送器压力变送器上mb压力变送器std920s2压力变送器器by934g压力变送器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台