读书笔记之深入理解Java虚拟机

 2019-12-22 10:24  阅读(765)
文章分类:JVM

前言

本文内容基本摘抄自《深入理解Java虚拟机》,以供复习之用,没有多少参考价值。想要更详细了解请参考原书。

第二章

1.运行时数据区域

20191200011\_1.png

程序计数器可以看作是当前线程所执行的字节码的行号指示器,每条线程都需要有一个独立的程序计数器。如果线程执行Java方法,计数器记录正在执行的虚拟机字节码指令地址;如果执行Native方法,计数器值为空。此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常。

本地方法栈类似虚拟机栈,区别是本地方法栈为虚拟机使用到的Native方法服务。

Java堆是所有线程共享的内存区域,在虚拟机启动时创建。所有对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域。从内存回收角度,Java堆可以细分为新生代和老年代,如果使用复制算法收集,还可以分为Eden空间、From Survivor空间、To Survivor空间。从内存分配角度,线程共享的Java堆可能划分出多个线程私有的分配缓冲区(TLAB)。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区是所有线程共享区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。HotSpot虚拟机上把方法区称为永久代。但用永久代实现方法区有问题,例如String.intern()在不同虚拟机有不同表现。JDK1.7已经把原本放在永久代的字符串常量池移出。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池是方法区的一部分。Class文件中除了类的版本、字段、方法、接口等描述信息外,还有常量池,这部分将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另外一个特征是动态性,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中(intern())。

直接内存不是虚拟机运行时数据区一部分。JDK NIO引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。分配的直接内存导致各内存区域总和大于物理内存限制从而导致动态扩展时出现OutOfMenmoryError。

2.关于对象

对象的创建:20191200011\_2.png

对象的内存布局:
对象在内存中存储的布局分为:对象头、实例数据和对齐填充。对象头分为对象运行时数据和类型指针。

对象运行时数据包括HashCode、GC分代年龄和锁状态标志位等。类型指针即对象指向它的类元数据的指针。另外,如果对象是一个Java数组,那在对象头中还有一块用于记录数组长度的数据。

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐补充不是必然存在,起着占位符作用。保证对象起始地址是8字节的整数倍。

对象的访问:
Java程序通过栈上的reference数据来操作堆上的具体对象。访问方式分为使用句柄和直接指针两种。使用句柄,Java堆中会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。如果使用直接指针访问,那么Java堆对象的布局中就要放置访问类型数据的相关信息。

3.部分虚拟机启动参数
-Xms:堆的最小值
-Xmx:堆的最大值
-Xmn::堆分配给新生代的大小
-XX:+HeapDumpOnOutOfMemoryError:虚拟机出现内存溢出异常时Dump出当前内存堆转储快照
-Xss:栈容量
-XX:PermSize:永久代最小值
-XX:MaxPermSize:永久代最大值
-XX:MaxDirectMemorySize:本机直接内存大小,如果不指定,默认与-Xmx一样。

第三章

1.对象存亡
判断对象是否存活有两种方法,引用计数算法和可达性分析。

引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0的对象就是不可能再被使用的。引用计数算法的优点是实现简单,判定效率也高。缺点是它很难解决对象之间相互循环引用的问题。

可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。

对象引用:
强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。
强引用只要存在,垃圾收集器永远不会回收被引用对象。软引用用来描述还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。弱引用也用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。一个对象是否有虚引用的存在不影响生存时间,也无法通过虚引用取得对象实例。虚引用唯一目的是在垃圾回收时收到一个系统通知。

对象存亡:
宣告对象死亡至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并进行一次筛选是否有必要执行finalize()方法。若对象没有覆盖此方法或曾经调用过此方法,则没必要执行。如果在finalize()方法中重新与引用链建立联系,则对象存活,否则 进行第二次标记。

方法区的回收:
永久代回收包括废弃常量和无用的类。其中类的回收条件较为苛刻:该类所有实例已回收;该类的类加载器已回收;该类的Class对象没有被引用。满足这三个条件才可以回收,而不是必然回收。

2.垃圾收集算法
标记-清除算法:标记和清除效率都不高且标记清除后产生大量不连续内存碎片。

复制算法:堆分为一块Eden和两块Survivor,大小为8:1:1,每次使用Eden和其中一块Survivor,回收时,将上面存活的对象复制到另外一块Survivor上,清理Eden和刚才使用过的Survivor空间。当Survivor空间不够时,需要依赖老年代进行分配担保。

标记-整理算法:复制算法在对象存活率高时需要较多复制操作,效率较低。标记整理类似标记清除,不过标记后让所有存活对象移向一端,然后清理掉边界以外的内存。

分代收集算法:根据对象存活周期不同将内存划分为几块。新生代使用复制算法,老年代使用标记清除和标记整理。

3.算法实现

可达性分析时间敏感:
可达性分析从GC Roots中查找引用链,GC Roots节点包括全局性引用与执行上下文,而仅仅方法区就有数百兆,如果逐个检查,会耗费大量时间。可达性分析还需要GC Roots停顿。

安全点:
HotSpot使用一组OopMap的数据结构存放对象引用。但HotSpot没有为每条指令生成OopMap,只在安全点记录信息。一般“长时间执行”的指令,如方法调用、循环跳转、异常跳转会产生SafePoint。GC时让所有线程在最近安全点停顿分为抢先式中断和主动式中断。

安全区域:
线程处于Sleep或Blocked状态,无法响应JVM的中断请求,就无法执行到安全点挂起。安全区域是指一段代码之中,引用关系不会发生变化,这个区域任意地方开始GC都是安全的。

4.垃圾收集器
20191200011\_3.png

Serial收集器:
单线程收集器,不仅仅只会使用一个CPU或一条收集线程去完成垃圾收集工作,它在进行垃圾收集时必须暂停所有其他工作线程。优点是简单而高效。

ParNew收集器:
多线程收集器,除了多线程收集都与Serial一样。优点是除了Serial,只有它能与CMS配合工作。

Parallel Scavenge收集器:
多线程收集器,目标是吞吐量优先(CPU用于运行用户代码的时间与CPU总消耗时间的比值)。

Serial Old收集器:
Serial老年代版本,单线程收集器,“标记-整理”算法。

Parallel Old收集器:
Parallel Scavenge的老年代版本,多线程收集,“标记-整理”算法,注重吞吐量。

CMS收集器:
以获取最短回收停顿时间为目标的收集器。“标记-清除”算法,分为:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记仍然需要“Stop The World”。
初始标记仅仅标记GC Roots能直接关联到的对象,速度很快;并发标记就是进行GC Roots Tracing的过程;重新标记是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间比初始标记稍长,比并发标记短。由于整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。所以CMS的内存回收是与用户线程一起“并发”执行的。
CMS有3个缺点:对CPU资源非常敏感;无法处理浮动垃圾、会产生大量空间碎片(标记-清除)。

G1收集器:
G1的优点是并行与并发、分代收集、空间整合、可预测的停顿。回收过程分为:初始标记、并发标记、最终标记、筛选回收。

5.内存分配与回收策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
  • 空间分配担保

第四章

1.JDK命令行工具

jps:虚拟机进程状况工具。可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID。
jstat:虚拟机统计信息监视工具。可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jinfo:Java配置信息工具。可以实时地查看和调整虚拟机各项参数。
jmap:java内存映像工具。用于生成堆转储快照。
jhat:虚拟机堆转储快照分析工具。分析jmap生成的快照。
jstack:Java堆栈跟踪工具。用于生成虚拟机当时的线程快照,就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
HSDIS:JIT生成代码反汇编。

2.JDK可视化工具

jconsole:Java监视与管理控制台。
VisualVM:多合一故障处理工具。

第六章

1.Class类文件的结构
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。无符号数分为u1、u2、u4、u8。表由多个无符号数或者其他表作为数据项构成的复合数据类型。

  • 魔数
  • Class文件版本
  • 访问标志
  • 类索引、父类索引与接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

其中,属性表集合有Code属性、Exceptions属性、LineNumberTable属性、LocalVariableTable属性、SourceFile属性、ConstantValue属性、InnerClasses属性、Deprecated及Synthetic属性、stackMapTable属性、Signature属性、BootstrapMethods属性等。

2.字节码指令

  1. 加载和存储指令
  2. 运算指令
  3. 类型转换指令
  4. 对象创建与访问指令
  5. 操作数栈管理指令
  6. 控制转移指令
  7. 方法调用和返回指令
  8. 异常处理指令
  9. 同步指令

第七章

1.类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。Java语言里,类型加载、连接和初始化过程都是在程序运行期间完成的,因此Java有高度的灵活性。

2.类加载的时机
20191200011\_4.png

加载、验证、准备、初始化、卸载按部就班的开始,解析阶段可以在初始化之后开始(为了支持Java语言的运行时绑定)。

立即对类进行初始化的五种情况:

  • 遇到new(实例化对象)、getstatic(读取类的静态字段)、putstatic(设置类的静态字段)、invokestatic(调用一个类的静态方法)4条字节码指令时;
  • 使用reflect对类反射调用时
  • 初始化一个类时,如果其父类还未初始化(对于接口来说,只有在真正使用父接口时才会初始化)
  • 虚拟机启动时指定执行主类
  • 使用动态语言支持时,MethodHandle的解析结果的方法句柄所对应的类没有进行过初始化

通过子类引用父类的静态字段,不会导致子类初始化;通过数组定义来引用类,不会触发此类的初始化;常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的初始化。

3.类加载的过程

加载:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(存放在方法区);

验证:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

准备:
准备阶段是正式为类变量分配内存并设置类变量初始值(通常情况为零值,final则直接赋值)的阶段,这些变量所使用的内存都将在方法区中进行分配。

解析:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关;直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用和虚拟机实现的内存布局相关。

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析
  5. 方法类型解析
  6. 方法句柄解析
  7. 调用点限定符解析

初始化:
前面的类加载过程中,除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)。初始化阶段是执行类构造器()方法的过程。

4.类加载器
类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”就是由类加载器实现的。且类加载器左右不限于此,任意一个类都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。

双亲委派模型:
从JVM角度只存在两种类加载器:启动类加载器(Bootstrap ClassLoader),由C++实现;所有其他类加载器,由Java实现。从开发角度分为四类:启动类加载器(Bootstrap ClassLoader),<JAVA_HOME>lib目录中或被-Xbootclasspath参数指定的路径中虚拟机识别的类库,用户无法直接引用;扩展类加载器(Extension ClassLoader)负责<JAVA_HOME>libext或被java.ext.dirs系统变量指定的路径中所有类库,用户可以直接使用;应用程序类加载器(Application ClassLoader),或称系统类加载器,负责加载ClassPath上指定的类库,用户可以使用,默认类加载器;自定义类加载器。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,所有的加载请求最后都应该传送到顶层的启动类加载器,只有当父加载器反馈无法加载(搜索范围没找到需要的类)时,子加载器才会尝试自己去加载。

双亲委派模型的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,保证了Java程序的稳定运作。

破坏双亲委派模型:

  1. 面对已经存在的用户自定义类加载器的实现代码,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass()。
  2. 双亲委派模型很好地解决了各个类加载器的基础类的统一问题,但基础类可能会回调用户代码。因此引入线程上下文类加载器,可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,默认为应用程序类加载器。这其实是父类加载器请求子类加载器完成类加载,打破了双亲委派模型。
  3. 用户对程序动态性的追求导致破坏(代码热替换、模块热部署)。OSGI实现模块化部署的关键是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

第八章

1.运行时栈帧结构
栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。20191200011\_5.png

局部变量表:
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法需要分配的局部变量表的最大容量。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果执行的是实例方法,局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,可通过“this”访问。

存在一种特殊情形,对象占用内存大、此方法的的栈帧长时间不回收、方法调用次数达不到JIT编译条件,手动将不再使用的变量设置null是有用的。

局部变量不像类变量有准备阶段,没有赋初始值不能使用。

操作数栈:
操作数栈是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stack数据项中。

当一个方法刚刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈出栈操作。

概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。

动态链接:
Class文件的常量池中存有大量的符号引用,这些符号引用一部分会在类加载阶段或者第一次使用时转化为直接引用,这种转化称为静态解析;另外一部分将在每一次运行期间转化为直接引用,称为动态链接。

方法返回地址:
方法开始执行后,有两种退出方式:正常完成出口和异常完成出口。方法返回时需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。方法正常退出时,调用者的PC计数器的值可以作为返回地址,会在栈帧中保存;方法异常退出时,返回地址是要通过异常处理器表来确定,栈帧中不保存。

附加信息:
允许增加附加信息到栈帧中。

2.方法调用
方法调用阶段唯一的任务就是确定被调用方法的版本,暂不涉及方法内部运行。

Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在内存的入口地址。需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

Java虚拟机五条方法调用指令:
invokestatic:调用静态方法;
invokespecial:调用实例构造器、私有方法和父类方法。
invokevirtual:调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
invokedynamic:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

前4条调用指令分派逻辑是固化在Java虚拟机内部的,而invokeddynamic指令的分派逻辑是由用户所设定的引导方法决定的。

解析:
在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这部分调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,在类加载的时候就会把符号引用解析为该方法的直接引用。

Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外,还有final修饰的方法,虽然final使用invokevirtual调用。

分派:
解析调用一定是静态过程,在编译期间完全确定,在类加载的解析阶段就会把符号引用替换为直接引用;而分派调用可能是静态或动态。

  • 静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但有时并不唯一,原因是字面量不需要定义,所以字面量没有显式的静态类型。
  • 动态分派:运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。动态分派的典型应用是方法重写。invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型。
  • 单分派与多分派:方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少宗量,可以将分派划分为单分派和多分派。Java语言是一门静态多分派、动态单分派的语言。
  • 虚拟机动态分派实现:使用虚方法表索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址。

3.动态类型语言支持
动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。编译器就进行类型检查过程的语言就是最常用的静态类型语言。“变量无类型而变量值才有类型”也是动态类型语言的一个重要特征。

静态语言在编译期确定类型,最显著的好处是编译器可以提供严谨的类型检查;动态类型语言在运行期确定类型,可以提供更大的安全性,也使代码更加清晰和简洁。

java.lang.invoke:
这个包的主要目的是在之前JVM单纯依靠符号引用来确定调用的目标方法这种方式之外,提供一种新的动态确定目标方法的机制,称为MethodHandle。

MethodHandle与Reflection相似之处很多,区别如下:Reflection是在模拟Java代码层次的方法调用,MethodHand是在模拟字节码层次的调用;Reflection是重量级,MethodHandle是轻量级;由于MethodHandle是对字节码的模拟,所以虚拟机在这方面的优化可以被MethodHandle借鉴。

invokedynamic:
invokedynamic指令与MethodHandle机制的作用一样,解决原有4条“invoke*”指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中。区别是一个采用上层Java代码和API实现,另一个用字节码和Class中其他属性、常量来完成。

4.基于栈的字节码解释执行引擎
Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。这一部分在JVM之外进行,而解释器和解释执行在JVM内部,所以Java的编译是半独立的实现。

Java编译器输出的指令流,是一种基于栈的指令集架构。基于栈的指令集主要优点就是可移植,缺点是执行速度相对较慢。

第十章

Javac编译器

从Javac的代码来看,编译过程分为3个过程:

  1. 解析与填充符号表过程:词法、语法分析;填充符号表
  2. 插入式注解处理器的注解处理过程
  3. 语义分析与字节码生成过程:标注检查、数据及控制流分析、解语法糖、字节码生成

Java语法糖

泛型和类型擦除:泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数。Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型,并在相应地方插入了强制类型转化代码,ArrayList和ArrayList是同一个类。Java的泛型是伪泛型。

自动装箱、拆箱与循环遍历:Java语言使用最多的语法糖。

条件编译:根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉,由于这种条件编译的实现方式使用了if语句,所以只能写在方法体内部,只能实现语句基本块级别的条件编译。

第十一章

1.HotSpot虚拟机内的即时编译器

解释器与执行器:

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

HotSpot虚拟机内置两个即时编译器,分别称为Client Compiler和Server Compiler(C1和C2).目前主流的HotSpot虚拟机中,默认采用解释器与其中一个编译器直接配合的方式工作。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机会逐渐启用分层编译的策略。

编译对象与触发条件:

在运行过程中会被即时编译器编译的热点代码有两类:被多次调用的方法;被多次执行的循环体。这两种情况编译器都会以整个方法作为编译对象。

判断是否是热点代码和是否需要触发即时编译的行为称为热点探测,方式分为:基于采样的热点探测、基于计数的热点探测。

编译过程:
Server Complier和Client Complier两个编译器编译过程不一样。Client是一个简单快速的三段式编译器,主要关注局部性的优化,Server是一个充分优化过的高级编译器。

2.编译优化技术

  • 方法内联:去除方法调用的成本(建立栈帧等);可以便于在更大范围内采取后续的优化手段。
  • 冗余访问消除:
  • 复写传播
  • 无用代码消除
  • 公共子表达式消除:如果表达式E已计算过,且E中所有变量值未发生变化,那就直接用已计算过的代替
  • 数组范围检查消除:如果编译器只要通过数据流分析就可以判定循环变量的取值范围在区间内,那在整个循环中可以把数组的上下界检查消除,可以节省很多次的条件判断操作
  • 逃逸分析:分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到方法中,称为方法逃逸,还有可能被外部线程访问到,称为线程逃逸。如果确认无法逃逸,可以进行一些高效优化:栈上分配、同步消除、标量替换。

第十二章

1.Java内存模型

Java内存模型规定了所有的变量都存储在主内存中(虚拟机内存一部分)。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程变量值的传递均需要通过主内存来完成。

内存交互:lock、unlock、read、load、use、assign、store、write。

volatile:
volatile变量两种特性:第一是保证此变量对所有线程的可见性,可见性是指当一条线程修改了这个变量的值,新值对于其他线程立即可知。第二是禁止指令重排序优化。
volatile使用场景:运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;变量不需要与其他的状态变量共同参与不变约束。

原子性、可见性、有序性

先行发生原则:先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是在发生操作B之前,操作A产生的影响能被操作B观察到:程序次序规则、监视器锁规则、volatile变量规则。时间先后顺序与先行发生原则之间基本没有太大关系。

2.Java与线程

实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。Java采用一对一线程模型。

Java线程调度:线程调度是指系统为线程分配处理器使用权的过程,分别是协同式线程调度和抢占式线程调度。

线程状态:新建、运行、无限期等待、限期等待、阻塞、结束。

第十三章

1.线程安全
Java中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。

线程安全的实现方法:互斥同步、非阻塞同步、无同步方案。

2.锁优化
自旋锁和自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁。

后记

第五章、第九章、第十章的插入式注解处理器等实例分析需要重点看一下。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 读书笔记之深入理解Java虚拟机

相关推荐