logo
关于我们

技术分享

技术分享 《深入理解Java虚拟机》读后

《深入理解Java虚拟机》读后

2021-06-10


第一章

以下是我想记录的一些定义(?)  (其实都是一些基本的常识,很惭愧= =)

JDK:Java程序设计语言、JVM、Java API类库这三部分统称为JDK,是用于支持Java程序开发的最小环境。

JRE:Java API类库中的Java SE API子集和JVM这两部分统称为JRE,是支持Java程序运行的标准环境。

准确式内存管理:虚拟机可以知道内存中某个位置的数据具体是什么类型。

以下是我的疑惑:

模块化:我所认为的模块化是在逻辑上将一个程序(系统)划分成多个部分,编写的时候保持低耦合高内聚。若是套在作者所说的Java模块化实际上也就是对原有的代码进行重整?

为什么函数式编程天然适合并行运行?

第一章说到底多是些了解性的内容,需要琢磨的点也较少,不过我还是对作者抛出的一些观点产生了疑惑,希望随着学习的深入可以解决。

第二章

JVM运行时数据区

 JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域:方法区、堆、虚拟机栈、本地方法栈以及程序计数器。

程序计数器

程序计数器的作用是记录下一条指令的地址。(个人理解)

注:当执行的是Native方法,这个计数器的值为空。Native方法:即本地方法,被Native关键字所修饰,和平台有关此内存区域是JVM规范中唯一一个没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型。

每个方法在执行的同时都会创造一个栈帧(一个数据结构),用以存储方法出口、局部变量表、操作数栈、动态链接等信息。每一个方法从调用至结束对应的即是栈帧在虚拟机栈中入栈出栈的过程。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

JVM规范中,对这个区域规定了两种异常状况:

(1)线程请求的栈深度超过虚拟机所允许的深度,抛出StackOverflowError异常

(2)如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈

这个区域与虚拟机栈十分相似,只不过这个区域是为本地方法服务的。而在JVM规范中也没有对这区域的设计有什么强制规定,具体的JVM实现可以自由的实现它。所以甚至有的虚拟机将虚拟机栈与本地方法栈合为一个区域。

以上三个区域均是线程隔离的数据区

Java堆

此内存区域的唯一目的就是存放对象实例,几乎所有对象的实例都在这里分配内存。

如果在堆中没有内存可以完成实例分配并且堆也不可以再扩展,则会抛出OutOfMemoryError异常。

方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

这区域的内存回收目标主要是针对常量池和对类型的卸载。

当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常。

以上两个区域是线程共享的数据区

直接内存

直接内存并不是JVM运行时数据区的一部分,也不是JVM规范中定义的内存区域。

在NIO类中提供了一种新的I/O方式,使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

注意,这区域也可能抛出OutOfMemoryError异常。

HotSpot对象

对象创建

虚拟机遇到new指令时进行的操作:

(1)检查这个指令的参数是否能在常量池中定位到类的符号引用,并且检查这个符号引用是否已被加载、解析及初始化。若没有则必须先进行相应的类加载过程。

(2)通过类加载检查后为新的对象实例分配内存(所需内存在类加载过程中就已确定。)

分配内存有两种方式:

指针碰撞:若Java堆的内存是规整(空闲空间在一边已占用空间在另一边)的,则将指针相应的移动一定的距离即可。

空闲列表:若Java堆的内存是不规整的,则需要维护一个空闲区域的列表,分配的时候在列表中找到足够大的区域进行分配并相应的刷新列表。

(3)虚拟机将分配的内存空间都初始化为零值

(4)对对象进行必要的设置,如该对象是哪个类的实例、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中

经过以上步骤,在虚拟机视角下,一个对象就创建出来了。但在Java程序视角下,这个程序并没有执行<init>方法(即构造方法)。所以一般来说,接下来会执行<init>方法,这样一个对象就算完全构造出来了。

对象的内存布局

对象在内存中存储的布局可以分为:对象头,实例数据和对齐填充

对象头分为两部分信息:

(1)存储对象自身运行时数据,如哈希码,GC分代年龄,锁状态标志等。官方称其为Mark Word。Mark Word可以根据对象的状态复用自己的存储空间。(在32(64)位系统下,占32(64)bit)。

(2)类型指针。即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

注:如果对象是数组,对象头还必须有一部分记录数组的长度实例数据部分存储的是代码中定义的具体的字段(继承至父类的字段也会被存储下来)

对齐填充部分没有具体的含义,仅仅起到占位符的作用。(HotSpot VM要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,因此当对象实例数据部分没有对齐时,就需要对齐填充部分来补全)

这一章主要是对虚拟机运行时内存区域的介绍以及HotSpot VM对于对象的创建,内存布局的介绍。多是一些描述性的知识,需要琢磨的点也不是很多。

第三章

判断对象是否已经死亡

引用计数法:(主流的Java虚拟机不采用,原因是很难解决对象之间相互循环引用的问题)

给对象中添加一个引用计数器,每当一个地方引用它便加1,引用失效时便减1,任何时刻计数器为0则表示该对象已死

可达性分析算法:

通过一系列的称为“GC Roots”的对象作为起始点,节点之间若有引用关系则连线,称为引用链。则若一个对象已死,则它与GC Roots之间是不可达的。

在Java语言中,可作为GC Roots的对象:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象

(2)方法区中类静态属性引用的对象

(3)方法区中常量引用的对象

(4)本地方法栈中Native方法引用的对象

引用

Java将引用分为四种:强引用,软引用,弱引用,虚引用

强引用:普遍存在,如obj = new ...(),即为强引用。只要强引用存在,垃圾收集器就不会回收掉被引用的对象

软引用:被软引用关联着的对象会在系统将要发生内存溢出异常之前进行回收。若回收之后还没有充足的内除才会抛出内存溢出异常。通过SoftReference类来实现软引用

弱引用:被弱引用关联着的对象只能存活到下一次垃圾收集发生之前,不论内存是否充足,都会回收只有弱引用关联着的对象。通过WeakReference类来实现弱引用

虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。通过PhantonReference类来实现虚引用

对象的死亡

即使在可达性分析算法中被判死亡的对象也有存活的可能。真正宣告一个对象的死亡至少要经历两次标记过程:

对象与GC Roots之间不可达时,进行第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或finalize()方法已经被虚拟机调用过了则视为"没有必要"

若一个对象被视为有必要执行finalize()方法,则这个对象将会放置在F-Queue的队列中,并在一个自动建立、低优先级的Finalizer线程去执行。(不保证它能运行结束),对象逃生的唯一机会就是在执行finalize()方法时,与GC Roots建立直接或间接的关联。

若在方法中没有建立相应的关联则会进行第二次标记,也标志着对象真正的死亡

注:任何对象的finalize()方法都只会被系统自动调用一次!

垃圾收集算法

标记-清除算法

首先标记出所有需要回收的对象,之后同意回收所有被回收的对象。标记的过程即是“对象的死亡”中所描述的标记过程

缺点:标记和清除两个过程的效率都不高;清除过后空闲空间过于分散,当分配一个较大对象时,没有足够的连续空间存储,从而不得不触发另一次垃圾收集动作。

复制算法

将内存按容量分为大小相等的两块,每次只用其中的一块。当进行垃圾收集时,将还活着的对象复制到另一块内存中,之后将原先的内存块全部清除。

缺点:将内存缩小为原来的一半

商业虚拟机都采用这种收集算法来回收新生代。将内存划分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还活着的对象复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的比例是8:1

当存活下来的对象所需的内存大于Survivor空间的内存时,就需要依赖其他内存(老年代)进行分配担保。(即将不够的那一部分内存通过分配担保机制直接进入老年代)

标记-整理算法

标记过程仍与标记-清除算法一样,之后将存活的对象都向一端移动,最后清理掉端边界以外的内存。(根据老年代的特征提出)

这里有个疑问:什么叫端边界以外的内存?这个算法的清理动作究竟是怎么进行的

分代收集算法

即根据对象存活周期的不同将内存分为几块。一般是分为新生代和老年代。之后根据不同年代相应的特点选择不同的垃圾收集算法。

HotSpot的算法实现

GC停顿:在进行可达性分析时,不可以出现在分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法保证。这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因。

OopMap:在HotSpot中,通过使用一组称为OopMap的数据结构来达到直接得知哪些地方存放着对象引用的目的。

安全点

HotSpot并没有为每一条指令都生成OopMap,只是在“特定的位置”生成,而将这些位置称为安全点。即程序执行时只有在到达安全点时才能暂停开始GC

特定位置:如方法调用、循环跳转、异常跳转等(指令序列复用)

如何在GC发生时让所有线程都跑到最近的安全点上再停顿?

有两种方式:抢先式中断和主动式中断

抢先式中断:在GC发生时,中断所有线程,若有线程不处于安全点上,则恢复线程并让其运行至安全点上。(现在几乎没有虚拟机实现采用)

主动式中断:其思想为当GC需要中断时,不对线程进行操作,而是设置一个标志。每个线程执行时主动去轮询这个标志,发现中断为真时就自己中断挂起。

这里有个疑问:书中说轮询标志的地方和安全点是重合的。这意思是说,采取主动式中断,即是较之前多执行了一个轮询指令,并且在这轮询指令处生成OopMap,也即该处就是安全点?如果是这样,那采取了主动式中断就没有了让线程跑到安全点这一说了,并且如何轮询?是线程在执行的过程中一直轮询?这样似乎不太合理啊

安全区域

安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方GC都是安全的。

在线程执行到Safe Region时首先标识自己已进入Safe Region,这样在进行GC时,就不用管位于Safe Region中的线程了。当线程要离开Safe Region时,需检查系统是否已完成根节点枚举,若完成则线程继续执行,反之则必须等待直到收到可以安全离开Safe Region时为止。

安全区域是为了解决线程处于Sleep或Blocked状态时无法响应JVM的中断请求提出的。乍看可能会感觉到疑惑:JVM请求线程中断是为了防止线程在GC时改变了对象之间的引用关系。而此时线程都直接Sleep(Blocked)了,肯定不会改变对象的引用关系啊。细想一下是这样的,为的就是防止在GC过程中,线程被唤醒(不阻塞),获得CPU时间继续执行,那么这时线程就很有可能改变对象之间的引用关系。

这里有个疑问:若是有线程在被阻塞时,并没有处在安全区域呢?而这时JVM又发起了GC,应该怎么办?

垃圾收集器

这一节介绍了几种垃圾收集器

Serial收集器

Serial收集器是最基本、最古老的收集器,曾经是虚拟机新生代收集的唯一选择。这个收集器是单线程的收集器,这个“单线程”不仅仅说明它只会使用一个CPU或一个收集线程去完成垃圾收集,更重要的是在收集过程中,必须暂停其他所有的工作线程直到它收集结束。

对于单CPU环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。

Parallel Scavenge收集器

Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,也被称为“吞吐量优先”收集器。

可通过设置相关参数来控制吞吐量。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS收集器是基于“标记-清除”算法实现的,整个过程分为四个步骤:

(1)初始标记

(2)并发标记

(3)重新标记

(4)并发清除

具体过程:首先标记GC Roots能直接关联到的对象初始标记,需要GC停顿,之后进行对象之间引用关系的搜并发标记,不需要GC停顿,之后修正用户间的引用关系重新标记,需要GC停顿,最后进行清除并发清除,不需要GC停顿

CMS收集器有三个明显的缺点:

(1)对CPU资源非常敏感。在收集过程中,由于并发标记、并发清除是和用户线程并发运行的,从而会导致用户程序变慢

(2)无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生

浮动垃圾:并发清理阶段出现的垃圾,由于这部分垃圾出现在标记过程之后,所以收集器无法清理,只能留到下一次GC清理。

CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败

(3)CMS收集器是基于“标记-清除”实现的,所以收集结束后可能会有大量的空间碎片

G1收集器

G1收集器将Java堆划分成多个Region,每个Region都有一个Remembered Set用以记录相关的引用信息。G1收集器为每个Region都进行评估,并维护了一个优先列表,每次根据允许的收集时间,优先回收评估最大的Region。在G1收集器中,仍保留有新生代,老年代的概念。只不过他们都是Region的集合(不一定连续)。

G1收集器的运作过程:

(1)初始标记

(2)并发标记

(3)最终标记

(4)筛选回收

可以看到,G1收集器的运作过程与CMS的大致相同。不过有些区别:G1收集器是基于“标记-整理”算法设计的;在筛选回收中,虽然G1收集器是需要GC停顿的。虽然可以不停顿,但是由于G1收集器只对Region进行回收而不是整个Java堆,并且停顿用户线程将大幅提高收集效率。

内存分配与回收策略

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,将发起Minor GC

当申请的对象大于某个值时,直接在老年代进行分配。该值可通过参数设置。

虚拟机给每个对象定义了一个对象年龄计数器,每熬过一次Minor GC,计数器就加一。当对象年龄增加到一定程度时就会晋升至老年代。

若在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,则年龄大于等于该年龄的对象可以直接进入老年代

 这一章主要介绍了GC的相关内容,如判断对象死亡的条件是什么,Java中引用的几种类型,垃圾收集的三种算法以及几款垃圾收集器的介绍。这之中描述性的内容占得多些,涉及的算法单从理论上也较好理解。但还是有几个疑问没有解决。

第六章

无关性

这里的无关性包括:平台无关性和语言无关性。如何做到无关性?我的理解是,在程序语言和具体的底层硬件之间加上Java虚拟机。Java虚拟机将底层具体的细节给屏蔽起来,并且根据class文件进行相应的操作。所以只要每台机器都装上了Java虚拟机,那么Java程序就不用考虑具体的硬件细节,从而实现了平台无关性。而Java虚拟机关注的仅仅是class文件,所以无论是由Java语言生成的class文件或者是其他语言生成的class文件,他都一视同仁,从而实现了语言无关性。(这是本人自己比较粗糙的理解

Class类文件的结构

这里我从书中摘了Class文件的数据

《深入理解Java虚拟机》读后

Class文件的数据结构只有两种类型:无符号数和表

无符号数:基本数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节。

表:由多个无符号数或其他表组成的复合数据类型。

对于Class文件的定义是有严格的规范的,对于上表中的数据项,无论是顺序还是大小等都有严格要求,不允许更改。

高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使文件格式未发生任何变化。

常量池

常量池中主要存放两大类常量:字面量和符号引用。

字面量与Java层面的常量较为类似,如文本字符串,final修饰的常量值等。

符号引用则包括了三类常量:(1)类和接口的全限定名(2)字段的名称和描述符(3)方法的名称和描述符

常量池中的每一个常量都是一个具体的表,而表的类型共有14种,每一种表都有其相应的数据结构

以下是表的种类,值得说明的是,每个表的第一位都是u1类型的tag,用以表征它是哪种表,而这对应的即是下表中的标志这一列

《深入理解Java虚拟机》读后

具体的表结构就不摘上来了。

访问标志

access_flags这个标志用于标识一些类或者接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型等。这个标志在其他一些表结构中也有存在。

字段表

字段表用于描述接口或类中声明的变量,具体的表结构如下:

《深入理解Java虚拟机》读后

其中access_flags为访问标志;name_index为字段的简单名称;descriptor_index为字段和方法的描述符;后两项分别为属性表长和属性表。

简单名称:指没有类型和参数修饰的方法或字段名称,如:void init()的简单名称为init

描述符的作用是描述字段的数据类型、方法的参数列表和返回值

字段表中不会列出从父类继承而来的字段,但有可能列出原代码中不存在的字段,如内部类中会自动添加指向外部类实例的字段

属性表

对于属性表集合不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

每一种属性都有各自的表结构,但可以认为是下列表结构的拓展:

《深入理解Java虚拟机》读后

其中attribute_name_index代表该属性的属性名,attribute_length为属性值的长度

书中一共介绍了11中属性及其表结构,篇幅太长就不在此记录了。

不是很理解:StackMapTable属性

这一章总体来说还是很枯燥的。主要介绍了class文件中的各种表结构还有字节码指令集。说实话,对于其中的一些内容我看完之后还是没有一个概念尤其是指令集那一块,就感觉是离自己很远的东西。还是没有足够的代码量啊,接触得太少。

第七章

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

疑惑:运行期动态加载和动态连接。对于这我完全没有一点概念,不知道他所代表的含义,仅仅是一个名词。

类加载的时机

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析统称为连接。

加载、验证、准备、初始化、卸载这些阶段的顺序是确定的,但解析却不是,在某些情况下它可能在初始化之后才开始。值得一提的是,这些阶段通常是互相较差地混合式进行的,通常会在一个阶段执行的过程中激活另一个阶段。

虚拟机规定了有且只有以下5种情况必须立即对类进行初始化:

(1)遇到new、getstatic、putstatic或invokestatic这4条指令时,若类没有进行过初始化,则需要先触发其初始化。相应的Java代码场景:使用new实例化对象;读取或设置一个类的静态字段以及调用类的静态方法时

(2)对类进行反射调用的时候,若类没有进行过初始化,则需要先触发其初始化

(3)初始化一个类时,若其父类还未初始化,则需要先触发其父类的初始化。注:接口没有这个限制

(4)虚拟机启动时,指定一个要执行的主类,虚拟机会先初始化这个主类

(5)MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄所对应的类未初始化,则需要先触发这个类的初始化(对这条一脸懵逼)

类加载的过程

加载

在加载阶段,虚拟机需要完成以下事情:

(1)通过一个类的全限定名来获取类的二进制字节流(没有指明二进制字节流必须要从class文件获取

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

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

验证

验证阶段大致会完成以下四种验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证

(1)文件格式验证:主要目的是保证输入的字节流能正确地解析并存储于方法区之内,只有通过了这个阶段的验证,字节流才会进入内存的方法区中。因此,后面的三个验证阶段都基于方法区的存储结构,不会直接操作字节流

(2)元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范

(3)字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。抱着被校验类在运行过程中不会做出对虚拟机有害的操作

StackMapTable属性:描述了方法体重所有的基本块开始时本地变量表和操作栈应有的状态。在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中记录的状态是否合法即可。

(4)符号引用验证:发生在虚拟机将符号引用转为直接引用的时候,这个动作将在解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行,若无法通过符号引用验证,将抛出一个java.lang.IncompatibleClassChangeError异常的子类

注:验证阶段不是必要的

准备

准备阶段是正式为类变量分配内存及设置类变量初始值的阶段。(内存在方法区分配)

注:(1)进行内存分配的仅包括类变量;(2)此处的初始值一般指数据类型的零值

解析

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

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄

对同一个符号引用进行多次解析是很常见的事情。虚拟机需要保证的是,保证对一个符号引用解析结果的一致性。但对于invokedynamic指令,则不需要。因为invokedynamic指令的目的就是用于动态语言支持。

初始化

另一个角度表示:初始化阶段是执行类构造器<clinit>方法的过程。

<clinit>()方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块合并产生的。

静态语句块中只能访问定义在静态语句块之前的变量,定义在之后的变量,在静态语句块中可以赋值但不能访问。

接口的实现类在初始化时不会执行接口的<clinit>()方法

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,也即是说若两个类由不同的类加载器加载,那么他们肯定不相等。

双亲委派模型

系统提供了以下三种类加载器:

(1)启动类加载器

(2)扩展类加载器

(3)应用程序类加载器

应用程序都是由着3种类加载器互相配合进行加载的,如果有必要,我们还可以定义自己的类加载器

类加载器之间的关系如下图:

《深入理解Java虚拟机》读后

 

双亲委派模型要求除了顶层的启动类加载器以外,其他的类加载器都应有自己的父类加载器。这里的父子关系一般不会以继承来实现,而是使用组合关系来复用父加载器的代码。

双亲委派模型的工作过程:若一个类加载器收到了类加载的请求,那么他会先将这个请求向上传递给他的类加载器,每个类加载器都是如此。因此类加载的请求最终应该都会传到启动类加载器中,只有当父类加载器无法对这个请求进行处理时,才会将这个请求逐层的下放。

破坏双亲委派模型

书中提到了三次破坏双亲委派模型的情况:

(1)第一次是为了兼容出现双亲委派模型之前版本的代码

(2)第二次则是为了解决基础类调用回用户的代码问题。这里Java设计团队引入了一个设计:线程上下文类加载器。

(3)第三次则是OSGi为了实现模块化热部署。这个就更没什么概念了。

这一章主要介绍了有关类加载的几个过程还有类加载器的双亲委派模型。在阅读这部分内容的时候,我脑中并没有形成太多的概念。可能是我目前仍没有一些工程经验的缘故吧。根据这些内容,我能想象到一些“技术”,像是在程序运行过程中动态生成一个类的定义啊之类的。当然,这属于我的胡思乱想,我不知道在实际的工程中这是不是常态或者是工程师们一直在致力于实现的功能。题外话:写完这篇博客就开始试着做工程了,期待!

第八章

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

局部变量表

局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义的局部变量。

reference类型表示对一个对象实例的引用。虚拟机实现应能通过这个引用做到以下两点:

(1)从此引用中直接或间接地找到对象在Java堆中的数据存放的起始地址索引

(2)从此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息

若该栈帧描述的是实例方法,则局部变量表的第0个变量存储的是该对象的引用,即this

局部变量表中的Slot是可以重用的。此处影响应注意书中P239~P241的描述

方法调用

方法调用并不等同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本,暂时还不设计方法内部的具体运行过程。

静态方法、私有方法、实例构造器、父类方法在类加载的时候就会把符号引用解析为该方法的直接引用,称为非虚方法。final方法也是非虚方法

分派

理解这一节,我觉得就需要明白两点

(1)变量的静态类型和实际类型:

其实弄清楚这点很简单,举个例子就可以了:Object temp = new Integer()。这个例子中Object是变量temp的静态类型,Integer是变量temp的实际类型

静态类型的变化仅在使用时发生,变量本身的静态类型不会改变,且变量的静态类型在编译期是可知的;实际类型变化的结果仅在运行期才可确定,编译期是不知道变量的实际类型的

虚拟机重载时是通过参数的静态类型而不是实际类型作为判定依据的

(2)invokevirtual指令的运行时解析过程:

  1)找到操作数栈顶元素所指向的对象的实际类型,记作C

  2)如果在C中能找到简单名称和描述符都一致的方法,则校验访问权限,若有相应的权限则

    返回方法的直接引用,反之,抛出java.lang.AbstractMethodError异常

  3)否则,按照继承关系从下往上依次对C的父类重复第二步

  4)若始终没有找到,则抛出java.lang.AbstractMethodError异常

知道了以上这两点,我觉得对于重载和重写就可以很容易理解了

动态类型语言支持

Method Handle

invokedynamic

以上两个内容不甚理解

这一章主要介绍栈帧这个数据结构以及分派。内容较上一章较少

 

第十一章

 

方法内联

 

在我们的角度上,方法内联这一个代码优化手段看起来是非常简单的。但是实际上Java虚拟机中的内联过程却没那么简单。因为,除了类方法、私有方法、类构造器、final方法及父类方法在编译期是可以确定其方法版本之外,其它方法(虚方法)在编译期无法确定其版本的。而Java语言中默认的实例方法属于虚方法。所以照此来看,实际上Java中大多数方法是无法进行方法内联的。

 

类型继承关系分析(CHA):这技术是一种基于整个应用程序的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息。

 

编译器在内联时:

 

(1)若方法为非虚方法则直接进行内联

 

(2)否则会向CHA询问该方法是否有多个版本供选择,若只有一个版本则直接进行内联。不过这种内联属于激进优化,需要预留一个“逃生门”。

 

(3)若有多个版本则会使用内联缓存来完成方法内联。

 

内联缓存:建立在方法正常入口之前的缓存。工作原理:方法为调用前,缓存为空。当第一次调用时,记录下方法接受者的版本信息,并且每次方法调用时都比较接受者版本。若一致,则这个内联一直用下去。若不一致,则取消内联,查找虚方法表进行方法分派。

 

逃逸分析

 

逃逸分析不是直接优化代码的手段,而是为其它优化手段提供依据的分析技术

 

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸;被外部线程访问,称为线程逃逸。

 

若是能证明一个对象不会逃逸到方法或线程之外,则可以为这个变量进行一些高效的优化,如:

 

(1)栈上分配:如果确定一个对象不会逃逸到方法之外,那么我们可以直接在栈上为其分配内存,让其随着栈帧的出栈而销毁,而不是在Java堆上分配内存,等待GC。

 

(2)同步消除:如果确定一个对象不会逃逸到线程之外,那么就不需要对这个对象进行同步操作

 

(3)标量替换:标量指一个数据无法再分解成更小的数据来表示,如int,long等;反之称为聚合量。如果确定一个对象不会被外部访问,并且这个对象是可拆散的,那程序执行的时候可能不会创建这个对象,而改为创建它的若干个被这个方法使用到的成员变量来代替。

 

云祺备份软件,云祺容灾备份系统,虚拟机备份,数据库备份,文件备份,实时备份,勒索软件,美国,图书馆
  • 标签:
  • 容灾备份

您可能感兴趣的新闻 换一批

现在下载,可享30天免费试用

立即下载