《深入理解Java虚拟机》学习笔记 - 2

类文件结构

1、各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石

2、任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里,譬如类或接口也可以通过类加载器直接生成

3、根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:

a、无符号数,属于基本的数据类型,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值

b、表,是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾

Class的结构没有任何分隔符号,所以哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

4、通过javap -verbose xxx.class,可以查看class文件的字节码内容

5、全限定名:把类全名中的“.”替换成“/”。

简单名称:是指没有类型和参数修饰的方法或者字段名称

方法和字段的描述符:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

6、Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

7、Java代码的方法特征签名只包括了方法名称、参数顺序及参数类型,而字节码的特征签名还包括方法返回值以及受查异常表。

8、Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要2个Slot来存放。局部变量表中的Slot可以重用

9、虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令。编写很复杂的jsp时可能会超过,需要特别注意

10、iload_,代表了iload_0、iload_1、iload_2和iload_3这几条指令。其实就是iload指令的特殊形式,省略掉了显式的操作数。iload_0的语义与操作数为0的iload指令语义完全一致。

11、在将int或long类型窄化转换为整数类型T的时候,转换过程仅仅是简单地丢弃除最低位N个字节以外的内容,N是类型T的数据类型长度,这将可能导致转换结果与输入值有不同的正负号

12、Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(monitor)来支持的。

13、Java虚拟机规范描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令集

14、虚拟机实现的方式主要有以下两种:

a 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。

b 将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)

15、Class文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱。

 

 

虚拟机类加载机制

1、虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

2、与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

3、类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,必须按这种顺序按部就班地开始。

4、有且只有5种情况必须立即对类进行“初始化”:

1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时.如果类没有进行过初始化.则需要先触发其初始化。生成这4条抬令的最常见的Java代码场景是:使用new关键字实例化对象的时候.读取或设置一个类的静态字段(被final修饰、已在编译期把结果放人常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化, 则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4)当虚拟机启动时.用户需要指定一个要执行的主类(包含main方法的那个类).虚拟机会先初始化这个主类.

5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化,

这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用,被动引用的3个例子:

1)通过子类引用父类的静态字段,不会导致子类初始化

2)通过数组定义来引用类,不会触发此类的初始化

3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

5、接口的加载过程与类加载过程稍有一些不同:主要是上述场景中的第3条,当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

6、Java语言中对数组的访问比C/C++相对安全是因为这个类(一个由虚拟机自动生成的、直接继承于java.lang.Object的子类)封装了数组元素的访问方法

7、对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面,这个对象将作为程序访问方法区中的这些类型数据的外部接口。

8、加载完成的工作:

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

9、验证完成的工作,4个阶段:

1)文件格式验证

2)元数据验证

3)字节码验证

4)符号引用验证

10、通过程序去校验程序逻辑是无法做到绝对准确的

11、准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value = 123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。假设将上述语句改为:

public static final int value = 123;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

12、解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机Class文件格式中。

直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

13、类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化阶段是执行类构造器()方法的过程

a ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

b ()方法与类的构造函数(或者说实例构造器()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。

c 由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

d ()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。

e 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法

f 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。需要注意的是,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出()方法,其他线程唤醒之后不会再次进入()方法。同一个类加载器下,一个类型只会初始化一次。

14、对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

15、双亲委派模型

16、3次对双亲委派模型的破坏

17、OSGI(Open Service Gateway Initative)技术是基于Java语言的动态化模块化系统的一系列规范。这个规范最初由Sun、IBM、爱立信等公司联合发起,目的是使服务提供商通过住宅网关为各种家用智能设备提供各种服务。在OSGi里面,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成了一种更为复杂的、运行时才能确定的网状结构。

 

 

虚拟机字节码执行引擎

0、栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

1、局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

2、Java中占用32位以内的数据类型有:boolean、byte、char、short、int、float、reference和returnAddress8种类型。 reference类型表示对一个对象实例的引用,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束约束

3、Slot的复用会直接影响到系统的垃圾收集行为

4、局部变量表不像类变量那样存在“准备阶段”。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整形变量默认为0,布尔型变量默认为false等这样的默认值。

5、操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。

6、32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。

7、Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

8、动态连接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)

9、方法返回地址,方法的两种退出方式:

a 正常完成出口(Normal Method Invocation Completion)

b 异常完成出口(Abrupt Method Invocation Completion),使用该方式退出,是不会给它的上层调用者产生任何返回值的

10、方法调用

a 解析,方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。符合此类的方法有:静态方法、私有方法、实例构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法)

b 分派

一、静态分派

Human man = new Man();

“Human”称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

//实际类型变化

Human man = new Man();

man = new Woman();

//静态类型变化

sr.sayHello((Man) man);

sr.sayHello((Woman) man);

虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,并且静态类型是编译期可知的。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载(Overload)。变长参数的重载优先级是最低的

二、动态分派

1、与重写(Override)有着很密切的关联,根据对象的实际类型做出选择

2、invokevirtual指令的多态查找过程。这个过程是Java语言中方法重写的本质

3、我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

三、单分派和多分派

1、方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

2、Java语言是一门静态多分派、动态单分派的语言。

11、虚拟机动态分派的实现:

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

12、动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。

13、运行时异常就是只要代码不运行到这一行就不会有问题。与运行时相对应的是连接时异常,例如很常见的NoClassDefFoundError便属于连接时异常,即使会导致连接时异常的代码放在一条无法执行到的分支路径上,类加载时(Java的连接过程不在编译阶段,而在类加载阶段)也照样会抛出异常。

14、“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征

15、目前确实已经有许多动态类型语言运行于Java虚拟机之上了,如Clojure、Groovy、Jython和JRuby等,能够在同一个虚拟机上可以达到静态类型语言的严谨性与动态语言的灵活性,这是一件很美妙的事情

16、在Java虚拟机层面上提供动态类型的直接支持就成为了Java平台的发展趋势之一,这就是JDK1.7中invokedynamic指令和java.lang.invoke包出现的技术背景。

17、invokestatic、invokevirtual、invokeinterface、invokespecial。JDK1.7新增invokedynamic。由于invokedynamic指令所面向的使用者并非Java语言,而是其他Java虚拟机之上的动态语言,因此仅依靠Java语言的编译器Javac没有办法生成带有invokedynamic指令的字节码。invokedynamic指令与前面4条“invoke*”指令的最大差别就是它的分派逻辑不是由虚拟机决定的,而是由程序员决定。

18、类:MethodHandle MethodHandles MethodType

19、Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的,而解释器是虚拟机内部,所以Java程序的编译就是半独立的实现。

20、基于栈的指令集与基于寄存器的指令集

a 基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。如果使用栈架构的指令集,用户程序不会直接使用这些寄存器,就可以由虚拟机实现来自行决定把一些访问最频繁的数据(程序计数器、栈顶缓存等)放到寄存器中以获取尽量好的性能,这样实现起来也更加简单一些。栈架构的指令集还有一些其他的优点,如代码相对更加紧凑(每个字节就对应一条指令,而多地址指令集中还需要存放参数)、编译器实现更加简单(不需要考虑空间分配的问题,所需空间都在栈上操作)等。

栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。所有主流物理机的指令集都是寄存器架构也从侧面印证了这一点。

虽然栈架构指令集的代码非常紧凑,但是完成相同功能所需的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。更重要的是,栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。尽管虚拟机可以采取栈顶缓存的手段,把最常用的操作映射到寄存器中避免直接内存访问,但这也只能是优化措施而不是解决本质问题的方法。由于指令数量和内存访问的原因,所以导致了栈架构指令集的执行速度会相对较慢。

 

类加载及执行子系统的案例与实战

1、一个功能健全的web服务器,需要解决的4个问题

2、tomcat5.* 的目录结构:

a 放置在/common目录中:类库可被tomcat和所有的web应用程序共同使用

b 放置在/server目录中:类库可以被tomcat使用,对所有的web应用程序都不可见

c 放置在/shared目录中:类库可被所有的web应用程序共同使用,但对tomcat不可见

d 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对tomcat和其他的web应用程序都不可见

3、tomcat6.*将 /common、/server和/shared三个目录默认合并到一起变成了一个/lib目录。如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用tomcat5.*的加载器架构

你可能感兴趣的:(Java)