读书笔记之《深入理解Java虚拟机》(完)

Posted Paxos

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读书笔记之《深入理解Java虚拟机》(完)相关的知识,希望对你有一定的参考价值。

本篇算是2/3(已完结),剩下的1/3在之前的

Paxos,公众号:Paxos

目录

  • 10. 类文件结构小知识

  • 11. 类加载小知识

  • 12. 动态方法调用,使用方法句柄(MethodHandle)还是类反射(Reflection)?

  • 13. 为什么 JVM 采用面向操作数栈而不是寄存器的架构?

  • 14. java 中有哪些编译器?

  • 15. java 中对代码如何优化?

  • 16. 多线程是如何实现的?

  • 17. 如何实现线程安全?


10. 类文件结构小知识


(1)整个 Class 文件(指 javac 前端编译后生成的 .class 文件)本质上就是一张表(由多个无符号数和其他表复合而成,举例来说,Class 的头 4 个字节是魔数,即 0xcafebabe,然后是次主版本号,常量池,接口,字段,方法,属性等),各个数据项目紧凑排列,是一组以 8 字节为基础单位的二进制流


(2)常量池中主要存放 2 大类常量:字面量(字符串,final 修饰的常量等)和符号引用(类和接口的全限定名,字段的名称和描述符,方法的名称和描述符),常量池类似字典表,池中的项目相互引用


(3)java 程序中如果定义超过 64kb(2^16)英文字符的常量或方法名,则无法编译,因为最大长度就是 2 字节能表示的范围(2^16)


(4) 除了 java.lang.Object 外,所有 java 类的父类索引都不为 0(至少有一个共同祖先:Object)


(5)方法中的代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面


(6)JVM 中指令的种类有上限,即 256 条,目前已经定义了 200 多条


(7)虚拟机规范中明确限制了一个方法不允许超过 65535 条字节码指令


(8)在任何实例方法里面,都可以通过“this”关键字访问到方法所属的对象(this 是默认的一个方法入参)


(9)编译器使用异常表而不是简单的跳转命令来实现 java 异常及 finally 处理机制


(10)对于非 static 类型的变量(实例变量)的赋值是在实例构造器\<init\>方法中进行,而类变量,有类构造器\<clinit\>方法和 ConstantValue 属性(javac 的选择是,当 final static 修饰基本类型和 String 时使用 ConstantValue 属性)


(11)正确实现 synchronized 关键字需要 javac 编译器与 JVM 两者共同协作支持


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


11. 类加载小知识


(1)java 里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接实现的(比如 OSGi,Open Service Gateway Initiative 技术)


(2)数组类本身不通过类加载器创建,而是由 JVM 直接创建


(3) 一个类必须与类加载器一起确定唯一性


(4) Class 对象比较特殊,它虽然是对象,但是存放在方法区,这个对象将作为程序访问方法区中的这些类型数据的外部接口


(5)如果在一个类的\<clinit\>方法中有耗时很长的操作,就可能造成多个线程阻塞(因为\<clinit\>方法是线程安全的,只会有一个线程执行,而且其他操作必须在该初始化方法之后才能进行


(6)双亲委派模型并不是一个强制性的约束模型,而是推荐的类加载器实现方式,历史上共有 3 次例外:一是双亲委派模型出来之前;二是 SPI 加载动作;三是 OSGi 技术


(7)类变量有 2 次赋初始值的过程,一次在准备阶段,赋予系统初始值;另一次在初始化阶段,赋予程序员定义的初始值


12. 动态方法调用,使用方法句柄(MethodHandle)还是类反射(Reflection)?


(1)本质上,MethodHandle 与 Reflection 机制都是模拟方法调用,但 Reflection 是在模拟 java 代码层次的方法调用,而 MethodHandle 是在模拟字节码层次的方法调用;


(2)Reflection 是重量级(包含方法属性表中各种信息等),而 MethodHandle 是轻量级(仅包含与执行该方法相关的信息);


(3)MethodHandle 是对字节码的方法指令调用的模拟,理论上可被优化(比如方法内联),而通过 Reflection 去调用方法则不行;


(4)Reflection API 的设计目标是只为 java 语言服务的,而 MethodHandle 则设计成可服务于所有 JVM 之上的语言,其中也包括 java 语言


13. 为什么 JVM 采用面向操作数栈而不是寄存器的架构?




两套指令集各有优势,基于栈的指令集的优势有:


- 可移植(寄存器由硬件提供,依赖寄存器就要受到硬件的约束,用户程序不直接操作,完全由虚拟机代管)

- 编译器实现简单(不需要考虑空间分配的问题,所需空间都在栈上操作)


栈架构指令集的主要缺点是执行速度相对来说慢一点(指令多,与内存而不是寄存器交互)。所有主流物理机的指令集都是寄存器架构也从侧面印证这一点(硬件世界,CPU 最快,寄存器次之,所以寄存器都在 CPU 周围;其次是高速缓存 cache,然后才是内存)。


14. java 中有哪些编译器?


(1)前端编译器(把 \*.java 转换为 \*.class 文件): Sun 的 javac


(2)JIT 编译器(虚拟机的后端运行期编译器,把字节码转换为机器码): HotSpotVM 的 C1、C2 编译器


(3)AOT 编译器(静态提前编译器,直接把 \*.java 编译成本地机器代码):GNU Compiler for the Java


15. java 中对代码如何优化?


虚拟机设计团队把对性能的优化集中在后端的即时编译器中,这样可以让那些非 javac 产生的 Class 文件(比如 JRuby)也能同样获得编译器优化带来的好处。


javac 做了很多针对 java 语言编码过程中的优化措施,来改善程序员的编码风格和提高编码效率。相当多新生的 java 语法特性,都是靠编译器的“语法糖”来实现(例如:foreach,try-with-resources)


尽管并不是所有的 java 虚拟机都采用解释器与编译器并存的架构,但许多主流的商业虚拟机,如 HotSpotVM,都同时包含解释器与编译器。


当程序需要迅速启动和执行,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器开始发挥作用,将越来越多的代码编译成本地机器码,获得更高的执行效率。解释器可作为编译器的后备,当优化不成立时可以回退。


HotSpotVM 中采用分层编译,其中包括:


- 第 0 层,程序解释执行,解释器不开启性能监控功能,可触发第 1 层编译;

- 第 1 层,C1 编译,将字节码编译为本地代码,简单可靠的优化,如有必要将加入性能监控的逻辑;

- 第 2 层,C2 编译,也是将字节码编译为本地代码,但会启用编译耗时较长的优化,还会激进优化(会做很多乐观的假设)


哪些代码会被编译呢(即热点代码)?


- 被多次调用的方法

- 被多次执行的循环体


如何发现热点代码?


- 基于采样的热点探测

- 基于计数器的热点探测


HotSpotVM 采用第二种,具体有方法调用计数器和回边计数器(在字节码中遇到控制流向后跳转的指令称为回边。简单来说就是统计循环体执行的次数)


经典的优化动作有很多,例如:


- 无用代码消除

- 循环展开

- 循环表达式外提

- 常量传播

- 基本块重排序

- 分支预测


最具有代表性的优化技术:


(1)语言无关的经典优化技术之一:公共子表达式消除;


(2)语言相关的经典优化技术之一:数组范围检查消除;


(3)最重要的优化技术之一:方法内联;


(4)最前沿的优化技术之一:逃逸分析;


16. 多线程是如何实现的?


进程是资源分配的最小单位,而线程是 CPU 调度的最小单位。


线程的引入,把进程的资源和执行调度分开,各个线程可以共享进程资源,又可以独立调度。


主流的操作系统都提供了线程实现,java 语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理。每个已经执行 start 方法且还未结束的 Thread 类的实例就代表了一个线程。


实现线程主要有 3 种方式:


(1)使用内核线程实现(一对一的线程模型)


内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成切换。内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。


程序一般不会直接使用内核线程,而是去使用内核线程的高级接口——轻量级进程(LWP),即通常意义上所讲的线程(轻量级进程与内核线程是一对一的)。


一个系统支持轻量级进程的数量是有限的。


(2)使用用户线程实现(一对多的线程模型)


广义上,一个线程不是内核线程,就是用户线程。


狭义上,用户线程完全建立在用户空间的线程库上,系统内核不能感知线程存在。


由于没有内核的支援,是优势也是劣势,使用用户线程实现的程序一般都比较复杂,java 曾经使用过(绿色线程),最终早已放弃(JDK12)。


(3)使用用户线程加轻量级进程混合实现(多对多的线程模型)


Java 线程模型目前为基于操作系统原生线程模型实现。对于 sun JDK 来说,Windows 和 Linux 版本都是使用一对一的线程模型实现的,一条 java 线程就映射到一条轻量级进程之中,因为 Windows 和 Linux 系统提供的线程模型就是一对一的。


线程调度是指系统为线程分配处理器使用权的过程,主要有 2 种:


- 协同式线程调度(线程执行完后主动切换另外线程)

- 抢占式线程调度(由系统来分配执行时间)


java 使用的线程调度方式是抢占式调度,可通过设置线程优先级完成。不过,线程优先级不太靠谱,因为 java 线程依赖系统原生线程,所以线程调度最终还是取决于操作系统。


JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现。


17. 如何实现线程安全?


线程安全:当多个线程访问一个对象时(共享对象),如果不用考虑这些线程在运行时环境下的调度和交替执行(不假定执行次序),也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。


按照线程安全的强弱程度,可将 java 中共享的数据分为以下 5 类:


(1)不可变对象一定是线程安全的(比如 String 对象,枚举类型)


(2)绝对线程安全(符合线程安全定义的。在 java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全)


(3)相对线程安全(在 java 语言中,大部分的线程安全类都是这种类型,例如 HashTable)


(4)线程兼容(对象不是线程安全,但通过额外的同步能在并发环境下安全的使用,比如使用锁来同步的 HashMap)


(5)线程对立(即使采取额外同步,也不能正确的并发执行)


对于线程兼容的对象如何实现线程安全?


- 互斥同步(例如:synchronized 关键字,锁)

- 非阻塞同步(例如:CAS 等原子指令,需要硬件支持,可认为是乐观同步)

- 可重入代码(例如:幂等函数,相同的输入不管执行几次,都是相同的输出)

- 线程本地存储(例如:ThreadLocal,都是线程内的私有数据,当然没有同步问题)


资料来源

1. 书籍:《深入理解Java虚拟机:JVM高级特性与最佳实践-第二版》






以上是关于读书笔记之《深入理解Java虚拟机》(完)的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java虚拟机之读书笔记四 性能监控与故障处理工具

深入理解Java虚拟机之读书笔记二 垃圾收集器

深入理解Java虚拟机之读书笔记三 内存分配策略

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

《深入理解java虚拟机》读书笔记:对象的内存布局

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