Java教程

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)读书笔记

本文主要是介绍深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)读书笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

我在读 深入理解java虚拟机 这本书,把整体其中的关键点标记了,希望自己对它有个不一样的理解,也希望大家能看看这本写的很好的书

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)

pdf提取码:hdiu 

目录

前言

该本书分为下面几部分

 第一部分 走进java 

第二部分 自动内存管理

第三部分 虚拟机执行子系统

第四部分 程序编译与代码优化

第五部分 高效并发

第一部分 走近Java

第1章 走近Java

1.1概述

1.2java技术体系

1.3 Java发展史

1.4 Java虚拟机家族

1.4.2 武林盟主:HotSpot VM

1.4.3 小家碧玉:Mobile/Embedded VM

1.4.4 天下第二:BEA JRockit/IBM J9 VM

1.4.5 软硬合璧:BEA Liquid VM/Azul VM

1.4.6 挑战者:Apache Harmony/Google Android Dalvik VM

1.4.7 没有成功,但并非失败:Microsoft JVM及其他

1.4.8 百家争鸣

1.5.2 新一代即时编译器

1.6 实战:自己编译JDK

1.6.1 获取源码

1.6.3 构建编译环境

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

2.1 概述

2.2 运行时数据区域

2.3 HotSpot虚拟机对象探秘

2.4 实战:OutOfMemoryError异常

第3章 垃圾收集器与内存分配策略

3.1 概述

3.2 对象已死?

3.3 垃圾收集算法

3.4 HotSpot的算法细节实现

3.5 经典垃圾收集器

3.6 低延迟垃圾收集器

3.7 选择合适的垃圾收集器

3.8 实战:内存分配与回收策略

3.9 本章小结

第4章 虚拟机性能监控、故障处理工具

4.1 概述

4.2 基础故障处理工具

4.3 可视化故障处理工具

4.4 HotSpot虚拟机插件及工具

第5章 调优案例分析与实战

5.1 概述

5.2 案例分析

5.3 实战:Eclipse运行速度调优

5.4 本章小结

第三部分 虚拟机执行子系统

第6章 类文件结构

6.1 概述

6.2 无关性的基石

6.3 Class类文件的结构

6.4 字节码指令简介

6.5 公有设计,私有实现

6.6 Class文件结构的发展

6.7 本章小结

第7章 虚拟机类加载机制

7.1 概述

7.2 类加载的时机

7.3 类加载的过程

7.4 类加载器

7.5 Java模块化系统

7.6 本章小结

第8章 虚拟机字节码执行引擎

8.1 概述

8.2 运行时栈帧结构

8.3 方法调用

8.4 动态类型语言支持

8.5 基于栈的字节码解释执行引擎

8.6 本章小结

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

9.1 概述

9.2 案例分析

9.3 实战:自己动手实现远程执行功能

9.4 本章小结

第四部分 程序编译和代码优化

第10章 前端编译与优化

10.1 概述

10.2 Javac编译器

10.3 Java语法糖的味道

10.4 实战:插入式注解处理器

10.5 本章小结

第11章 后端编译与优化

11.1 概述

11.2 即时编译器

11.3 提前编译器

11.4 编译器优化技术

11.5 实战:深入理解Graal编译器

11.6 本章小结

第五部分 高效并发

第12章 Java内存模型与线程

12.1 概述

12.2 硬件的效率与一致性

12.3 Java内存模型

12.4 Java与线程

12.5 Java与协程

12.6 本章小结

第13章 线程安全与锁优化

13.1 概述

13.2 线程安全

13.3 锁优化

13.4 本章小结

总结



该本书分为下面几部分

 第一部分 走进java 

       第一章 主要介绍了Java技术体系过去、现在的情况以及未来的发展趋势;并介绍如何实现openjdk12

第二部分 自动内存管理

  •     第二章 虚拟机内存划分,各个区域出现内存溢出异常的常见原因
  •     第三章  垃圾收集算法和hotspot虚拟机提供的垃圾收集器
  •     第四章  jdk发布的基础命令行工具与可视化的故障处理工具的使用方法
  •     第五章  分享实际案例

第三部分 虚拟机执行子系统

  • 第六章 讲解class文件结构的各个部分 ,演示class的数据是如何存储访问的
  •  第七章 介绍类的加载 验证 准备 解析  初始化 五个阶段中虚拟机 分别进行了哪些动作
  • 第八章 分析了虚拟机在执行代码 ,如何找到正确的方法、如何执行方法内的字节码
  • 第九章 通过几个类加载执行子系统的案例中,哪些值得借鉴的地方

第四部分 程序编译与代码优化

  • 第十章 分析了java中泛型 、主动装箱 拆箱条件编译等多种语法的前因后果
  • 第十一章 讲解了虚拟机热点探测方法 、hotspot即时编译器、编译触发条件

第五部分 高效并发

  • 第十二章 讲解了虚拟机java内存模型的结构及操作,以及原子性。可见性和有序性在java内存模型种的体现
  • 第十三章 介绍了线程安全所涉及的概念和分类。介绍虚拟机在高并发的情况所做的锁一系列优化措施

第一部分 走近Java

第1章 走近Java

1.1概述

java不仅仅是一门编程语言,它还是一个由一系列计算机软件和规范组成的技术体系,这个体系提供了完整的用于软件开发和跨平台部署支撑 ;据不完全统计全世界大概有两千三百多万程序从事者,而java程序员就占600多万,这都是得益于这个技术体系而得到的支撑并广泛应用于嵌入式系统、移动终端、企业服务器、大型机等多种场合。

图 1-1 Java 技术的广泛应用

不可忽视的优点

  •  实现了“一次编写,到处运行”的理想
  • 它提供一种相对安全的内存管理和访问机制,避免了绝大部分内存泄露和指针越界的问题
  • 他实现了热点代码检测和运行时编译优化,是的java长时间运行时,代码会得到更高性能,但在某种情况下会出现问题,例如多线程的情况下,这个在开发过程中也会提供解决办法

1.2java技术体系

从广义上来讲Kotlin、Clojure、JRuby、Groovy等运行在java虚拟机上的编程语言及其相关程序都属于java程序中一员;

  • Java各个组成部分的功能来进行划分

jdk(Java Development Kit)是什么

Java程序设计语言、java虚拟机、java类库共同组成jdk;JDK是用于支持Java程序开发的最小环境  

JRE(Java Runtime Environment)是什么
把java的类库api中的java se api子集和java虚拟机这两部分的统称,JRE 是支持 Java 程序运行的标准环境。 图 1-2 Java 技术体系所包括的内容
  • 按技术体系来划分
·Java Card:支持 Java 小程序( Applets )运行在小内存设备(如智能卡)上的平台 Java ME: 支持 Java 程序运行在移动终端(手机、 PDA )上的平台,这条线是指之前像10年之前的诺基亚手机,而并不是现在的android手机 Java SE:支持面向桌面级应用(如 Windows 下的应用程序)的 Java 平台 ,而这条线则被大量使用在服务器等等 ·Java EE:支持使用多层架构的企业应用(如ERP、MIS、CRM应用)的Java平台,基本被抛弃 java.* 为包名的包都是 Java SE API 的核心包,

1.3 Java发展史

1991 年4月, Oak 开始开发 ,1995年开始 自己发展的市场定位并蜕变成为Java语言

                           图 1-3 Java 技术发展的时间线 1995 年 5 月 23 日, 正式命令为java 后面一直在迭代更新; 1999 年 4 月 27 日, HotSpot 虚拟机诞生。 2006 年 12 月 11日 ,JDK 6 发布 2014 年 3 月 18 日 JDK 8的 开发  整个发展阶段 分为sun公司阶段 和oracle公司阶段,  在 Sun 掌舵的前十几年里, Java 获得巨大成 功,同时也渐渐显露出来语言演进的缓慢与社区决策的老朽;而在 Oracle 主导 Java 后,引起竞争的同时 也带来新的活力, Java 发展的速度要显著高于 Sun 时代。

1.4 Java虚拟机家族

1.4.1 虚拟机始祖:Sun Classic/Exact VM      Classic VM,这款虚拟机只能使用纯解释器方式来执行 Java代码;也就是说当需要使用即时编译器那就必须外挂,但是假如外挂了即时编译器的话,即时编译器就会完全接管虚拟机的执行系统,解释器便不能再工作了。 在 JDK 1.2 及之前,用户用 Classic 虚拟机执行 java-version 命令

编译与解释 的区别   也是在于 编译是将代码一次性编译出来,生成二进制文件,然后在计算机上直接运行速度很快,而解释,只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的. 

其中的 “sunwjit” ( Sun Workshop JIT )就是 Sun提供的外挂编译器 ,Symantec JIT 和 shuJIT等 ,为什么会说出java语言很慢,因为用了sunwjit 编译执行器,也会每一行代码都进行编译,执行效率都慢 在JDK 1.2时 ,Solaris平台上发布过一款名为Exact VM的虚拟机 ,热点探测、两级即时编译器、编译器与解释器混合工作模式等 该 虚拟机 为后面虚拟机做了铺垫,   因为它使用了内存准确管理而得名,准确式内存管理是指虚拟机可以知道内存中某个位置的数据具体是什么类型。地址的引用类型还是一个数值为 123456的整数,准确分辨出哪些内存是引用类型 在 JDK 1.3 时, HotSpot VM成为默认虚拟机 ,它仍作为虚拟机的“备用选择”发布,可以使用 java-classic参数切换 

1.4.2 武林盟主:HotSpot VM

hotspot  虚拟机  和exact vm  最开始都很难选择那个虚拟机作为基准,HotSpot虚拟机的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知即时编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发即时编译和栈上替换编译(on-stack和replacement osr )行为。

得益于 Sun/OracleJDK 在 Java 应用中的统治地位, HotSpot 理所当然地成为全世界使用最广泛的 Java 虚拟机,是虚拟机家族中毫无争议的 “ 武林盟主 ” 。

1.4.3 小家碧玉:Mobile/Embedded VM

Java ME 中的java 虚拟机 应用在移动端的。原本它提出的卖点是智能手机上的跨平台 ,需要在开发应用中添加CDC 虚拟机,也就是移动端应用的虚拟机;

1.4.4 天下第二:BEA JRockit/IBM J9 VM

bea system公司开发的 jrockit 和ibm公司开发 的j9;JRockit虚拟机曾经号称是“世界上速度最快的Java虚拟机”. 这个速度快的原因在于它不包含解释器的实现,全部代码靠即时编译后执行。BEA将其发展为一款专门为服务器硬件和服务端应用场景高度优化的虚拟机,由于专注于服务端应用;

JRockit 随着 BEA 被 Oracle 收购,现已不再 继续发展,永远停留在 R28 版本,这是 JDK 6 版 JRockit 的代号。 IBM主力发展无疑就是J9虚拟机 IBM J9 虚拟机的职责分离与模块化做得比 HotSpot 更优秀 由 J9 虚拟机中抽象封装出来的核心组件库(包括垃圾收集器、即时编译器、诊断监控子系统等)就单独构 成了IBM OMR 项目

1.4.5 软硬合璧:BEA Liquid VM/Azul VM

Zing 虚拟机是一个从 HotSpot 某旧版代码分支基础上独立出来重新开发的高性能 Java虚拟机。 代码热点到对象分配监控、锁 竞争监控等。Zing 能让普通用户无须了解垃圾收集等底层调优,就可以使得 Java 应用享有低延迟、快 速预热、易于监控的功能,这是Zing 的核心价值和卖点

1.4.6 挑战者:Apache Harmony/Google Android Dalvik VM

Dalvik 虚拟机并不是一个 Java 虚拟机,它没有遵循《 Java虚拟机规范》;不能直接执行Java的 Class 文件,使用寄存器架构而不是 Java虚拟机中常见的栈架构。它执行的DEX(Dalvik Executable)文件可以通过Class文件转化而来,使用Java语法编写应用程序
就因为 这个程序,导致google被告。

1.4.7 没有成功,但并非失败:Microsoft JVM及其他

介绍 jvm在Windows 版本上支持

1.4.8 百家争鸣

·KVM ·Java Card VM Squawk VM ·JavaInJava

1.5.2 新一代即时编译器

Hotspot虚拟机中含有两个即时编译器

  • 编译耗时段,但输出代码优化程度较低的客户端编译器简称C1
  • 编译耗时长,但输出代码优化质量高的客户端编译器简称C2

通常他们与解释器相互协作配合共同构成hotspot虚拟机的执行子系统

自 JDK 10 起, HotSpot 中又加入了一个全新的即时编译器: Graal 编译器

1.6 实战:自己编译JDK

1.6.1 获取源码

openjdk和oraclejdk之间的关系

图 1-7 OpenJDK 和 OracleJDK 之间的关系 到了 JDK 10 及以后的版本,在组织上出现了一些新变化,此时全部开发工作统一归属到 JDK 和 JDK Updates 两条主分支上,主分支不再带版本号,在内部再用子分支来区分具体的 JDK 版本。 OpenJDK 12 。

图 1-8 OpenJDK 版本之间的关系 获取 OpenJDK 源码有两种方式。一是通过 Mercurial 代码版本管理工具从 Repository 中直接取得源 码( Repository 地址: https://hg.openjdk.java.net/jdk/jdk12 ),zip中直接下载 笔者建议尽量在 Linux 或者 MacOS 上构建 OpenJDK 

图 1-9 JDK 12 的根目录

在 x86 上构建

至少,建议使用 2-4 个内核的机器以及 2-4 GB 的 RAM。(使用的内核越多,您需要的内存就越多。)至少需要 6 GB 的可用磁盘空间(在 Solaris 上构建最少需要 8 GB)。

即使对于 32 位构建,也建议使用 64 位构建机器,而使用--with-target-bits=32.

1.6.3 构建编译环境

Ubuntu 里用户可以自行选择安装 GCC 或 CLang 来进行编译 在 Ubuntu 系统上安装 GCC 的命令 为

表 1-1 OpenJDK 编译依赖库 在 Ubuntu 中使用以下命令安装 OpenJDK 11 :

编译调试功能我放到后面进行继续做,

第二部分 自动内存管理

第 2 章 Java 内存区域与内存溢出异常 · 第 3 章 垃圾收集器与内存分配策略 · 第 4 章 虚拟机性能监控、故障处理工具 · 第 5 章 调优案例分析与实战

第2章 Java内存区域与内存溢出异常

2.1 概述

在java程序员中,在虚拟机自动内存管理机制的帮助下,像是一堵墙,隔绝开对内存的管理,一般不容易出现内存泄露等,一旦出现内存泄漏,则非常不好解决

2.2 运行时数据区域

图 2-1 Java 虚拟机运行时数据区

这就是在程序运行时在虚拟机中运行的数据区

公共的区域 方法区和堆  私有的 程序计数器 本地方法栈 虚拟机栈

2.2.1 程序计数器

程序计数器是一块较小的空间,它可以看作是当前线程所执行的字节码行号指示器,字节码解释器就是通过改变该值来选取下个需要执行的字节码指令,它是程序控制流的指示器、分支 循环 、异常处理、线程恢复都依赖它。

每个线程都有一个独立的程序计数器,这个在运行时,保证线程间来回切换;恢复到正确的执行位置,线程封闭,让我想起了threadlocal,但不是一个概念哈;

2.2.2 Java虚拟机栈 它的生命周期与线程相同,虚拟机栈描述的是,java方法执行的线程内存模型(jmm),每个方法执行时,java虚拟机都会为同步创建栈栈帧,用于存储局部变量,操作树栈,动态链接、方法出口等信息 局部变量表存放编译器可知的各种java虚拟机基本数据类型 这些数据类型在局部变量表中的存储空间以局部变量槽( Slot)来表示,其中64位长度的long和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。 实现一个变量槽是固定,然后不会被概念的 2.2.3 本地方法栈

基本与java虚拟机栈一致

2.2.4 Java堆 java堆是垃圾收集器管理的内存区域,因此在一些资料中它被称为GC堆,hotspot里面也出现了不采用分代设计的新垃圾收集器,强调可以使用非连续的内存空间 2.2.5 方法区

方法区和java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息,常量 静态变量 即时编译器编译后的代码缓存数据;在hotspot虚拟机中,使用永久代去实现方法区,为什么采用这种方式,也是由于hotspot的垃圾收集器能够像管理java堆一样管理这部分内存,省去专门为方法区编写内存管理的代码 jdk6以前,然后发现在实现过程中这样的问题,

除了和Java堆一样不需要连续的内存和可以选 择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。

2.2.6 运行时常量池

运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字段和字符引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2.2.7 直接内存 直接内存( Direct Memory )并不是虚拟机运行时数据区的一部分,也不是《 Java 虚拟机规范》中 定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建

首先在new的指令时,会检查类信息,既类是否被加载 解析 和初始化过,  在类加载确定后,将对象从堆内存中划分,有两种  如果堆内存是规整的,分配内存就仅仅把那个指针 往后挪一段与对象大小相等的距离, 指针碰撞;如果堆内存不规整,则 去寻找与需要对象内存大小足够大的空间,这种方式叫空闲列表。

对象在虚拟机中创建是非常频繁的行为,即仅仅是修改一个指针所指向的行为,有可能导致 a对象还没创建成功,然后b对象继续进行创建,内存分配出现问题。一种方案 是对分配内存空间动作进行同步处理-实际上虚拟机是采用cas配上重试机制保证原子型。

分配完内存 对象创建才刚刚开始,构造函数 

2.3.2 对象的内存布局

在new对象时,会创建对象的添加必要的设置

对象在堆内存中有三个部分  对象头,示例数据、和对齐填充 

表 2-1 HotSpot 虚拟机对象头 Mark Word

2.3.3 对象的访问定位

主流的访问方式主要有句柄和直接指针两种从虚拟机栈上访问堆上的对象数据

图 2-2 通过句柄访问对象

图 2-3 通过直接指针访问对象 句柄方式 使用代理 访问速度慢一点,但是不用频繁修改指针;而直接指针方式是访问快,对象移动时,改变移动指针 机 HotSpot 而言,它主要使用第二种方式进行对象访问

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出 在eclipse中设置 -xms20m -xmx20m -xx:+heapdumponoutofmemoryerror  不断添加创建对象,并添加到数组中,使得gc roots 还在连接中,并抛出outofmemory  常规的处理方法是首先通过内存映像分析工具(如 Eclipse Memory Analyzer )对 Dump出来的堆转储快照进行分析 内存泄漏(Memory Leak)还是内存溢出(Memory Overflow) 2.4.2 虚拟机栈和本地方法栈溢出 HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,栈容量只能由 -Xss 参数来设定 1 )如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError 异常。 2 )如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。 HotSpot 虚拟机  的选择是不支持扩展 2.4.3 方法区和运行时常量池溢出

string::intern()是一个本地方法,在jdk1.6中,intern 方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代的 字符串引用,则对比时返回false 

而jdk7  以及其他虚拟机则的intern方法 实现就不需要拷贝字符串示例到永久代,则等于true ;

不符合 intern() 方法要求 “ 首次遇到 ” 的原则  这怎么理解,intern关键字的意义在于查找常量池 是否存在,不存在则对应的操作,在1.6时,不存在都会将堆中对象复制到常量池中;如果在1.7则只是将引用放到常量池中, 也是在于永久代的取消,1.7第二次进行创建相同常量池  则会出现数据为false ,因为时对象和引用的对比肯定会返回false 2.4.4 本机直接内存溢出 Unsafe 类去申请内存  真正申请分配内存的方法是Unsafe::allocateMemory()。   计算得知内存无法分配就会 在代码里手动抛出溢出异常

第3章 垃圾收集器与内存分配策略

3.1 概述

为了达到防止栈和堆溢出,做了虚拟机很多处理,垃圾收集器 ;强大的垃圾回收机制,

3.2 对象已死?

判断哪些对象是否还存着,对象还活着;

3.2.1 引用计数算法 很多教科书 就采用这种算法来判断对象是否死掉还是活着, 在对象中添加一个引用计数器,每当有一个地方引用它时,就加1,当引用失效,计数器减一, 计数器不为0时,则不能被使用 引用计数算法( Reference Counting )虽然占用了一些额外的内存空间来进行计数,但 它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。 引用计数算法的缺陷 就不能判断对象是否存活。其实已经需要回收了 3.2.2 可达性分析算法 可达性分析(reachability analysis)算法判断对象是否存活。这个算法的基本思路就是通过一系列称为 "gc roots" 的根对象作为起始结点集,从结点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链 ,如果某个对象到 GC ROOT 间没有任何引用链相连,或者用图论的语言来说,就是gc roots 到这个对象不可达时,则证明不可能被使用 图 3-1 利用可达性分析算法判定对象是否可回收 java技术体系里面,固定可作为 GC Roots对象包括以下几种 在虚拟机栈(本地局部变量表)中引用的对象,譬如各个线程被调用的方法堆栈使用到的参数 局部变量 临时变量等 在方法区中的静态属性引用的对象,例如java类的引用类型静态变量 在方法区中常量引用的对象,譬如字符串的引用 在本地方法栈中jni引用对象 所有被同步锁synchrozide持有的对象 反映java虚拟机内部情况的xmlbean jvmti 中注册的回调 本地缓存等 根据用户所用的垃圾收集器以及当前回收的内存区域不同,共同构成gc roots 集合;保证可达性分析算法的正确性 3.2.3 再谈引用 本节主要介绍在jdk1.2之后引用概念的区分,包括强引用、弱引用、软引用、虚引用的概念 强引用是最传统的引用,是指程序代码之中普遍存在的引用赋值。即类似 object obj 这种引用关系。无论在任何情况下,只要强引用还在 垃圾回收器就不会回收 软引用是用来描述一些还有用,但非必须的对象。  在内存溢出之前回收该内存 softreference 来引用 弱引用在于只会存活到下一次垃圾收集器,使用weakreference 类来实现 虚引用 也称为 幽灵引用 或者 幻影引用 它是一种最弱的一种引用关系。虚引用的作用只是为了这个对象在垃圾回收时收到一个通知  phantomreference  3.2.4 生存还是死亡? 判断对象是否回收,除了引用计数器为0或者gc roots不可达时,也不是一定会被回收,还会处于等待状态,至少要经历两次标记, 如果finalize方法,被虚拟机调用过,或者没有被覆盖,则认为没有必要执行,就算执行,也可能不会等待该方法执行完毕,才回收, 在方法中标记引用,则会使得该对象存在着。 finalize方法不会调用两次,这就会导致,第二回收时,在赋值,该对象就面临回收 代价昂贵,所以笔者是建议大家忘记java语言中的这个方法 3.2.5 回收方法区 判断方法区中数据回收jvm虚拟机规范是不要求必须实现的 判断一个常量是否能被回收, 主要是该类的实例都已经被回收,也就是堆中不存在该类及其任何派生子类实例

3.3 垃圾收集算法

重点介绍分代收集理论和几种算法思想及其发展过程 从如何判定对象消亡的角度出发,分为两大类 引用计数器时垃圾收集和追踪式垃圾收集 ,这两类也常被称作“直接垃圾收集”和“间接垃圾收集” 3.3.1 分代收集理论 这是大部分的垃圾收集器都遵循的理论  弱分代  绝大数对象都是朝生夕灭 的 强分代假说  熬过多次垃圾回收就是越难消亡的 跨代引用假说 :跨代引用相对于同代引用来说仅占极 少数 收集器应该将java堆划分出不同区域,低代价回收大量的空间 java堆中划分不同的区域,垃圾回收器,才可以每次只回收其中某一个或者某些部分的区域 因而才有了 minor gc  majar gc full gc 回收类型划分; 设计者会把java堆划分为新生代和老年代两个区域, 存在相互引用关系的对象,是应该倾向于同时生存,同时消亡的。肯定不能全部扫描,维护一个记录数,总比扫描会划算很多 3.3.2 标记-清除算法 最基础也是最早出现的垃圾收集算法  ,mark-sweep 标记清除算法;可以标记需要被回收的,当然也可标记不被回收的对象 后续算法大部分都是以标记 清除算法为基础 对缺点进行改造  主要缺点有 1.、第一个执行效率不稳定。大量需要回收时,会出现效率疯狂降低 2.第二个导致空间的碎片化的问题, 图 3-2 “ 标记 - 清除 ” 算法示意图 3.3.3 标记-复制算法 标记 - 复制算法常被简称为复制算法。为了解决标记 - 清除算法面对大量可回收对象时执行效率低 的问题,将内存分为两块,当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 新生代的虚拟机大多数都优先采用这种收集算法,因为大多数对象,都活不过第一轮收集 将eden和survivor 种仍然存活的一次性对象复制到另外快对象;这里还有个担保机制 3.3.4 标记-整理算法 标记 -复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低 ,并且老年代相对来说就比较小,老年代肯定不选择这种方式, 标记 - 清除算法与标记 - 整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动 式的。是否移动回收后的存活对象是一项优缺点并存的风险决策 老年代移动存活对象,会停顿用户应用,就会导致用户体验变差,

3.4 HotSpot的算法细节实现

3.4.1 根节点枚举

前主流 Java 虚拟机使用的都是准确式垃圾收集, 这个任何实现有点空洞,等后期来继续分析;总的来说在降低停顿性,从而保证应用上的 使用

3.5 经典垃圾收集器

七种不同的垃圾收集器,两个垃圾收集器之间连线,就说明他们可以搭配使用。 3.5.1 Serial收集器 Serial 收集器是最基础、历史最悠久的收集器, serial收集器在jdk1.3之前是hotspot唯一的新生代收集器,他的单线程的概念不只是使用单个线程或者占用一个cpu,而是暂停其他工作线程,直到收集完成 现在都是新生代默认收集器,因为它是额外消耗最小的收集器;serial因为是单线程的,没有线程交互的开销,专心做垃圾收集,自然获得最高的单线程收集效率;只要不频繁发生收集,还是能接收的, 3.5.2 ParNew收集器 parnew实际上是serial收集器的多线程版本,多个线程垃圾收集外,对象分配规则 回收策略等,都于serial一致 不少运行在hotspot服务端虚拟机 在 jdk5发布了 cms收集器。这款收集器真正意义支持并发, 就算多线程,也会有一定的停顿。 cms收集器才真正的将垃圾收集和用户线程并发执行 分代收集器  parnew在单核心的情况下并不serial收集器的效果好,随着核心数的增加,会随着增加 3.5.3 Parallel Scavenge收集器 parallel scavenge收集器也是一款新生代收集器,他也是多线程 可并行的垃圾收集器,基于标记复制的算法 parallel scavenge 关注点为吞吐量,运行用户代码时间与处理器消耗时间的比值 停顿时间越短就适合需要与用户交互或需要保证服务响应质量的程序,主要适合后台运算不需要太多交互的分析任务 提供了两个参数用于精确控制吞吐量, -xx :maxgcpausemills 控制停顿时间,如果该值设置越小,则把新生代调小些,垃圾回收频繁,吞吐量会下降 Parallel Scavenge收集器也经常被称作 “ 吞吐量优先收集器 ” 。 3.5.4 Serial Old收集器 serial old 是标记整理算法 在老年代的收集器,是单线程的。 一种是在 JDK 5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用 [1] ,另外一种就是作为 CMS 收集器发生失败时的后备预案 3.5.5 Parallel Old收集器 既然有 serial old收集器  ,当然也有parallel old 收集器   Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记 - 整理算法实 现。 可以考虑 parallel scavenge 和parallel old 收集器结合 3.5.6 CMS收集器 cms 收集器 是一种以收获最短回收停顿时间为目标的收集器。 mark sweep 这个收集器是标记清理算法实现 整个过程分为四个步骤,包括: 1 )初始标记( CMS initial mark ) 2 )并发标记( CMS concurrent mark ) 3 )重新标记( CMS remark ) 4 )并发清除( CMS concurrent sweep ) 初始标记 重新标记会停顿用户线程,但非常快的。 主要时间在于 并发标记  ,但不用停顿用户线程, cms是一款优秀的收集器,主要体现在  并发收集、低停顿;但是它还是达不到完美的程度, CMS 收集器对处理器资源非常敏感,占用处理器的计算能力,导致程序变慢,降低总吞吐量。如果核心数在四个或者以上时,该收集器线程只占用不到25% 的处理器资源。并且会随着处理器增加而降低。增量式并发收集器,减少垃圾收集线程的独占资源时间,但资源下降没这么快,但速度会减慢许多。 cms收集器无法处理 浮动垃圾  ,也就是 在并发标记 和并发收集垃圾过程中产生的垃圾,无法在本次回收,只有下次回收 ;老年代阈值收集器在68%时就会触发回收 以适当调高参数 -XX : CMSInitiatingOccu-pancyFraction 的值 来提高 CMS 的触发百分比,降低内存回收频率,获取更好的性能;开启full gc来对碎片进行整理 3.5.7 Garbage First收集器 garbage first收集器,特殊收集器, 这个收集器,它面向的是整个堆内存在任何部分组成回收集,进行回收,衡量标准不在属于那个分代,而是在于那块内存垃圾多,回收益最大。 G1 开创的基于 Region 的堆内存布局是它能够实现这个目标的关键。 g1不在固定分为新生代还有老年代,而是把连续的堆,划分为多个相等的独立区域。把新生代和老年代分为小的了, g1将堆内存化整为0的思路,G1收集器的运作过程大致分为4步 初始标记 并发标记 最终标记 筛选回收 G1的优点  , G1 从整体来看是基于 “ 标记 - 整理 ”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现 而在大内存应用上 G1 则大多能发挥其 优势,这个优劣势的Java 堆容量平衡点通常在 6GB 至 8GB 之间,

3.6 低延迟垃圾收集器

3.6.1 Shenandoah收集器 sun研发的垃圾收集器,一直在排除该来及收集器 · 初始标记 ( Initial Marking ):与 G1 一样,首先标记与 GC Roots 直接关联的对象,这个阶段仍 是 “Stop The World” 的,但停顿时间与堆大小无关,只与 GC Roots 的数量相关。 · 并发标记 ( Concurrent Marking ):与 G1 一样,遍历对象图,标记出全部可达的对象,这个阶段 是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。 · 最终标记 ( Final Marking ):与 G1 一样,处理剩余的 SATB 扫描,并在这个阶段统计出回收价值 最高的 Region ,将这些 Region 构成一组回收集( Collection Set )。最终标记阶段也会有一小段短暂的停 顿。 · 并发清理 ( Concurrent Cleanup ):这个阶段用于清理那些整个区域内连一个存活对象都没有找到 的 Region (这类 Region 被称为 Immediate Garbage Region )。 · 并发回收 ( Concurrent Evacuation ):并发回收阶段是 Shenandoah 与之前 HotSpot 中其他收集器的 核心差异。 · 初始引用更新 并发引用更新 3.6.2 ZGC收集器 zgc收集器是一款基于region内存布局不设分代,使用读屏障,染色指针和内存多重映射等技术来实现可并发标记整理算法,以低延迟为首要目标的一款垃圾收集器;zgc基于region 的堆内存布局 染色指针的作用就是将少量额外的信息存储在指针上的技术, 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分 配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表( Forward Table ),记录从旧对象到新对象的转向关系。 ZGC 准备要对一个很大的堆做一次完整的并发收集

3.7 选择合适的垃圾收集器

3.7.1 Epsilon收集器 3.7.2 收集器的权衡 如果你接手的是遗留系统,软硬件基础设施和 JDK 版本都比较落后,那就根据内存规模衡量一 下,对于大概 4GB 到 6GB 以下的堆内存, CMS 一般能处理得比较好,而对于更大的堆内存,可重点考 察一下G1 。 3.7.3 虚拟机及垃圾收集器日志 主要涉及的是一些查看堆的命令,这个在文档中会展示出来,我们在继续看 3.7.4 垃圾收集器参数总结 包括一些收集器的参数设置,我们可以针对不同的垃圾收集器使用不用设置参数

3.8 实战:内存分配与回收策略

3.8.1 对象优先在Eden分配 大多数情况下,对象在新生代eden区中分配,当eden区没有足够空间发起一次 minor gc  当内存还是不够时会移植到老年代中去 3.8.2 大对象直接进入老年代 大对象指的是需要连续空间的java对象,最典型的大对象就是很长的字符串,或者元素非常庞大的组织, 虚拟机给每个对象定义了一个对象年龄(Age)计数器, 3.8.4 动态对象年龄判定 如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 -XX : MaxTenuringThreshold 中要求的年龄。 3.8.5 空间分配担保 虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总 空间

3.9 本章小结

垃圾收集的算法、若干款 HotSpot 虚拟机中提供的垃圾收集器的特点以及运作原理。

第4章 虚拟机性能监控、故障处理工具

4.1 概述

运用知识去处理异常故障的问题

4.2 基础故障处理工具

除了编译和运行 Java程序外,打包、部署、签名、调试、监控、运维等各种场景都可能会用到它们 图 4-1 JDK 自带工具 这些工具主要是用于监视虚拟机运行状态和进行故障处理的工具 · 商业授权工具: 主要是 JMC ( Java Mission Control )及它要使用到的 JFR ( Java Flight Recorder ), JMC 这个原本来自于 JRockit 的运维监控套件从 JDK 7 Update 40 开始就被集成到 OracleJDK 正式支持工具: 这一类工具属于被长期支持的工具,不同平台、不同版本的 JDK 之间,这类工具 可能会略有差异,但是不会出现某一个工具突然消失的情况 实验性工具: 这一类工具在它们的使用说明中被声明为 “ 没有技术支持,并且是实验性质 的 ” ( Unsupported and Experimental )产品, 4.2.1 jps:虚拟机进程状况工具 可以列出正在运行的虚拟机进 程,并显示虚拟机执行主类( Main Class , main() 函数所在的类)名称以及这些进程的本地虚拟机唯一 ID 4.2.2 jstat:虚拟机统计信息监视工具 jstat 是用于监听虚拟机各种运行状态信息的命令行工具 使用 jstat 工具在纯文本状态下监视虚拟机状态的变化,在用户体验上也许不如后文将会提到的 so s1  servivor  e eden 新生代  o 老年代  p 永久代 ygc  minor gc  fgc   full gc t  耗时时间  gc 时间  JDK 中提供了 jstatd 工具可以很方便地建立远程 RMI 服务器。 4.2.3 jinfo:Java配置信息工具 jinfo 的作用是实时查看和调整虚拟机各项参数。 4.2.4 jmap:Java内存映像工具 jmap(memory map for java) 命令用于生成堆转快照,开启参数自动转为快照, 生成dump文件  ,生成的dump文件进行分析就要用到下面的工具 4.2.5 jhat:虚拟机堆转储快照分析工具 jhat命令与jmap搭配使用,来分析jmap生成的堆转储快照, 但为什么不推荐使用,分析工作是相当耗时的,其次分析的工具效果并不好,因此不建议推荐使用 oql页标签的功能,前者可以找到内存中总容量最大的对象 4.2.6 jstack:Java堆栈跟踪工具 jstack命令用于生成虚拟机当前时刻的线程快照,一般称为threaddump 或者javacore文件,线程快照就是当前虚拟机内每个线程正在执行的堆栈的集合,生成线程快照的意义在于,定位出线程长时间出现停顿的原因,如线程间死锁,死循环等,stacktraceelement 对象获取堆栈  获取线程的意义 4.2.7 基础工具总结 无论jdk发展到什么版本,学习基础命令是一定不变的 jmap是为了生成快照查看每个类的堆栈内存, 而jstack是为了查看虚拟机的线程快照 某个类的停留时间, jstat查找虚拟机中各个堆栈占用情况,jps  查看线程 jinfo  jhat 作为性能故障查询

4.3 可视化故障处理工具

jdk中除自带的大量的命令行工具,还提供了几个功能集成度更高的可视化工具, 这类工具主要包括 JConsole 、 JHSDB、 VisualVM 和 JMC 四个。诊断调试,visualvm不是jdk中的正式成员,这几个调试工具, 4.3.1 JHSDB:基于服务性代理的调试工具 jcmd 及jhsdb 的命令行模式 jhsdb 一款基于服务型代理,实现的进程外调试工具。服务性代理是hotspot 虚拟机中一组用于映射java虚拟机运行信息的、服务性代理api,可以在独立的java虚拟机的进程里分析其他hotspot虚拟机内部数据,  堆的内存和快照,在应用的地方使用; 4.3.2 JConsole:Java监视与管理控制台 jconsole是基于xml的可视化 管理工具。它主要功能是通过j'm'x的mbean堆系统进行信息收集和参数动态调整,jmx是一种开发性的技术;不仅可以用在虚拟机本身管理上,还可以运行在虚拟机之上的软件中,直接通过jdk/bin下面的jconsole.exe进行启动。双击选择其中一个进程便可以进入主界面进行监控 2.内存监控 这段代码的作用是以 64KB/50ms 的速度向 Java 堆中填充数据,一共填充 1000 次,使用 JConsole 的 “ 内存 ” 页签进行监视 -xmn指定新生代的大小, 这段代码的作用是以 64KB/50ms 的速度向 Java 堆中填充数据,一共填充 1000 次,使用 JConsole 的 “ 内存 ” 页签进行监视 3.线程监控 那 “ 线程 ” 页签的功能就相当于可视化 的 jstack 命令了,遇到线程停顿的时候可以使用这个页签的功能进行分析。 线程 Thread-43 在等待一个被线程 Thread-12 持有的 Integer 对象,而点击线 程 Thread-12 则显示它也在等待一个被线程 Thread-43 持有的 Integer 对象,这样两个线程就互相卡住,除 非牺牲其中一个,否则死锁无法释放。 4.3.3 VisualVM:多合-故障处理工具 visual vm 是功能最强大的运行监视和故障处理程序之一,使用 jvisualvm 命令行的方式启动 3. 分析程序性能 4.BTrace动态日志跟踪 4.3.4 Java Mission Control:可持续在线的监控工具

4.4 HotSpot虚拟机插件及工具

HSDIS : JIT 生成代码反汇编 在jvm虚拟机规范里面详细定义了虚拟机 指令集中每条指令的语义,尤其是执行过程前后对操作数栈 局部变量表的影响。  这并不是jvm虚拟机规范的内容,而是hotspot自己实现的内容

第5章 调优案例分析与实战

5.1 概述

了处理 Java 虚拟机内存问题的知识与工具,在处理应用中的实际问题 时,除了知识与工具外,经验同样是一个很重要的因素。

5.2 案例分析

5.2.1 大内存硬件上的程序部署策略

给 HotSpot 虚拟机 只分配了 1.5GB 的堆内存,当时用户确实感觉到使用网站比较缓慢,但还不至于发生长达十几秒的明 显停顿,后来将硬件升级到 64 位系统、 16GB 内存希望能提升程序效能,却反而出现了停顿问题,尝试 过将 Java 堆分配的内存重新缩小到 1.5GB 或者 2GB ,这样的确可以避免长时间停顿,但是在硬件上的投 资就显得非常浪费。 而根据监控显示,主要停顿在full gc 一次full gc就可能几秒,效果非常不好,而控制full gc主要就是要使得对象朝生夕死,大多数对象生存对象不能过长,尤其不能有成批量 生存时间长的大对象产生 保证老年代空间的稳定 多数对象的生存周期应该是请求级别的或者页面级别的。 5.2.2 集群间同步导致的内存溢出 一个基于 B/S 的 MIS 系统,集群间共享数据,出现的内存溢出,里面存在着大量的org.jgroups.protocols.pbcast.NAKACK对象 图 5-1 JBossCache 协议栈 总的来说就是jboss在确认所有节点都收到正确消息之前,发送消息都必须保存到内存中,而次mis的服务端有个负责安全校验的全局过滤器,每当接收请求时,均会更新最后操作时间,并且在这个时间同步到所有的节点中去,使得一个用户在一段时间内不能在多台机器上重复登录。在服务使用过程中,往往一个页面会产生数次或者数十次的请求,因此这个过滤器导致集群各个节点之间网络交互频繁,当网络情况不能满足传输情况时,重发数据在内存不断积累,很快产生了内存溢出 5.2.3 堆外内存导致的溢出错误 这是一个学校的小型项目:基于 B/S 的电子考试系统,为了实现客户端能实时地从服务器端接收考 试数据,系统肯定使用ajax技术 选用comet1.1.1作为服务端推送框架;服务器是Jetty 7.1.4,硬件为一台很普通PC机,Core i5 CPU,4GB内存,运行32位Windows操作系统。 着 jstat 紧盯屏幕,发现垃圾收集并不频繁, Eden 区、 Survivor 区、老年代以及方 法区的内存全部都很稳定 例中使用的 CometD 1.1.1 框架,正好有大量的 NIO 操作需要使用到直接内存。 这里所有的内存总和受到操作系统进程最大内存的限制 直接内存:可通过 -XX : MaxDirectMemorySize 调整大小,内存不足时抛出 OutOf-MemoryError 或 者 OutOfMemoryError : Direct buffer memory 。 线程堆栈:可通过 -Xss 调整大小,内存不足时抛出 StackOverflowError (如果线程请求的栈深度大 于虚拟机所允许的深度)或者 OutOfMemoryError (如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时 无法申请到足够的内存)。 虚拟机和垃圾收集器:虚拟机、垃圾收集器的工作也是要消耗一定数量的内存的。 5.2.4 外部命令导致系统缓慢 一个数字校园应用系统,运行在一台四路处理器的 Solaris 10 操作系统上,中间件为 GlassFish 服务 器。系统在做大并发压力测试的时候,发现请求响应时间比较慢,通过操作系统的 mpstat 工具发现处 理器使用率很高, 执行这个 Shell 脚本是通过 Java 的 Runtime.getRuntime().exec() 方法来调用的。 这种调用方式可以达到执行 Shell 脚本的目的,但是它在 Java 虚拟机中是非常消耗资源的操作,即使外 部命令本身能很快执行完毕,频繁调用时创建进程的开销也会非常可观。 java虚拟机执行这个命令操作的过程是首先复制一个和当前虚拟机拥有一样环境变量的进程,在用这个新的进程去执行外部命令,最后在退出这个进程。如果频繁执行这个操作,系统消耗必然会很大,而且不仅仅是处理器的消耗,内存负担也很重。 用户根据建议去掉这个 Shell 脚本执行的语句,改为使用 Java 的 API 去获取这些信息后,系统很快恢 复了正常 5.2.5 服务器虚拟机进程崩溃 一个基于 B/S 的 MIS 系统,硬件为两台双路处理器、 8GB 内存的 HP 系统,服务器是 WebLogic 9.2 每个节点的虚拟机进程在崩 溃之前,都发生过大量相同的异常 通知 OA 门户 方修复无法使用的集成接口,并将异步调用改为生产者/ 消费者模式的消息队列实现后,系统恢复正 常。 5.2.6 不恰当数据结构导致内存占用过大 一个后台 RPC 服务器,使用 64 位 Java 虚拟机,内存配置为 -Xms4g-Xmx8g-Xmn1g ,使用 ParNew 加 CMS的收集器组合。 业务上需要每 10分钟加载一个约 80MB 的数据文件到内存进行数据分析,这些数据会在内存中形成超过 100 万个 HashMapLong>Entry 如果不修改程序,仅从 GC 调优的角度去解决这个问题,可以考虑直接将 Survivor 空间去掉(加入 参数 -XX : SurvivorRatio=65536 、 -XX : MaxTenuringThreshold=0 或者 -XX : +Always-Tenure ),让新生 代中存活的对象在第一次Minor GC 后立即进入老年代,等到 Major GC 的时候再去清理它们。 让新生代的数据直接放到老年代中,治本的方法必须要要修改程序,因为这里产生的根本原因是用 hashmap结构来存储数据文件效率太低 5.2.7 由Windows虚拟内存导致的长时间停顿 有一个带心跳检测功能的 GUI 桌面程序,每 15 秒会发送一次心跳检测信号,如果对方 30 秒以内都 没有信号返回,那就认为和对方程序的连接已经断开。 在 Java 的 GUI 程序中要避免这种现象,可以加入参数“Dsun.awt.keepWorkingSetOnMinimize=true”来解决。启动配置文件中就有这个参数,保证程序在恢复最小化时能够立即响 应。在这个案例中加入该参数 5.2.8 由安全点导致长时间停顿 因为集群读写压力较大,而离线分析任务对延迟又不会特别敏感,所以将 -XX : MaxGCPauseMillis 参数设置到了 500 毫秒。不过运行一段时间后发现垃圾收集的停顿经常达到 3 秒以 上,而且实际垃圾收集器进行回收的动作就只占其中的几百毫秒,现象如以下日志所示。 ·user :进程执行用户态代码所耗费的处理器时间。 ·sys :进程执行核心态代码所耗费的处理器时间。 ·real :执行动作从开始到结束耗费的时钟时间。 处理器时间,代表的是线程占用处理器一个核心的耗时计数,而时钟时间则表示的是,现实世界的时间计数 日志中的 2255 毫秒自旋( Spin )时间就是指由 于部分线程已经走到了安全点,但还有一些特别慢的线程并没有到,所以垃圾收集线程无法开始工 作,只能空转(自旋)等待

5.3 实战:Eclipse运行速度调优

5.3.1 调优前的程序运行状态 测试过程中反复启动数次 Eclipse 直到测试结 果稳定后,取最后一次运行的结果作为数据样本(为了避免操作系统未能及时进行磁盘缓存而产生的 影响) 5.3.2 升级JDK版本的性能变化及兼容问题 64MB 的永久代内存空间显然是不够的,内存溢出是肯 定的,但为何在JDK 5 中没有发生过溢出呢? 5.3.3 编译时间和类加载时间的优化 编译时间是指虚拟机的即时编译器编译热点代码的耗时,我们知道java语言为实现跨平台的语言特性,java代码编译出来后形成class文件中存储的是字节码,虚拟机通过解释的方式执行字节码命令,比起c和c++编译成本地二进制代码来说,速度要慢不少,为解决程序执行的速度问题,在1.2之后hotspot虚拟机内置两个即时编译器,如果一段java方法被调用次数达到一定程度,就会判定为热代码交给即时编译器即时编译为本地代码,提高运行速度,java的运行编译的一大缺点就是它进行编译要消耗机器的计算资源,影响程序正常时间,也就是上面说的编译时间。 相对与即时编译器,hotspot虚拟机有c1轻量级的即时编译器, 以及重量级的c2即时编译器 5.3.4 调整内存设置控制垃圾收集频率 调整full gc的时间,是很消耗时间的,在启动过程中在不断的gc,降低垃圾收集停顿时间的主要目标就是要降低full gc 这部分时间 eclipse启动时full gc 大多数是由于老年代容量扩展导致的,由永久代扩展也有部分,为了避免这些扩展导致性能上的浪费,则直接设置为 避免启动时,固定下来,避免自动扩展 把新生代容量提升到 128MB ,避免新生代频繁发生 Minor GC ; 把 Java 堆、永久代的容量分别固定为 512MB 和 96MB 5.3.5 选择收集器降低延迟

5.4 本章小结

Java 虚拟机的内存管理与垃圾收集是虚拟机结构体系中最重要的组成部分,对程序的性能和稳定 有着非常大的影响。

第三部分 虚拟机执行子系统

· 第 6 章 类文件结构 · 第 7 章 虚拟机类加载机制 · 第 8 章 虚拟机字节码执行引擎 · 第 9 章 类加载及执行子系统的案例与实战

第6章 类文件结构

代码编译得结果从本地机器码转为字节码,是存储格式发展得一小步,却是编程语言发展的一 大步

6.1 概述

我们编写的程序编译成二进制本地机器码( Native Code )已不再是唯一的选择,越来越多的程序语 言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。 因为了解了文件的编译,我们明白为什么this关键字 直接访问方法 属性等,为什么参数长度不同 ,方法名不同也可以出现等等

6.2 无关性的基石

如果全世界所有计算机得指令只有x86一种,操作系统就只有windows一种,也许就不会有java语言得出现, “ 一次编写,到处运行 ” 。 各种不同平台的虚拟机,以及所有平台都统一支持程序存储格式 -字节码是构成平台无关性的基石; Java 语言规范 和jvm虚拟机规范,实现语言无关性的基础仍然是虚拟机和字节码存储格式, java虚拟机不与包括java语言在内的任何语言绑定,它只与 class文件这种二进制文件格式所关联,class文件中包含了java虚拟机指令集 符号表与若干其他辅助信息, 图 6-1 Java 虚拟机提供的语言无关性 java是用javac编译器进行编译成class文件,  jrubyc 

6.3 Class类文件的结构

解析 Class 文件的数据结构是本章的最主要内容。   java技术能够一直保持向后兼容性,class文件结构稳定功不可没,任何一门程序语言能够获得商业上的成功,都不可能去做升级版本后,旧版本编译的产品就不再能够运行这种事情。任何一个class文件都对应着唯一的一个类或接口的类定义信息,但是反过来说,类或接口并不一定都得定义在文件里,譬如类或接口也可以动态生成,直接送入类加载器中; class文件是一组以8个字节为基础得二进制流,各个数据项目严格按照顺序紧凑得排列在文件之中 中间没有添加任何分隔符,这使得整个class文件中存储得内容几乎全部是运行得必要数据,没有空隙得存在,当遇到需要占用8个字节以上空间得数据项时,则会按照高位在前得方式分割成若干个8个字节进行存储 根据java语言规范得规定,class文件格式采用一种类似c语言结构体得伪结构来存储数据:无符号数和表,后面解析都要以这两种数据类型为基础 无符号数属于基本的数据类型,以 u1 、 u2 、 u4 、 u8 来分别代表 1 个字节、 2 个字节、 4 个字节和 8 个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 值。也就是说一些基本得数据类型 包括引用 值 数量等,都可以采用这种格式 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表的命名 都习惯性地以 “_info” 结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上也可以视 作是一张表 无论无符号数还是表,当需要描述同一类型但数量不多得多个数据时,经常用到前置容量计数器加若干连续得数据项得形式,这时候称为集合  6.3.1 魔数与Class文件的版本 每个class文件得头4个字节被称为魔数, magic number ,它得唯一作用是确定这个文件是否为java虚拟机接受的class文件,不仅仅是class文件,很多文件格式标准中都有使用魔数来进行身份校验 0xCAFEBABE (咖啡宝贝?)  笔者准备了一段最简单的 Java 代码 开头 4 个字 节的十六进制表示是0xCAFEBABE ,代表次版本号的第 5 个和第 6 个字节值为 0x0000 ,而主版本号的值 为0x0032 ,也即是十进制的 50 , 图 6-2 Java Class 文件的结构 从 JDK 1.1 到 13 之间,主流 JDK 版本编译器输出的默认的和可支持的 Class 文件版本 号。 注:从 JDK 9 开始, Javac 编译器不再支持使用 -source 参数编译版本号小于 1.5 的源码。 6.3.2 常量池 紧接着主、次版本号之后的是常量池入口,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,常量池的数量不固定的,因此在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值, constant_pool_count, 常量池中主要存放两大类常量:字面量( Literal )和符号引用( Symbolic References )。 动态连接  独立的数据结构,常量池是最繁琐的数据,这17种常量类型有着各自的完全独立的数据结构,两两之间并没有什么连系, tag 是标志位,它用于区分常量类型; name_index 是常量池的索引值,它指向常量池中一个 CONSTANT_Utf8_info 类型常量, 由于 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名 称,所以 CONSTANT_Utf8_info 型常量的最大长度也就是 Java 中方法、字段名的最大长度。 用 javap 工具的 - verbose 参数输出的 TestClass.class 文件字节码内容 所以用了17种类型去标志基本的常量 6.3.3 访问标志 在常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的方法信息,包括这个class是类还是接口,是否定义为抽象类型等等,是否是public 是否定义为abstact  access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 9 个 [1] ,没有使用到的标志位要求一 律为零 图 6-5 access_flags 标志 因此它的 access_flags 的值应为: 0x0001|0x0020=0x0021 。从图 6-5 中看到, access_flags 标志(偏移地址: 0x000000EF )的确为 0x0021 。 6.3.4 类索引、父类索引与接口索引集合 类索引( this_class )和父类索引( super_class )都是一个 u2 类型的数据,而接口索引集合 ( interfaces )是一组 u2 类型的数据的集合, 它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量 入口的第一项 u2 类型的数据为接口计数器( interfaces_count ),表示索引表 的容量。 中 javap 命令计算出来的常量池,找出 对应的类和父类的常量 6.3.5 字段表集合 字段表( field_info )用于描述接口或者类中声明的变量字段修饰符放在access_flags项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2 的数 据类型 跟随 access_flags 标志的是两项索引值: name_index 和 descriptor_index 。它们都是对常量池项的引 用,分别代表着字段的简单名称以及字段和方法的描述符。现在需要解释一下“ 简单名称 ”“ 描述符 ” 以 及前面出现过多次的“ 全限定名 ” 这三种特殊字符串的概念。 6.3.6 方法表集合 因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE 标志和 ACC_TRANSIENT 标志。与之相对, synchronized 、 native 、 strictfp 和 abstract 关键字可以修饰方法 方法里的java代码,经过javac编译器编译成字节码指令之后,存放到方法属性表种code的属性集合里面, 属性名称的索引值为 0x0009 ,对应常量为 “Code” ,说明 此属性是方法的字节码描述。 也就是说,如果两个方法有相同的名称和特征签 名,但返回值不同,那么也是可以合法共存于同一个 Class 文件中的。 6.3.7 属性表集合 与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序 这个里面存所有的code及方法代码等,所以除了固定位数并且严格顺序以外,也需要动态变化的 1.Code 属性 max_locals 代表了局部变量表所需的存储空间。  max_stack 代表了操作数栈( Operand Stack )深度的最大值。 操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存, java虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令。 code属性是class文件中最重要的属性, 到javap 中输出的“Args_size”的值 都为1 的原因,那 Locals 又为什么会等于 1? 没有定义局部变量, 那locals为什么会等于1? 在任何实例方法里面,都可以使用this来访问到此方法属性所属的对象,他得实现就是通过javac编译器编译得时候通过把this关键字访问转变成为对一个普通方法参数得访问,然后在虚拟机调用实例方法时自动传入此参数而已,因此在实例方法得局部变量表中至少放一个指向当前实例得局部变量,局部变量表中也会预留出第一个slot位来存放对象得引用,这个处理只对实例方法有效,对静态方法是没有用得, 并且将此时 x 的值复制一份副 本到最后一个本地变量表的Slot中returnValue  3.LineNumberTable 属性 linenumbertable 属性是描述java源码行号与字节码行号之间的对应关系。它并不是运行时必须的属性,但默认会生成到class文件中 4.LocalVariableTable 属性 localvariabletable属性用于描述栈帧中局部变量表中表与java源码中定义的变量之间的关系 5.SourceFile 属性 sourcefile属性用于记录生成这个class文件的源码文件名称。这个属性也是可选的 6.ConstantValue 属性 ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。在类构造器<clinit>方法中或者使用ConstantValue属性 对 final 关键字的要求是 Javac 编译器自己加入的限制。而对 ConstantValue 的属性值 只能限于基本类型和 String ConstantValue 属性是一个定长属性,它的 attribute_length 数据项 值必须固定为2 。 7.InnerClasses 属性    InnerClasses 属性用于记录内部类与宿主类之间的关联。 8.Deprecated 及 Synthetic 属性 9.StackMapTable 属性 stackmaptable属性在jdk1.6发布后增加到了class文件规范中,它是一个复杂变长属性, 10.Signature 属性 Signature 属性在 JDK 1.5 发布后增加到了 Class 文件规范之中,它是一个可选的定长属性, 可以出现于类、属性表和方法表结构的属性表中。 11.BootstrapMethods 属性 BootstrapMethods 属性在 JDK 1.7 发布后增加到了 Class 文件规范之中,它是一个复杂的变 长属性,位于类文件的属性表中。这个属性用于保存invokedynamic 指令引用的引导方法限定符

6.4 字节码指令简介

java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字称为操作码以及跟随其后的零至多代表此操作所需参数称为操作数而构成,字节码指令集可算是一种鲜明特点、优势和劣势均很突出的指令集, 6.4.1 字节码与数据类型 在java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如iload指令用于从局部变量表中的int型的 数据到 操作数栈中,而float指令加载的则是float类型数据。但在class文件中它们必须用于各自独立的操作码。 java虚拟机的操作码只有一个字节,java虚拟机的指令集对于特定的操作只提供有限的类型相关指令支持,换句话说指令集会被设计成为非完全独立的。 大部分指令都没有支持整数类型byte  short char ,甚至没有支持boolean的,编译器会在编译期间或者运行期将带符号进行扩展;实际上都是使用响应的int类型进行计算 6.4.2 加载和存储指令 加载和存储指令用于将数据在栈帧中局部变量表和操作数栈,之间来回传输,这类指令加载到操作栈 将局部变量放到操作数栈 iload 将一个数值从操作数栈存储到局部变量表:  istore 将一个常量加载操作数栈 bipush  6.4.3 运算指令 算法指令用于对两个操作数栈上的值进行某种特定特定运算,并把结果重新计算存入操作栈顶 运算指令分为两种一种是整数,一种是浮点数, · 加法指令: iadd 、 ladd 、 fadd 、 dadd · 减法指令: isub 、 lsub 、 fsub 、 dsub java虚拟机的指令集直接支持了在 java语言规范中描述各种对整数和浮点数操作, 虚拟机实现在处理浮点数时,必须严格遵循 IEEE 754 规范中所规定行为和限制 Java 虚拟机在处理浮点数运算时,不会抛出任何运行时异常 在对long类型数值进行比较时,java虚拟机采用 带符号的表达方式,而对浮点数值比较        6.4.4 类型转换指令 类型转换指令可以将两种不同的数值类型相互转换,用于实现用户显示转换;在处理窄化类型转换可能会导致转换结果产生不同正负号,精度丢失 如果浮点值是 NaN ,那转换结果就是 int 或 long 类型的 0 。  如果浮点值不是无穷大的话那转换结果就是v;否则,将根据v的符号,转换为T所能表 示的最大或者最小正数。  这里涉及到ieee754标准进行转换  《 Java 虚拟机规 范》中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。 6.4.5 对象创建与访问指令 类实例和数组都是对象,但java虚拟机对象实例和数组的创建使用了不同的字节码 指令 把数组元素加载到操作数栈指令 baload 将一个操作数栈的值储存到数组元素中的指令 bastore 取数组长度的指令  arraylength 检查类实例类型的指令 instanceof  6.4.6 操作数栈管理指令 操作普通数据结构的堆栈一样,java虚拟机提供一些用于直接操作堆栈的指令,将栈顶 pop  6.4.7 控制转移指令 控制转移指令可以让 Java 虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下 一条指令继续执行程序,在java虚拟机中有专门的指令集来处理int和reference的条件分支  6.4.8 方法调用和返回指令 invokevirtual指令  用于调用对象的实例方法, 根据对象的实例类型进行分派, 6.4.9 异常处理指令 java程序中显示抛出异常的操作 throw都有athrow指令来实现, 处理异常( catch 语句)不是由字节码指令来实现的 6.4.10 同步指令 java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构使用管程来实现得。 方法级得同步是隐式的,无须通过字节码指令控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池中方法 编译器必须确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都必须有其对 应的 monitorexit 指令,而无论这个方法是正常结束还是异常结束。 为了保证在方法异常完成时monitorenter和monitorexit指令异常正确配对 ,它的目的就是用来执行monitorexit指令

6.5 公有设计,私有实现

java虚拟机规范描绘了java虚拟机应有的共同程序存储格式, 虚拟机实现者可以使用这种伸缩性来让 Java 虚拟机获得更高的性能、更低的内存消耗或者更好的 可移植性 将输入的java虚拟机代码在加载时或执行时翻译成另一种虚拟机指令集 · 将输入的 Java 虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即即时编译器 代码生成技术)。

6.6 Class文件结构的发展

class文件结构由java虚拟机规范订立以来,已经超过20年的历史,class文件结构一直处于一个相对比较稳定的状态,class文件的主体结构,字节码指令的语义和数量上几乎没有出现变动过, class文件格式所具备的平台中立 紧凑 稳定和扩展的特点 ,是java技术体系实现平台无关 语言特性无关

6.7 本章小结

Class 文件是 Java 虚拟机执行引擎的数据入口,也是 Java 技术体系的基础支柱之一。了解 Class 文件 的结构对后面进一步了解虚拟机执行引擎有很重要的意义。

第7章 虚拟机类加载机制

代码编译的结果从本地机器码变为字节码,是存储格式发展的一小步,确是编程语言发展的一大步

7.1 概述

上一章描述的是class文件存储格式的具体细节,在class文件中描述的各类信息,最终都要加载到虚拟机中才能被运行和使用。虚拟机如何加载这些Class文件,Class文件中的信息进入到虚 拟机后会发生什么变化,这些都是本章将要讲解的内容。 虚拟机的类加载机制 ,java虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型这个过程就是Java虚拟机类加载机制;在java语言中,类型的加载、连接和初始化过程都在程序运行期间完成的,这种策略,提前编译麻烦 但提供了极高的灵活性和扩展性,比如面向接口的程序,可以等到运行时才指定其实际的实现类,

7.2 类加载的时机

一个类型被加载到虚拟机内存中开始,他的生命周期将会经历 加载  验证  准备  解析 初始化  使用  卸载七个阶段   验证、准备、解析三个部分统称 为连接(Linking) 其中 加载 校验  准备  初始化  使用 卸载 都是固定的,解析 可以在初始化之后进行   有6种情况必须对类进行初始化 1.包括 用new关键字  读取和设置一个类型的静态字段   调用一个类型的静态方法的时候。  2.java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有初始化,则会预先进行初始化 3.当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4.当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先创建 java语言种对数组的访问要比c c++ 相对安全 ,在 java语言中,当检测到数组越界时,会抛出arrayindexoutofboundsexception 异常,避免直接造成非法内存访问 接口的加载过程与类加载过程稍有不同,针对接口需要做一些特殊说明:接口也有初始化过程, 这点与类是一致的,上面的代码都是用静态语句块 “static{}” 来输出初始化信息的,

7.3 类加载的过程

Java 虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化这五 个阶段所执行的具体动作。 7.3.1 加载 loading 是类加载其中的一个过程, Java 虚拟机需要完成以下三件事情 通过一个类的全限定名来获取定义此类的二进制字节流 将这个字节流所代表的静态存储结构转换成方法区的运行时的数据结构 在内存中生成一个代表这个类的class对象,可以从网络中获取 zip中获取,反射获取数据库中获取 jsp中获取 加载阶段既可以使用java虚拟机里内置的引导类加载器来完成。 (重写一个类加载器的 findClass() 或 loadClass() 方法);java虚拟机外部二进制字节流就按照虚拟机所设定存储格式存储在方法区中了 7.3.2 验证 验证阶段是连接的第一步,这一阶段的目的是确保class文件的字节流中包含的信息符合java虚拟机规范的全部约束要求. java是相对安全的语言,如果java代码 访问数组之外的数据,就算尝试去做,都会抛出异常,拒绝编译,他和shell脚本是完全不一样的策略。 shell阶段默认认为脚本是正确的,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重
  • 1.文件格式验证
是否以魔数 0xCAFEBABE开头。· 主、次版本号是否在当前 Java 虚拟机接受范围之内。基于方法区的存储结构上进行的,不会在读取 操作字节流了
  • 2.元数据验证
第二阶段是对字节码描述的信息进行语义分析, 包括对是否有父类,不允许被继承的类,是不是抽象类
  • 3.字节码验证
第三个阶段是通过数据流分析和控制流分析,保证方法体,中类型转换有效的
  • 4.符号引用验证
最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用 [3] 的时候,这个转换动作将连接到第三个阶段 解析阶段发生 符号引用中通过字符串描述的全限定名是否能找到对应的类。 7.3.3 准备 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值的阶段,如果类字段的字段属性中存在 constantvalue属性,那在准备阶段变量值就会被初始化属性所指定的初始值 7.3.4 解析 解析阶段是java虚拟机将常量池内符号引用替换为直接引用的过程,符号引用是一组符号来描述所引用的目标,符号可以是任何字面量,java语言本身是一门静态语言 1.类或接口的解析 2.字段解析 要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_index [3] 项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用。 3.方法解析 如果解析成功,那么我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的方法搜索 1 )由于 Class 文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的 方法表中发现 class_index 中索引的 C 是个接口的话 4. 接口方法解析 接口方法也是需要先解析出接口方法表的 class_index [5] 项中索引的方法所属的类或接口的符号引 用 7.3.5 初始化 类的初始化阶段是类加载的最后阶段,初始化阶段就是去执行类执行器 cinit 方法过程,javac编译器的自动生成物, ·<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块( static{} 块)中的 语句合并产生的, Java 虚拟机会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行 完毕。因此在 Java 虚拟机中第一个被执行的 <clinit>() 方法的类型肯定是 java.lang.Object 。 cinit方法对于类或接口并不是必须的,如果一个类,中没有静态语句块,也没有对变量的赋值操作 Java 虚拟机必须保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁同步,如果多个线程同 时去初始化一个类

7.4 类加载器

java虚拟机团队有意把类加载阶段中的 通过一个类的全限定名来获取描述该类的二进制字节流 这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为类加载器  classload 类加载器可以说是 Java语言的一项创新,它是早期 Java 语言能够快速流行的重要原因之一 7.4.1 类与类加载器 类加载器虽然只用于实现类的加载动作,但它在java程序中远不止类加载阶段。 类加载器和这个类本身一起共同确立其在 Java虚拟机中的唯一性,这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回结果 7.4.2 双亲委派模型 Java虚拟机的角度来看,只存在两种类加载器,一种是启动类加载器 bootstrap classloader ,这个类加载器使用 C++ 语言实现 [1] ,是虚拟机自身的一部分和 独立存在于虚拟机外部 , 全都继承自抽象类 java.lang.ClassLoader。 java实现的 三层类加载器 · 启动类加载器( Bootstrap Class Loader):这个类加载器负责加载存放在 <JAVA_HOME>\lib目录 按照文件名识别,如 rt.jar 、 tools.jar ,名字不符合的类库即使放在 lib 目录中也不会被加载 扩展类加载器( Extension Class Loader ):  它负责加载<JAVA_HOME>\lib\ext 目录中 应用程序类加载器( Application Class Loader) 由于应用程序类加载器是 ClassLoader 类中的 getSystem- ClassLoader() 方法的返回值,所以有些场合中也称它为 “ 系统类加载器 ” 双亲委派模型:如果类加载器收到类加载的请求,它首先不会自己尝试加载这个类,而是把请求委派给父类加载器去执行完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到最顶层的启动类加载器中,只有父类反馈说没有找到时,子类才会完成加载 7.4.3 破坏双亲委派模型 双亲委派模型并不是一个具有强制性约束的模型,java设计者们推荐给开发者使用的类加载方式 双亲委派模型主要出现过 3 次较大规模 “ 被破坏 ” 的情况。 1. 在jdk1.2时已经有 自定义类加载器,则采用findclass 方法去, 如果父类加载失败,会自动调用自己的 findClass() 方法来完成加载,这样 既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。 2. 有基础类型又要调用回用户的代码  ,线程上下文类加载器 ,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 JNDI服务使用这个线程上下文类 加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,打通了双亲委派模型的层次结构来逆向使用类加载器,Java模块化标准OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现, 每一个程序模块( OSGi 中称为 Bundle)都有一个自己的类加载器

7.5 Java模块化系统

在jdk9中引入java模块化系统是对java技术的一次重要升级,为了能够实现模块化的关键目标 ——可配置的封装隔离机制,Java虚拟机对类加载架构也做出了相应的变动调整,依赖其他模块的列表 可配置的封装隔离机制首先要解决 JDK 9 之前基于类路径( ClassPath )来查找依赖的可靠性问 题。 7.5.1 模块的兼容性 为了使可配置的封装隔离机制能够兼容传统的类路径查找机制,jdk9提出了与类路径相对应的模块路径的概念,如果同一个模块发行了多个不同的版本,那只能由开发者在编译打包时人工选择好正 确版本的模块来保证依赖的正确性。 Java 模块化系统目前不支持在模块定义中加入版本号来管理和约 束依赖,本身也不支持多版本号的概念和版本选择功能。          “javac--module-version”来指定模块版本, 在 Java 类库 API中也存在 java.lang.module.ModuleDescriptor.Version这样的接口可以在运行时获取到模块的版本号。 7.5.2 模块化下的类加载器 为了模块化系统顺利施行,模块化下的类加载器仍然发生了一些应该被注意到变动 平台类加载器和应用程序类加载器都不再派生自urlclassloader ,在更高的版本全部继承于builitinclassloader 在jdk1.9之后类加载器发生了大的改变

图7-6    JDK 9及以后的类加载器继承架构

平台内部委派关系 在jdk中仍然维持着三层类加载器和 双亲委派模型的架构,但类加载的委派关系也发生了变化,

7.6 本章小结

本章主要介绍的是类加载的过程 加载验证 准备解析 和初始化的工作; 第 8 章我们将探索 Java 虚拟机的执行引擎

第8章 虚拟机字节码执行引擎

代码编译结果从本地机器码转变成字节码,是存储格式发展一小步,却是编程语言发展的一大步

8.1 概述

执行引擎是java虚拟机核心的组成部分之一,虚拟机相对与物理机的概念,这两种机器都有代码执行的能力,其区别是物理机的执行引擎是直接建立在cpu 缓存 指令集和操作系统的 ,而虚拟机的执行引擎则是由软件自行实现的。

8.2 运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元 ,栈帧则是用于支持虚拟机进行方法调用和方法执行的背后结构,的局部变量表、操作数栈、动态连接和方法返回地址,一个栈帧需要分配多少内存,并不会收到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现栈内存布局 Java 程序的角度来看,同一时刻、同一条线程里面,在 调用堆栈的所有方法都同时处于执行状态。 8.2.1 局部变量表 局部变量表是一组变量值的存储空间,用于存放方法参数和内部定义的局部变量,方法的code属性的max_locals数据项中确定了方法所需分配的        局部变量表的最大容量 以变量槽( Variable Slot )为最小单位。 Java 虚拟机的数据类型 对于64位数据类型,java虚拟机会以高位对齐的方式对分配的连续的变量空间,java虚拟机通过索引的方式使用局部变量表。索引值的范围是从0开始至局部变量表最大的变量槽数量。虚拟机不允许采用任何方式单独访问其中的某一个;当一个方法被调用时,java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程 placeholder 能否被回收的根本原因就是:局部变量表中的变量槽是否还存有 关于 placeholder 数组对象的引用。 placeholder 原本所占用的变量槽还没有被其他变量 所复用,所以作为GC Roots 一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断, 绝大部分情况下影响都很轻微。 但局部 变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。 8.2.2 操作数栈 操作数栈也常被称为操作栈,它是一个先入后出, 两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在 大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。其实并不是说两个栈帧里面的操作数栈一定是互斥的 8.2.3 动态连接 每个栈帧都包含一个指向运行时常量池中栈帧所属方法的引用, 字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。 8.2.4 方法返回地址 返回的退出只有两种方式,一种是正常的退出字节码指令,还有一种是异常的方式, 8.2.5 附加信息 一 般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

8.3 方法调用

方法调用并不等同于方法中代码执行,方法调用阶段唯一的任务就是确定被调用方法的版本 (即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。 8.3.1 解析 承接前面关于方法调用的话题,所有方法调用的目标方法在 Class 文件里面都是一个常量池中的符 号引用,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法 的调用被称为解析 合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法 4种, 这 5 种方法调用会在类加载的时候就可以把符号引 用解析为该方法的直接引用。这些方法统称为 “ 非虚方法 ” ( Non-Virtual Method ),与之相反,其他方 法就被称为“ 虚方法 ” ( Virtual Method )。 使用 invokestatic 、 invokespecial 调用的方法之外还有一种,就是被 final 修饰 的实例方法。 final 方法是使用 invokevirtual指令来调用的, 解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号 引用全部转变为明确的直接引用,不必延迟到运行期再去完成。 8.3.2 分派 java是一门面向对象的程序语言,java语言具备三个特征 继承 封装 多态 如 “ 重载 ” 和 “ 重写 ” 在 Java虚拟机之中是如何实现的,这里的实现当然不是语法上该如何写 1. 静态分派 代码清单 8-6 中的代码实际上是在考验阅读者对重载的理解程度 ,静态分派发生在编译器,静态分派动作实际不是由虚拟机来执行的 Javac编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一”的 2. 动态分派 在 Java 里面只有虚方法存在, 字段永远不可能是虚的,换句话说,字段永远不参与多态,哪个类的方法访问某个名字的字段时,该 名字指的就是这个类能看到的那个字段。 先隐式调用了 Father 的构造函数, 3. 单分派与多分派 方法的接收者与方法的参数统称为方法的宗量 单分派是根据一个宗量对 目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。 主要进行拆分开所有的 4. 虚拟机动态分派的实现

8.4 动态类型语言支持

java虚拟机的字节码指令集的数量自从sun公司的第一款java虚拟机问世至今,i nvokedynamic 指令 实现动态类型语言( Dynamically Typed Language )支持而进行的改进之一, 8.4.1 动态类型语言 动态语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的。 一门语言的哪一种检查行为要在运行期进行,哪一种检查要在编译期进行并没有什么 必然的因果逻辑关系, ECMAScript ( JavaScript )中情况则不一样,无论 obj 具体是何种类型,无论其 继承关系如何,只要这种类型的方法定义中确实包含有println(String) 方法,能够找到相同签名的方 法,调用便可成功。 产生这种差别产生的根本原因是 Java 语言在编译期间却已将 println(String) 方法完整的符号引用 8.4.2 Java与动态类型 动态语言相对于静态语言来说有更大的灵活性,(如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配 Java 虚拟机层次上去解决才最合适  8.4.3 java.lang.invoke包 jdk时新加入java.lang.invoke包, JSR 292 的一个重要组成部分,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,也提供一种新的动态确定目标方法机制,称为方法句柄。 8.4.4 invokedynamic指令 8.4.5 实战:掌控方法分派规则 invokedynamic 指令与此前 4 条传统的 “invoke*” 指令的最大区别就是它的分派逻辑不是由虚拟机决 定的,而是由程序员决定。
void thinking() { try {MethodType mt = MethodType.methodType(void.class); Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); lookupImpl.setAccessible(true); MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class,"thinking", mt, GrandFather.class); mh.invoke(this); } catch (Throwable e) { } }

8.5 基于栈的字节码解释执行引擎

8.5.1 解释执行 大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集 之前, 源码转化为抽象语法树 8.5.2 基于栈的指令集与基于寄存器的指令集 javac编译器输出的字节码指令流,基本上 [1]是一种基于栈的指令集架构(Instruction Set Architecture , ISA ),字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工 作。与之相对的另外一套常用的指令集架构是基于寄存器的指令集,最典型的就是x86的二地址指令集, iadd 指令集的 栈架构指令集的主要缺点是理论上执行速度相对来说会稍慢一些,所有主流物理机的指令集都是 寄存器架构 [3] 也从侧面印证了这点。 8.5.3 基于栈的解释器执行过程 利用一段简单的算法来解释运算过程
public int calc() { 
int a = 100; 
int b = 200; 
int c = 300; 
return (a + b) * c; 
}

代码清单8- 18    一段简单的算术代码的字节码表示

public int calc ();
Code :
Stack=2, Locals=4, Args_size=1
0 :   bipush  100
2 :   istore_1
3 :   sipush  200
6 :   istore_2
7 :   sipush  300
10 :  istore_3
11 :  iload_1
12 :  iload_2
13 :  iadd
14 :  iload_3
15 :  imul
16 :  ireturn
}

javap提示这段代码需要深度为2的操作数栈和4个变量槽的局部变量空间

这其中会涉及到一个算法逆波兰表达式 ireturn 指令是方法返回指令之一,它将结束方法执行并将操作数栈顶 的整型值返回给该方法的调用者。到此为止,这段方法执行结束。 虚拟机最终会对执行过程做出一系列优化来提高 性能,实际的运作过程并不会完全符合概念模型的描述。

8.6 本章小结

执行时涉及的内存结构 以及java程序 是如何存储的。如何载入的

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

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一 大步。

9.1 概述

在class文件格式与执行引擎这部分里,用户程序能直接参与的内容并不太多,class文件以何种格式存储,类型何时加载,如何连接、以及虚拟机如何执行字节码,主要是字节码生产与类加载器这两部分的功能,

9.2 案例分析

类加载器和字节码的案例各有两个。动态加载的 9.2.1 Tomcat:正统的类加载器架构 主流的web服务器,都自定义了类加载器, 而且一般还都不止一个。因此所使用的类库实现相互隔离, · 部署在同一个服务器上的两个 Web 应用程序所使用的 Java 类库可以实现相互隔离。 · 部署在同一个服务器上的两个 Web 应用程序所使用的 Java 类库可以互相共享。 · 服务器需要尽可能地保证自身的安全不受部署的 Web 应用程序影响。 支持 JSP 应用的 Web 服务器,十有八九都需要支持 HotSwap 功能。 一同分析 Tomcat 具体是如何规划用户类库结构和类加载器的。 为了支持这套目录结构,并对里面的目录做隔离,tomcat自定义了多个类加载器 Common 类加载器、 Catalina 类加载器(也称为 Server 类 加载器)、Shared 类加载器和 Webapp 类加载器则是 Tomcat 自己定义的类加载器 每一个web应用程序对应一个webapp类加载器,Common类加载器能加载的类都可以被Catalina类加载器和Shared类加载器使用, 9.2.2 OSGi:灵活的类加载器架构 学习类加 载器的知识,就推荐去看OSGi 源码 OSGi 中的每个模块(称为 Bundle )与普通的 Java 类库区别并不太大,两者一般以jar格式进行封装 只是体现了 OSGi中最简单的加载器委派关系。 在OSGi中,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成一种更为复杂的、运行时才能确定的网状结构。 互相死锁,也提供了一个以牺牲性能为代价的解决方案——用户可以启用 osgi.classloader.singleThreadLoads 参数来按单线程串行化的方式强制进行类加载动作。 9.2.3 字节码生成技术与动态代理的实现 JDK 里面的 Javac 命令就是字节码生成技术的 “ 老祖 宗” ,并且 Javac 也是一个由 Java语言写成的程序,它的代码存放在openjdk中的jdk.compiler\share\classes\com\sun\tools\javac 目录中 它的优势不在于省去了编写代理类那一点编码工作量,而是实 现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系 后,就可以很灵活地重用于不同的应用场景之中。 通过动态代理出现的, 接口中的每一个方法,以及从 java.lang.Object中继承来, 统一调用了 InvocationHandler 对象的 invoke()方法(代码中的 “this.h” 就是父类 Proxy 中保存的 InvocationHandler 实例变量)来实现这些方法的 内容 9.2.4 Backport工具:Java的时光机器 譬如 JDK 5 时加入的自动装箱、 泛型、动态注解、枚举、变长参数、遍历循环(foreach 循环);譬如 JDK 8 时加入的 Lambda 表达式、 Stream API、接口默认方法等。 以独立类库的方式便可实现。  Retrolambda 的 Backport 过程实质上就是生成一组匿名内部类来代替 Lambda ,里面会做一些优化措 施

9.3 实战:自己动手实现远程执行功能

排查问题的过程中,想查看内存中的一些 参数值,却苦于没有方法把这些值输出到界面或日志中。在应用程序中内置动态执行的功能。 9.3.1 目标 9.3.2 思路 要执行编译后的 Java 代码,让类加载器加载这个类生成一个 Class对象,我们想把程序往标准输出(System.out)和标准错误输出(System.err)中打印的信息收集起来。 9.3.3 实现 9.3.4 验证

9.4 本章小结

我们描绘了一个虚拟机应该是怎样 运行Class 文件的概念模型的,对于具体到某个虚拟机的实现,为了使实现简单清晰,或者为了更快的 运行速度

第四部分 程序编译和代码优化

· 第 10 章 前端编译与优化 · 第 11 章 后端编译与优化

第10章 前端编译与优化

从计算机程序出现第一天起,对效率的追逐就是程序员天生坚定的信仰,这一信仰犹如一场没有终点、永不停歇的f1方程式竞赛,程序员是车手,技术平台是赛道上飞驰的赛车

10.1 概述

· 前端编译器: JDK 的 Javac 、 Eclipse JDT 中的增量式编译器( ECJ ) [1] 。 · 即时编译器: HotSpot 虚拟机的 C1 、 C2 编译器, Graal 编译器。 · 提前编译器: JDK 的 Jaotc 、 GNU Compiler for the Java ( GCJ ) [2] 、 Excelsior JET [3] 如果把 “ 优化 ” 的定义放宽,把对开 发阶段的优化也计算进来的话,Javac 确实是做了许多针对 Java 语言编码过程的优化措施来降低程序员 的编码复杂度、提高编码效率。

10.2 Javac编译器

分析源码是一项了解,技术实现的内幕彻底手段,java编译器不像hotspot虚拟机那样使用c++语言实现,这为纯java程序员了解编译带来很大的便利 10.2.1 Javac的源码与调试 jdk6以前javac不属于javase 标准库的一部分,  严格定义了 Class文件格式的各种细节 ,可是对如何把Java源码编译为Class文件却描述得相当宽松。Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类, compile方法和compile2方法 10.2.2 解析与填充符号表 1. 词法、语法分析 词法分析是将源代码的字符流转变为标记token集合得过程,语法分析是根据标记序列构造抽象语法树得过程,在javac得源码中,语法分析过程由 com.sun.tools.javac.parser.parser 类实现,经过词法和语法分析生成语法树后,编译器就不会在对源码字符流进行操作了,后续操作都在语法树上了 2. 填充符号表 完成语法分析和词法分析之后,下一阶段是对符号表进行填充得过程, enterTrees() 方法,符号表是由一组符号地址和符号信息构成得数据结构,有点类似于哈希表中键值对得存储形式(实际形式上符号表不一定是哈希表得实现,可以是有序序号表、树状符号表、树状符号表等形式)。 10.2.3 注解处理器 jdk1.5之后,java语言提供了对注解的支持,注解在设计上原本与普通的java代码一样,都只会在程序运行期间发挥作用,我们可以提前至编译器对代码中特定注解进行处理, 10.2.4 语义分析与字节码生成 经过语义分析之后,编译器获得了程序代码的抽象语法树表示,无法保证语义是符合逻辑的 ,在语义分析阶段的检查结果 1. 标注检查 javac在编译过程中,语义分析过程可分为标注检查和数据及控制分析两个步骤, 标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否 能够匹配 插入式表达式( Infix Expression )的值已经在语法树上 标注出来了(ConstantExpressionValue : 3 )。由于编译期间进行了常量折叠 2. 数据及控制流分析 数据流分析和控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值,方法的每一条路径是否都有返回值,它可以检查正确返回值final修饰符等 3. 解语法糖 语法糖( Syntactic Sugar),也称糖衣语法  这种语法对语言的编译结果和功能并没有实际影响,但是却能更方便程序员使用该语言。通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。 Java 在现代编程语言之中已经属于 “ 低糖语言 ” 4. 字节码生成 字节码是javac编译过程的最后一个阶段,就会把填充了所有所需信息的符号表交com.sun.tools.javac.jvm.ClassWriter类手上

10.3 Java语法糖的味道

10.3.1 泛型 泛型的本质是参数化类型( Parameterized Type )或者参数化多态( Parametric Polymorphism )的 应用,将这些操作的数据类型指定为方法签名中一种特殊的参数, 1.Java 与 C# 的泛型 2. 泛型的历史背景 3. 类型擦除 元素访问、修改时自动插入一些强制类型转换和检查指令, 使用擦除法实现泛型直接导致了对原始类型( Primitive Types )数据的支持又成了新的麻 烦,遇到原生类型时把装箱、拆箱也自动做了得了。这个决定后面导致了无数构造包 装类和装箱、拆箱的开销,成为 Java 泛型慢的重要原因 运行期无法取到泛型类型信息,会让一些代码变得相当啰嗦,譬如代码清单 10-2 中罗列的 几种 Java 不支持的泛型用法,都是由于运行期 Java 虚拟机无法取得泛型类型而导致的。像代码清单 10-8 这样,我们去写一个泛型版本的从 List 到数组的转换方法,由于不能从 List 中取得参数化类型 T ,所以 不得不从一个额外参数中再传入一个数组的组件类型进去,实属无奈。 4. 值类型与未来的泛型 值类型可以与引用类型一样,具有构造函数、方法或是属性字段 10.3.2 自动装箱、拆箱与遍历循环 自动装箱、自动拆箱与遍历循环( for-each 循环)这些语法糖, 10.3.3 条件编译 很多程序语言都提供了条件编译的途径,如c c++中使用了预处理器指示符来完成条件编译,

10.4 实战:插入式注解处理器

Java 的编译优化部分在本书中并没有像前面两部分那样设置独立的、整章篇幅的实战,因为我们 开发程序,考虑的主要还是程序会如何运行,较少会涉及针对程序编译的特殊需求。 10.4.1 实战目标 通过阅读javac编译器的源码,我们知道前端编译器把java程序编译为字节码的时候,会对java程序源码做各方面的检查,名称所表示的那样,是指混合使用大小写字母来分割构成变量或函数的名字 10.4.2 代码实现 AbstractProcessor 必须实现抽象方法 process() 它是javac编译器在执行注解处理器代码时调用过程,annotations”中获取到此注解处理器所要处理的注解集合 还有两个经常配合着使用的注解,分别是:@SupportedAnnotationTypes @SupportedSourceVersion 每一个注解器都是单例的 NameCheckProcessor 能处理基于 JDK 6 的源码,它不限于特定的注 解,对任何代码都“ 感兴趣 ” , Visitor 模式 完成对语法树的遍历 10.4.3 运行与测试 Javac 命令的 “-processor” 参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号进行隔开。 10.4.4 其他应用案例

10.5 本章小结

我们从 Javac 编译器源码实现的层次上学习了 Java 源代码编译为字节码的过程 之所以把 Javac 这类将 Java 代码转变为字节码的编译器称作 “ 前 端编译器” ,是因为它只完成了从程序到抽象语法树或中间字节码的生成

第11章 后端编译与优化

从计算机程序出现的第一天起,对效率的追逐就是程序员天生的坚定信仰,这个过程犹如一场没 有终点、永不停歇的 F1 方程式竞赛,程序员是车手,技术平台则是在赛道上飞驰的赛车。

11.1 概述

如果我们把字节码看作是程序语言的一种中间表示形式的话,即时编译才是占绝对的主流的编译形式,编译器无论在何时 在任何状态下把class文件转换成本地基础设施相对应的二进制机器码,它都可以是编译后端

11.2 即时编译器

目前通用的商用java虚拟机 最初都是通过解释器执行的, 11.2.1 解释器与编译器 两个编译器各有优缺点,当程序需要迅速启动时,解释器可以首先发挥作用,省去编译的时间,立即执行,当程序启动后,随着时间的推移编译器椎间发挥作用,编译成本地代码,减少解释器中间的损耗,提高执行效率, hotspot虚拟机中内置两到三个即时编译器,其中有两个编译器存在已久了,分别称为客户端编译器和服务端编译器,或者简称为C1编译器和 C2编译器 HotSpot 虚拟机通常是采用解释器与其中 一个编译器直接搭配的方式工作,程序使用哪个编译器,只取决于虚拟机运行的模式 为了在程序启动响应速度与运行效率之间达到平衡,hotspot虚拟机在编译子系统中加入了分层编译的功能,分层编译的概念 因为编译执行中要优化代码需要解释执行进行监控,则会导致效率降低 分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次 实施分层编译后,解释器 客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多次编译,用客户端编译器获取更高的编译速度, 11.2.2 编译对象与触发条件 概述中提到了在运行过程中会被即时编译器编译的目标是 “ 热点代码 ” ,热点代码包括被多次调用的方法, 包括多次执行的方法,被多次执行的循环体, 编译的目标对象都是整个方法体,而不会是单独的循环体。尽管编译动作整个方法作为编译对象,编译动作是由循环体触发的,也需要整个方法作为编译对象,只是执行入口编译时会传入执行入口点字节码序号 基于采样的热点探测 基于采样的热点探测的好处是实现简单高效 基于计数器的热点探测 统计方法的执行次数,如果执行次数超过一定的阈值就认为 它是 “ 热点方法 ” 方法调用计数器(Invocation Counter)和回边计数器,这个计数器就是用于统计方法被调用的次数,的代码编译请求 即一段时间之内方法被调用的次数。 回边计数器,它的作用是统计一个方法中循环体代码执行 的次数,建立回边计数器统计的目的是为了触发栈上替换编译 回边计数器没有计数热度衰减的过程 , 这个计数器统计的就是该方法循 环执行的绝对次数。当计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态 11.2.3 编译过程 方法调用产生标准编译请求,还是栈上替换编译请求,虚拟机按解释器方式执行继续执行代码,只有编译好了才会按编译方式执行代码, 三段式编译器,关注点在于局部性的优化,放弃许多耗较长的全局优化手段。 第一个阶段,平台独立的前端字节码构成一种高级中间代码表示,会做一部分基础优化 如方法内联 常量传播 hir 第二个阶段 一个平台相关的后端从hir 中产生低级中间代码表示 最后的阶段是在平台相关的后端使用线性扫描算法 图 11-5 Client Compiler 架构 服务端编译器则是专门面向服务端的典型应用场景,并为服务端的性能配置针对性调整过的编译器,也是个能容忍很高优化复杂度的高级编译器 11.2.4 实战:查看及分析即时编译结果

11.3 提前编译器

提前编译在java中早已经存在了,在android中出现的 11.3.1 提前编译的优劣得失 这是传统的提前编译应用形式 即时编译要占用程序运行时间和运算资源。某个点上某个变量的值是否一定为常量, 性能分析制导优化 ,激进预测性优化, , Java 语言天生就是动态链接的 11.3.2 实战:Jaotc的提前编译 Class 文件和模块进行提前编译的工具 Jaotc,这些运行时信息与编译的结果是直接相关的,

11.4 编译器优化技术

对即时编译、提前编译的讲解,编译器的目标虽然是做程序代码翻译为本地机器码做工作, 11.4.1 优化技术概览 HotSpot 虚拟机设计团队列出了一个相对比较全面的、即时编译器中采用 的优化技术列表 去除方法调用的成本 是为其他优化建立良好的基础,消除多余的代码 行无用代码消除( Dead Code Elimination ),无用代码可能是永远不会被执行的代码 方法内联。逃逸分析。 11.4.2 方法内联 方法内联是将目标方法的代码原封不动地复制到发起调用的方法之中,避免真实方法调用而已,但实际过程中远没有这么简单 使用内联缓存来建立, 如果以后进 来的每次调用的方法接收者版本都是一样的,那么这时它就是一种单态内联缓存(Monomorphic Inline Cache) 11.4.3 逃逸分析 逃逸分析( Escape Analysis )是目前 Java 虚拟机中比较前沿的优化技术,它与类型继承关系分析一 样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。 逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部 方法所引用,对象所占用的内存 空间就可以随栈帧出栈而销毁 标量替换:若一个数据已经无法再分解成更小的数据来表示了,java虚拟机中的原始数据类型, 那么这些数据 就可以被称为标量。逃逸分析 11.4.4 公共子表达式消除 公共子表达式消除是一项非常经典的、普遍应用于各种编译器的优化技术,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。 11.4.5 数组边界检查消除 数组边界检查消除( Array Bounds Checking Elimination )是即时编译器中的一项语言相关的经典优 化技术。

11.5 实战:深入理解Graal编译器

11.5.1 历史背景 Graal 编译器在 JDK 9 时以 Jaotc 提前编译工具的形式首次加入到官方的 JDK中,成为 HotSpot 分层编译中最顶层的即时编译器 · 响应 HotSpot 的编译请求,并将该请求分发给 Java 实现的即时编译器。 · 允许编译器访问 HotSpot 中与即时编译相关的数据结构,包括类、字段、方法及其性能监控数据 等,并提供了一组这些数据结构在 Java 语言层面的抽象表示。 11.5.2 构建编译调试环境 11.5.3 JVMCI编译器接口 如果让您来设计 JVMCI编译器接口,既然JVMCI面向的是Java语言的编译器接口 workload()方法肯定很快就会被虚拟机发现是热点代码因而进行编译。特殊版的 JDK 8 11.5.4 代码中间表示 Graal 编译器在设计之初就刻意采用了与 HotSpot 服务端编译器一致的中间表达式,也即是sea-of-nodes 的中间表示,或者与其等价的被称为理想图, 11.5.5 代码优化与生成 Graal 理想图的中间表示了,那对应到代码上, Graal 编译器是如 何从字节码生成理想图? 该过程被封装在 BytecodeParser 类中,这个解析器我们可以 按照字节码解释器的思路去理解它。

11.6 本章小结

第五部分 高效并发

· 第 12 章 Java 内存模型与线程 · 第 13 章 线程安全与锁优化

第12章 Java内存模型与线程

人类去压榨计算机运算能力的最有力的武器,amdahl定律 通过系统中并行化与串行化的比重来描述多处理器系统获得的运算加速能力,摩尔定律则是描述处理器晶体管数量与运行效率之间的发展关系。这两个定律的更替,代表着近年来从对硬件频率的追求,转变到对多核心并行处理的发展过程

12.1 概述

多任务处理在现代计算机操作系统中几乎已是一项必备的功能了。 存储和通信子系统的速度差距太大,大量的时间花费在磁盘i/0 网络通信或者数据库访问上了。如果不希望处理器大部分时间里都处于等待其他资源的空闲状态,就会造成很大的性能浪费

12.2 硬件的效率与一致性

物理上遇到的并发问题,与虚拟机中的情况有很多相似之处,i/o耗时操作是很难消除的,我们只能降低,由于计算机的存储设备与处理器的运算速度有着几个数量级的差距, 其中引入了一个叫缓存一致性的问题,MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。 而 Java 虚拟机也有自己的内存模型,并且与这里介绍的内存访问操作及硬 件的缓存访问操作具有高度的可类比性。 图 12-1 处理器、高速缓存、主内存间的交互关系 增加高速缓存之外,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入 代码进行乱序执行( Out-Of-Order Execution)优化,但并不保证程序中各个语句计算的先后顺序与输入代码的顺序 ,Java虚拟机的即时编译器中也有指令重排序(Instruction Reorder)优化。

12.3 Java内存模型

《 Java 虚拟机规范》 [1] 中曾试图定义一种 “Java 内存模型 ” [2] ( Java Memory Model , JMM )来屏 蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效 果。 主流程序语言(如 C 和 C++ 等)直接使用物理硬件和操作系统的内存模型。不同平台上的java内存模型实现不一样,就有可能导致一台平台上是可以的,但是在另外一个平台上访问则经常出现错误 所以在某些场景下必须针对不同的平台来编写程序。 我想为什么叫这个名称,还是基于处理器访问内存,导致数据不正确,因此叫的这个名字。在jdk5之后才有了完善的内存模型 12.3.1 主内存与工作内存 java内存模型的主要目的是定义程序中各种变量的访问规则,即关注虚拟机中把变量存储到内存和从内存中取出变量的底层细节。而java内存模型并没有限制执行引擎使用处理器的特定寄存器或者缓存来进行交互,也没有限制即时编译器是否要进行调整代码执行顺序这类优化措施。 Java 内存模型规定了所有的变量都存储在主内存( Main Memory )中(此处的主内存与介绍物理 硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分)。 图 12-2 线程、主内存、工作内存三者的交互关系(请与图 12-1 对比) 这里所讲的主内存、工作内存与第 2 章所讲的 Java 内存区域中的 Java 堆、栈、方法区等并不是同一 个层次的对内存的划分 主内存直接对应于物理硬件的内存 12.3.2 内存间交互操作 关于主内存与工作内存之间交互协议,有load store  read write  ·lock (锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 ·unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。 ·read (读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的 load 动作使用。 ·load (载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的 变量副本中。 ·use (使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 ·assign (赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 ·store (存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的 write 操作使用。 ·write (写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的 变量中。 不允许 read 和 load 、 store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内 存不接受,或者工作内存发起回写了但主内存不接受的情况出现。 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存 中 先行发生原则,用来确定一个操作在并发环境下 是否安全的。 12.3.3 对于volatile型变量的特殊规则 关键 字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制,但是它并不容易被正确,和完整的理解 Java 内存模型为 volatile 专门定义了一些特殊的访问规则 ,当变量定义为volation之后,具备保证变量对所有线程可见性。 可见性也就是马上同步 忽略缓存, 基于 volatile 变量的运算在并发下是线程安全的 ” 这样 的结论。volatile 变量在各个线程的工作内存中是不存在一致性问题的 每次使用之前都要刷新访问,Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的, 只能保证变量在其他线程可见  当 getstatic指令把 race的值取到操作栈顶时, volatile 关键字保证了 race 的值在此时是正确的,但是在执行 iconst_1 、 iadd 这 些指令的时候,其他线程可能已经把race 的值改变了 putstatic 指令执行后就可能把较小的 race 值同步回主内存之中。 也并不意味执行这条指令就是一个原子操作或者能够确保只有单一的线程修改变量的值 变量不需要与其他的状态变量共同参与不变约束。 使用volatile 第二个语义是禁止指令重排优化, 这个操作的作用相当于一个内存屏障 ( Memory Barrier 或 Memory Fence ,指重排序时不能把后面的指令重排序到内存屏障之前的位置 指令重排序是指处理器采用了允许多条指令不按规则的顺序分开发送各个响应的电路单元进行处理 volatile 的同步机制的性能确实要优于锁 (使用synchronized 关键字或 java.util.concurrent 包里面的锁), volatile 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能 会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。 12.3.4 针对long和double型变量的特殊规则 Java 内存模型要求 lock 、 unlock 、 read 、 load 、 assign 、 use 、 store 、 write这八种操作都具有原子性,在64位的数据类型允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行 long 和double  半个变量的数值 12.3.5 原子性、可见性与有序性 java内存模型是依据 可见性  原子性 有序性建立起来的  1.原子性 atomicity 直接保证的原子性变量操作包括 read 、 load 、 assign 、 use 、 store 和 write 这六个 2. 可见性( Visibility ) 可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。java内存模型是通过在变量修改后将新值同步回主内存 java还有两个关键字能实现可见性,他们是sychronized和final  3. 有序性( Ordering ) 在线程内部观察,所有操作都是有序的,而如果在另一个线程观察,所有操作则是无序的 12.3.6 先行发生原则 如果java内存模型中所有的操作有序性都按照volatile和synchroized来保证,很多操作都会非常啰嗦, 先行发生是 Java 内存模型中定义的两项操作之间的偏 序关系 · 程序次序规则 在一个线程内,按照控制流顺序,书写在前面的操作先行 发生于书写在后面的操作。 · 管程锁定规则 · volatile 变量规则 · 线程启动规则 · 线程终止规则 · 线程中断规则 · 对象终结规则 传递性

12.4 Java与线程

并发不一定要依赖多线程(如 PHP 中很常见的多进程并发),但是在 Java 里面谈论并发,基本上 都与线程脱不开关系。既然本书探讨的是 Java 虚拟机的特性,那讲到 Java 线程,我们就从 Java 线程在虚 拟机中的实现开始讲起。 12.4.1 线程的实现 线程比进程更轻量级的调度执行单位,线程的引入,把一个进程的资源分配和执行调度分开,各个线程可以共享进程资源,又可以独立调度, 实现线程主要的三种方式:使用内核线程实现,使用用户线程实现 使用用户线程加轻量级进程混合实现 1. 内核线程实现 使用内核线程实现的方式也被称为 1:1实现。轻量级进程就是我们通常意义上所讲的线程,即使其中某个轻量级进程被阻塞,也不会影响整个进程继续工作。系统支持轻量级进程的数量是有限的。 主要还是由于要占用内核资源 2. 用户线程实现 使用用户线程实现的方式被称为 1 : N实现。用户线程指的是完全建立在用户空间的线程库上,多线程的情况下如何将线程映射到其他处理器上这类问题解决起来异常困难 3. 混合实现 为 N : M实现,在这些操作系 统上的应用也相对更容易应用M:N的线程模型。 4.Java 线程的实现 JDK 1.3 起 采用1 : 1的线程模型。可以设置线程优先级给操作系统提 供调度建议 该给线程分配多少处理 器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统 全权决定的。 12.4.2 Java线程调度 线程调度是指系统为线程分配处理器的使用权的过程,分别是协同式 (Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度。 协同式调度的多线程系统,线程的执行时间由线程本身来控制, 线程的执行时间是系统可控 的,也不会有一个线程导致整个进程甚至整个系统阻塞的问题。 Java 使用的线程调度方式就是抢占式 调度。 用线程优先级去调节手段,不仅仅体现在某些操作系统上, 优先级推进器 ” 的功能 12.4.3 状态转换 Java 语言定义了 6 种线程状态  一个线程有且只有一个状态,并通过不同的方法进行线程间切换 新建( New)运行( Runnable) 无限期等待(Waiting)限期等待( Timed Waiting) 阻塞( Blocked)“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到 一个排它锁 结束( Terminated ):已终止线程的线程状态,线程已经结束执行

12.5 Java与协程

Java语言抽象出来隐藏了各种操作系统线程差异性的统一线程接口,无数多线程的应用与框架, 以 “ 一对一服务 ” 的方式处理由浏 览器发来的信息。 12.5.1 内核线程的局限 但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高昂,系统 能容纳的线程数量也很有限。 12.5.2 协程的复苏 了各种线程实现方式的优缺点 内核线程的调度成本主要来自于用户态与核心态之间的状态转换, 协程的主要优势是轻量,无论是有栈协程还是无栈协程,都要比传统内核线程要轻量得多。 12.5.3 Java的解决方案 对于有栈协程,有一种特例实现名为纤程( Fiber ) Loom 项目背后的意图是重新提供对用户线程的支持,但与过去的绿色线程不同,这些新功能不是 为了取代当前基于操作系统的线程实现,而是会有两个并发编程模型在 Java 虚拟机中并存,可以在程 序中同时使用

12.6 本章小结

下一章中,我们的主 要关注点将是虚拟机如何实现“ 高效 ” ,以及虚拟机对我们编写的并发代码提供了什么样的优化手段。

第13章 线程安全与锁优化

并发处理的广泛应用是 Amdahl 定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人 类压榨计算机运算能力的最有力武器。

13.1 概述

这种思维方式直接站在计算 机的角度去抽象问题和解决问题,被称为面向过程的编程思想。 面向对象的编程思想则站 在现实世界的角度去抽象和解决问题,它把数据和行为都看作对象的一部分

13.2 线程安全

比较恰当 当多个线程同时访问一个对象时,如果不考虑这些线程在运行环境下调度和交替执行,也不需要进行额外同步,或者在调用方进行其他任何协调操作,调用这个对象的行为都可以获得正确结果,那就称对象线程安全 13.2.1 Java语言中的线程安全 将 Java 语言中各种操作共享的数 据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。 1. 不可变 只要 一个不可变的对象被正确地构建出来(即没有发生this 引用逃逸的情况),那其外部的可见状态永远都 不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“ 不可变 ” 带来的安全性是最直接、 最纯粹的。 2. 绝对线程安全 3. 相对线程安全 4. 线程兼容 5. 线程对立 线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。 13.2.2 线程安全的实现方法 1.互斥同步 互斥同步( Mutual Exclusion & Synchronization )是一种最常见也是最主要的并发正确性保障手 段。 同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些, 当使用信号量的时候)线程使用。 被 synchronized 修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块 也不会出现自己把自己锁死的情况。 · 被 synchronized 修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他 线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制 正在等待锁的线程中断等待或超时退出。 有经验的程序员都只会在确实必要的情况下才使用这种 操作。synchronized 是 Java 语言中一个重量级的操作, ReentrantLock 与 synchronized 相比增加了一些高级功能, · 等待可中断: 是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改 为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。 · 公平锁: 是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁; · 锁绑定多个条件: 是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象 笔者仍然推荐在 synchronized 与 ReentrantLock 都可满足需要时优先使用 synchronized : Lock 应该确保在 finally 块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不 会释放持有的锁。 2. 非阻塞同步 互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞 同步( Blocking Synchronization )。 Java 里最终暴露出来的是 CAS 操作 CAS 指令需要有三个操作数,分别是内存位置(在 Java 中可以简单地理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和准备设置的新值(用 B 表示)。 Java 类库中才开始使用 CAS 操作,该操作由 sun.misc.Unsafe 类里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包装提供。 过 CAS操作避免阻塞同步,效率将会提高许多。 3. 无同步方案 因此会有一些代码天生就是线程安全的 可重入代码( Reentrant Code ):这种代码又称纯代码( Pure Code ),是指可以在代码执行的任何 时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不 会出现任何错误,也不会对结果有所影响。 线程本地存储( Thread Local Storage ):如果一段代码中所需要的数据必须与其他代码共享,那就 看看这些共享数据的代码是否能保证在同一个线程中执行。 Java 语言中,,如果一个变量要被多线程访问,可以使用volatile 关键字将它声明为 “ 易变的 ”

13.3 锁优化

适应性自旋( Adaptive Spinning )、锁消除( Lock Elimination)、锁膨胀( Lock Coarsening )、轻量级锁( Lightweight Locking )、偏向锁( Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题 13.3.1 自旋锁与自适应自旋 提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢 复线程的操作都需要转入内核态中完成不过无论是默认值还是用户指定的自旋次数 13.3.2 锁消除 锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享 数据竞争的锁进行消除。 13.3.3 锁粗化 13.3.4 轻量级锁 对于绝大部分的锁,在整个同步周期内都是不存在竞争 的” 这一经验法则 13.3.5 偏向锁 偏向锁也是 JDK 6 中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。

13.4 本章小结

本章介绍了线程安全所涉及的概念和分类、同步实现的方式及虚拟机的底层运作原理,并且介绍 了虚拟机为实现高效并发所做的一系列锁优化措施。

总结

看完整本书,自己对整个java虚拟机由了个整体概念,学习到很多之前不知道的东西,不了解为什么在开发过程java语言的特性,以及jvm内存编译的特性,在实际开发遇到很多问题时,从哪方面考虑问题,这是我对这本书学习到最大特点,也许读一遍并不能特别理解这本书,我建议包括里面实例,编译jdk等等,都需要自己做一遍才有更深的领悟

这篇关于深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)读书笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!