[看书笔记]《深入java虚拟机》——java体系结构(二)


java虚拟机的三种含义:

  • 抽象的规范
  • 一个具体的实现
  • 一个运行中的虚拟机实例

———————java虚拟机的生命周期:
java虚拟机实例的天职就是负责运行一个java程序。
启动一个java程序,一个虚拟机实例诞生了;程序关闭退出,虚拟机消亡。
有几个java程序正在运行,就有几个java虚拟机实例。每个java程序都运行在自己的java虚拟机实例中。

java虚拟机中有两种线程:

  • 守护线程:由虚拟机自己使用的(执行垃圾收集线程),java程序也可以把任何线程标记为守护线程。
  • 非守护线程:main()这种。

非守护线程都终止时,虚拟机实例才自动退出。(也可以调用Runtime或System的exit()方法退出程序)。

———————java虚拟机的体系结构:
见同目录第五章.Java虚拟机-体系结构图!
方法区和堆是该虚拟机实例的所有线程共享的。
当虚拟机装载一个class文件时,他会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息放到方法区中。
当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。

每个新线程被创建时,他都将得到它自己的PC寄存器以及一个java栈(不是所有线程共享)。
java虚拟机为每个线程创建内存区,这些内存区域是私有的,任何线程都不能访问另一个线程的PC寄存器或者java栈。
如果线程正在执行的是一个java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令。
java栈则总是存储该线程中java方法调用的状态(局部变量、被调用时传进来的参数、返回值、运算的中间结果)

本地方法调用的状态则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能在寄存器或者其他某些特定实现相关的内存区中。

———————栈帧(1):
java栈上由许多栈帧(帧)组成的,一个栈帧包含一个java方法调用的状态。
当线程调用一个java方法时,虚拟机压入一个新的栈帧到该线程的java栈中;当方法返回时,这个栈帧被从java栈中弹出并抛弃。

java虚拟机没有寄存器,其指令集使用java栈来存储中间数据(为了平台无关性)。

———————类型数据
类型数据分为来那个中:
基本类型:float、double、byte、short、int、long、char、boolean
引用类型:reference(类引用、接口引用、数组引用)
注意:编译成字节码时boolean用int表示,

java虚拟机中,最基本的数据单元就是字,至少选择32位作为字长。
运行时数据区的大部分内容,都是基于“字”这个抽象概念的。

———————类装载器子系统:
类装载器的装载、连接和初始化:

  • 装载:查找并装载类型的二进制数据
  • 连接:执行验证,准备,以及解析(可选)。
    验证:确保被导入类型的正确性。
    准备:为静态变量分配内存,并将其初始化为默认值。
    解析:把类型中的符号引用转换为直接引用。
  • 初始化:把静态变量初始化为正确初始值。

启动类装载器:
只要是符合java class文件格式的二进制文件,java虚拟机实现都必须能够从中辨别并装载器中的类和接口。每个java虚拟机实现必须有一个启动类加载器来加载受信任的类,比如java API。

用户自定义类装载器:
用户自定义的类装载器是普通的java对象,它的类必须派生自java.lang.ClassLoader类。ClassLoader类中定义的方法为程序提供了访问类装载器机制的接口。
用户自定义类装载器和Class类的实例都放在内存中的堆区,而装载的类型信息则都位于方法区。

ClassLoader类的四个通往java虚拟机的方法:
protected final Class defineClass(String name, byte data[], int offset, int length);
接收一个名为data[]的字节数组,并且在data[offset]到data[data+length]之间的二进制数据必须符合java class格式(表示一个新的可用类),name是该类型的全限定名,并将赋以默认的保护域。注意:该方法返回Class对象实例时表示指定的class文件已经被找到并装载到了方法区(不一定连接和初始化)

protected final Class defineClass(String name, byte data[], int offset, int length,
ProtectionDomain protectionDomain);
与前面一样,只是,该类型的保护域将由它的protectionDomain参数指定。

protected final Class findSystemClass(String name);
接收一个字符串name(该类型的全限定名),用于启动系统类装载器(必须保证能启动)。

protected final void resolveClass(Class c);
接收一个Class实例的引用作为参数,它将对该Class实例表示的类型执行连接动作(必须保证能执行连接动作)。

———————方法区:
所有线程都共享方法区,所以它们对方法区数据的访问必须被设计为是线程安全的。
方法区的大小不必说固定的,虚拟机可以根据应用的需要动态调整。
方法区不必是连续的。
方法区也可以被垃圾收集。

类型信息:
对每个装载的类型,虚拟机都会在方法区中存储一下类型信息:

  • 该类型全限定名
  • 该类型的直接超类的全限定名(除非是Object类,没有超类)
  • 该类型是类类型还是接口类型
  • 该类型的访问修饰符(public、abstract、final的某个子集)
  • 任何型直接超接口的全限定名的有序列表

  • 该类型的常量池【核心】:所有常量的类型字段方法的符号引用(索引访问)

  • 字段信息:字段名,字段类型,字段修饰符
  • 方法信息:方法名,返回类型,参数数量和类型(按序),方法修饰符
  • 除了常量以外的静态变量
  • 一个到类ClassLoader的引用:跟踪保存该类加载时所用的加载器的类别
  • 一个到Class类的引用:虚拟机会创建一个相应的java.lang.Class类的实例

———————方法表:
方法表:类型数据结构
虚拟机为每个非抽象类都生成一个表,把它作为类信息的一部分保存在方法区。
方法表是一个数组,元素是所有它的实例可能被调用的实例方法的直接引用
,包括是超类直接继承过来的方法。运行时可以通过方法表快速搜寻在对象中调用的实例方法。

———————堆:
java程序在运行时创建的所有类实例或数组都放在同一个堆中(要考虑线程同步问题)。
和方法区一样,堆空间也不是连续的内存区。

———————数组:
数组的内部表示:
在java中,数组才是真正的对象
和其他对象一样,数组总是存储在堆中。
和其他对象一样,数组也有一个与他们的类关联的Class实例,所有具有相同维度和类型的数组都是同一个类的实例(不管数组的长度是多少)。

数组类的名称由两部分组成,每一维度用一个”[“表示,用字符或字符串表示元素类型。如“[[Ljava/lang/Object”表示元素为Object的二维数组。

在堆中,每一个数组对象还必须要保存数组的长度、数组数据以及某些指向数组的类数据的引用。

———————程序计数器(PC寄存器):
每一个线程都有它自己的PC寄存器,它是在该线程启动的时候创建的。
PC寄存器的大小是字长,因此能够持有一个本地指针,也能够持有一个returnAddress。当线程执行某个java方法时,PC寄存器的内容总是写一条将被执行的指令的地址,这里的地址可以是个本地指针,也可以是在方法字节码中相对于该方法其实指令的偏移量。如果该线程正在执行一个本地方法,此时PC寄存器的值是“undefined”。

———————Java栈:
每当启动一个新线程时,java虚拟机都会为它分配一个java栈。
虚拟机智慧直接对java栈执行两种操作:以帧为单位的压栈或出栈。
每当线程调用一个java方法时,虚拟机都会在该java栈中压入一个新帧。
java方法不管是正常返回还是抛异常中止,虚拟机都会将当前栈弹出java栈,然后释放内存。

java栈和栈帧在内存中也不必说连续的。

———————栈帧(2):
栈帧的构成:

  • 局部变量区:

    是一个以字长为单位,从0开始计数的数组。字节码指令通过从0开始的索引来使用其中的数据。
     int、float、reference和returnAddress的值在数组中只占一项;
     byte、short、char的值在存入数组前都被转换成int,所以也只占一项;
     boolean虚拟机不支持,用int来表示,所以也只占一项;
     long、double的值在数组中占两项(使用只要指出第一项索引值)。
     局部变量区包含对应的方法参数和局部变量。
     按参数的顺序来存入局部变量数组中。真正的局部变量顺序任意。
    
  • 操作数栈:

    是一个以字节为单位的数组(不是通过索引来访问),是通过标准的栈操作(压栈和出栈)来访问。
    存储的数据类型和局部变量区的一样。
    虚拟机把操作数栈作为他的工作区,大多数指令都要从这里弹出数据,执行计算,然后把结果压回到操作数栈中。
    
  • 帧数据区:

    常量池解析、正常方法返回、异常派发机制。
    每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
    帧数据区还要帮助虚拟机处理java方法的正常结束或异常中止:
    如果正常结束:虚拟机必须恢复发起调用的方法的(上一个)栈帧,调用完成方法的指令的下一个指令。如果有返回值,虚拟机将把返回值压倒发
    

起调用的方法的操作数栈。
如果抛出异常,根据帧数据区的异常表来处理。如果有catch语句,就会交给catch中的代码,如果没有,则立即异常中止。

———————本地方法栈:
当线程调用本地方法时,虚拟机会保持java栈不变,不再在线程的java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

就像其他运行时内存区一样,本地方法栈占用的内存区也不必说固定大小的,它可以根据需要动态扩展或者收缩。

———————执行引擎:
任何java虚拟机实现的核心都是它的执行引擎。
在java虚拟机规范中,执行引擎的行为使用指令集来定义。
作为运行时实例的执行引擎就是一个线程。
运行中java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。



来源博客:Wang Jie's Blog's Blog
本文链接:https://blog.wangjiegulu.com/2013/02/24/看书笔记-《深入java虚拟机》——java体系结构(二)/
版权声明:本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处。