《深入理解JAVA虚拟机》(一) JVM 结构 + 栈帧 详解


image

1、程序计数器(Program Counter Register)
        线程独有,每个线程都有自己的计数器;由于CPU的任意时刻只能执行所有线程中的一条,所以需要使用程序计数器来支持JVM的并发;另外字节码解释器读取下一行指令、分支、循环、跳转、异常处理等等逻辑都依赖于程序计数器。程序计数器是JVM唯一不存在OutOfMemoryError的区域。

2、Java虚拟机栈(Java Virtual Machine Stacks)
        线程独有,用于保存线程相关的栈帧(@注释1);生命周期与线程相同,线程中,方法执行的过程等同该方法对应栈帧从入栈到出栈的过程,如果方法执行过程中调用了其它方法,那么同样的道理,方法内部调用的其它方法的栈帧也会入栈,以此类推,直到当前方法执行完,然后从虚拟机栈中出栈即代表一个方法完整的执行过程。

Java虚拟机栈内存大小即可固定,也支持动态拓展(在JVM运行内存的范围内):

(1)当固定大小情况下,线程请求分配的栈容量大于Java虚拟机栈最大容量时,抛出异常:StackOverFlowError。

(2)当可拓展时,如果在拓展过程中,无法申请到足够的内存时,抛出异常:OutOfMemoryError(比如:JVM运行内存被占满,此时已经无处可以申请内存了)。

3、本地方法栈(Native Method Stack)
        类似上述Java虚拟机栈,放入Java虚拟机栈中的是Java方法的栈帧;而本地方法栈中的内容是native方法(@注释2)的栈帧,他为native方法服务。

例如:java.lang.String.intern() 和  java.lang.Object.hashCode()方法的定义如下:

public native String intern();
public native int hashCode();

4、Java堆(Java Heap)
       Java堆是线程共享的,它是JVM中占用内存空间最大的部分,它的作用是存放Java对象实例,几乎所有的Java对象都从堆中分配内存;因为Java堆存放了大量对象实例,所以这里也是垃圾回收发生的主要场所;Java堆内存大小即可设为固定值,也可以动态拓展,Java堆可以处于非连续的物理内存上(大学的某门课讲过原理,逻辑上连续即可)。

5、方法区(Method Area)
       (1) 基本概念:同Java堆,方法区也是线程共享;方法区保存JVM加载的:类的信息、常量、静态变量、即时编译器编译后的代码,等数据;Java虚拟机规划中对方法区限制较少,是否受垃圾回收器管理是可选的,大小可固定或可拓展,可以存在于不联系的内存空间上。

(2) 关于HotSpot虚拟机:Java 7之前,方法区与Java堆共享内存,Java堆被划分为:青年代、老年代、永久代,其中永久代即指方法区,同样此时方法区被垃圾回收器管理;到了Java 8,HotSpot虚拟机改变了实现方式,方法区分配的内存被移至虚拟机外,此时称呼其为元空间,不再与Java堆共享内存,也不再被垃圾回收器管理。

(3) 运行时常量池

a 基本概念:位于方法区,它是每一个类或接口中的,常量池表,运行时的表示形式;每一个运行时常量池(对应 类/接口),在加载 类/接口时被创建并分配到方法区中。

b 常量池作用:存放编译器生成的字面量和符号引用,当虚拟机运行时,从常量池获取字面量或者符号引用,在类创建或者运行时,映射到具体的内存中。

c 字面量(常量):字符串   ||   final   变量

d 符号引用:类/接口的全限定名  ||    字段名称及其描述符   ||   方法名称及其描述符

本文仅限于JVM各个组成部分即基本介绍,不深入讨论原理,旨于深入探究JVM前建立相关概念模型。

* 拓展及注释
1、栈帧
        基本概念:在JAVA虚拟机栈中使用,用于支持JVM进行方法调用和方法执行的数据结构(栈帧就是虚拟栈中的一个元素),每个方法执行时都会创建一个栈帧,它包括:局部变量表、操作数栈、动态链接、方法返回地址等。对于JVM执行引擎来说,在活动的线程中,只有处于栈顶的栈帧是有效的,所有字节码指令都只对作用在当前栈帧关联的方法上。

(1)局部变量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,局部变量表的容量以  变量槽(Slot)  为单位;每个Slot的大小与JVM平台有关,一般可以存放32位以内的数据类型:8种基本数据类型,以及  对象应用(reference)和returnAddress(方法返回地址)。对于  对象应用(reference),JVM必须做到能够直接或间接查找到对象在JAVA堆中的数据存储地址起始索引,以及能查找到对象所属数据类型在方法区中存储的类型信息;

(2)操作数栈:它是一个LIFO栈,操作数栈中的每一个元素都可以是任意的Java数据类型,栈的最小容量单位为32bite,32位的数据类型栈一个容量单位,64位数据类型占2个容量单位。JVM引擎执行的实质可以理解成:从操作数栈栈顶提取元素,然后执行指令,并把执行结果压入栈中的过程。

(3)动态链接:

前提条件(背景):每一个栈帧都会持有一个指向运行时常量池中该栈帧所属的方法引用,持有该引用的目的是为了支持方法调用过程中的动态链接。字节码(Class文件中,可以借助工具查看编译后的Class文件中的字节码指令)  的方法调用指令就是以Class文件常量池中的符号引用作为参数的;

动态链接:上述符号引用中,有的符号引用会在每次运行期间转化为直接引用,该类引用称为动态链接;

静态链接:相较于动态链接,对于静态链接,符号引用会在类加载  或   第一次使用的时候就转化为直接引用,而不是每次执行时去转化为直接引用。

(4)方法返回地址:方法执行完有两种方式退出方法,其一为方法执行过程中遇到任意一个方法返回的字节码指令,然后将相关结果返回给方法的调用者。其二,方法执行碰到异常,并且异常没有被捕获处理时推出方法。方法退出,即代表当前     (栈)帧   出栈,后续将会执行当前方法的调用者(上层方法)所对应的   (栈)帧   后续指令,那么对应的操作即为:恢复调用者对应  栈帧  的局部变量表和操作数栈,把当前方法的返回值压入调用者方法对应栈帧的操作数栈中,调用PC计数器的值并使之执行当前方法调用指令的下一条指令(当前方法执行完,挫骨扬灰,只留下了舍利供调用者使用,当然也可能直接烧成灰了啥也没有(无返回值))。

2、native方法(本地方法):
        可以理解为java调用非Java代码实现的方法,的接口。众所周知,Java中很多东西都是其它语言实现的(例如C语言),使用native方法(本地方法)的原因如下:

(1)与java环境外交互:
                有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
        (2)与操作系统交互:
                JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法。

posted @ 2021-06-19 15:13  bokerr  阅读(320)  评论(0编辑  收藏  举报