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

image.png

第1章 走近Java1.1 概述

1.2 Java技术体系

  • Java程序设计语言口各种硬件平台,上的Java虚拟机
  • Class文件格式
  • Java API类库
  • 来自商业机构和开源社区的第三方Java类库

image.png

1.3 Java发展史

image.png

1.4 Java虚拟机家族

1.4.1 虚拟机始祖:Sun Classic/Exact VM

Sun Classic 1996年1月23日,Sun发布JDK 1.0, Java语 言首次拥有了商用的正式运行环境,这个JDK中所带的虛拟机就是ClassicVM。这款虚拟机只能使用纯解释器方式来执行Java代码,如果要使用即时编译器那
就必须进行外挂,但是假如外挂了即时编译器的话,即时编译器就会完全接管虚拟机的执行系统,解
释器便不能再工作了。

Exact VM因它使用准确式内存管理(Exact Memory Management,也可以叫Non-Con-servative/Accurate Memory Management)而得名。准确式内存管理是指虚拟机可以知道内存中某个位置的数据具体是什么类型。譬如内存中有一个32bit的整数123456,虚拟机将有能力分辨出它到底是一个指向了123456的内存地址的引用类型还是-一个数值为123456的整数,准确分辨出哪些内存是引用类型,这也是在垃圾收集时准确判断堆上的数据是否还可能被使用的前提。由于使用了准确式内存管理,Exact VM可以抛弃掉以前Classic VM基于句柄(Handle) 的对象查找方式(原因是垃圾收集后对象将可能会被移动位置,如果地址为123456的对 象移动到654321,在没有明确信息表明内存中哪些数据是引用类型的前提下,那虚拟机肯定是不敢把内存中所有为123456的值改成654321的,所以要使用句柄来保持引用值的稳定),这样每次定位对象都少了一次间接查找的开销,显著提升执行性能。

1.4.2 武林盟主:HotSpot VM

HotSpot既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势,如它名称中的HotSpot指的就是它的热点代码探测技术(这里的描写带有“历史由胜利者书写”的味道,其实HotSpot与Exact虚拟机基本上是同时期的独立产品,HotSpot 出现得还稍早一些,一开始HotSpot就是基于准确式内存管理的,而ExactVM之中也有与HotSpot几乎一样的热点探测技术,为了ExactVM和HotSpotVM哪个该成为Sun主要支持的虚拟机,在Sun公司内部还争吵过一场,HotSpot击败Exact并不能算技术上的胜利),HotSpot虚拟机的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知即时编译器以方法为单位进行编译。如果–个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准即时编译和栈上替换编译(On-StackReplacement,OSR) 行为。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与
最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更复杂的代码优化技术,输出质量更高的本地代码。

1.4.3 小家碧玉:Mobile/Embedded VM

面向嵌入式和移动端,小巧移除服务端编译器,只保留Serial/Serial Old垃圾收集器.

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

JRockit虚拟机曾经号称是“世界上速度最快的Java虛拟机”(广告词,IBMJ9虛拟机也这样宣传过,总体上三大虚拟机的性能是交替上升的),它是BEA在2002年从Appeal Virtual M achines公司收购获得的Java虛拟机。BEA将其发展为一款专门为服 务器硬件和服务端应用场景高度优化的虚拟机,由于专注于服务端应用,它可以不太关注于程序启动速度,因此JRockit内 部不包含解释器实现,全部代码都靠即时编译器编译后执行。除此之外,JRockit的垃圾收集器和JavaMisionControl故障处理套件等部分的实现,在当时众多的Java虚拟机中也处于领先水平。JRockit 随着BEA被Oracle收购,现已不再继续发展,永远停留在R28版本,这是JDK 6版JRockit的代号。

IBM J9虛拟机并不是IBM公司唯一的Java虚拟机,不过目前IBM主力发展无疑就是J9。J9这个名字最初只是内部开发代号而已,开始选定的正式名称是“IBM Technology for Java Virtual M achine”,简称IT4J,但这个名字太拗口,接受程度远不如J9。J9 虚拟机最初是由IBM Ottawa实验室的一个SmallTalk虚拟机项目扩展而来,当时这个虚拟机有- -个Bug是因为8KB常量值定义错误引起,工程师们花了很长时间终于发现并解决了这个错误,此后这个版本的虛拟机就被称为K8,后来由其扩展而来、支持Java语言的虚拟机就被命名为J9。与BEAJRockit只专注于服务端应用不同,IBMJ9虛拟机的市场定位与HotSpot比较接近,它是-款在设计上全面考虑服务端、桌面应用,再到嵌入式的多用途虛拟机,开发J9的目的是作为IBM公司各种Java产品的执行平台,在和IBM产品(如IBM WebSp here等)搭配以及在IBM AIX和z/OS这些平台上部署Java应用。

IBM J9直至今天仍旧非常活跃,IBM J9虚拟机的职责分离与模块化做得比HotSpot更优秀,由J9虚拟机中抽象封装出来的核心组件库(包括垃圾收集器、即时编译器、诊断监控子系统等)就单独构成了IBM OMR项目,可以在其他语言平台如Ruby、Python中快速组装成相应的功能。从2016年起,IBM逐步将OM R项目和J9虚拟机进行开源,完全开源后便将它们捐献给了Eclipse基金会管理,并重新命名为Eclipse OM R和OpenJ9l21。如果为了学习虚拟机技术而去阅读源码,更加模块化的OpenJ9代码其实是比HotSpot更好的选择。如果为了使用Java虚拟机时多一种选择, 那可以通过AdoptOpenJDK来获得采用OpenJ9搭配上OpenJDK其他类库组成的完整JDK。

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

Liquid VM也被称为JRockit VE ( Virtual Edition,VE) ,它是BEA公司开发的可以直接运行在自家Hy pervisor系统上的JRockit虚拟机的虚拟化版本,Liquid VM不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等。由虚拟机越过通用操作系统直接控制硬件可以获得很多好处,如在线程调度时,不需要再进行内核态/用户态的切换,这样可以最大限度地发挥硬件的能力,提升Java程序的执行 性能。随着JRockit虚拟机终止开发,Liquid VM项目也已经停止了。

Azul VM是Azul Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件Vega系统上的Java虚拟机,每个Azul VM实例都可以管理至少数十个CPU和数百GB的内存的硬件资源,并提供在巨大内存范围内停顿时间可控的垃圾收集器(即业内赫赫有名的PGC和C4收集器),为专有硬件优化的线程调度等优秀特性。2010年起,Azul公 司的重心逐渐开始从硬件转向软件,发布了自己的Zing虚拟机,可以在通用x86平台上提供接近于Vega系统的性能和一- 致的功能特性。

Zing虚拟机是一个从HotSpot某旧版代码分支基础上独立出来重新开发的高性能Java虚拟机,它可以运行在通用的Linux/x86-64平台上。Azul公 司为它编写了新的垃圾收集器,也修改了HotSpot内的许多实现细节,在要求低延迟、快速预热等场景中,Zing VM都要比HotSpot表现得更好。Zing的PGC、C4收集器可以轻易支持TB级别的Java堆内存,而且保证暂停时间仍然可以维持在不超过10亳秒的范围里,HotSpot要 一直到JDK 11和JDK 12的ZGC及Shenandoah收集器才达到了相同的目标,而且目前效果仍然远不如C4。Zing的ReadyNow!功能可以利用之前运行时收集到的性能监控数据,引导虚拟机在启动后快速达到稳定的高性能水平,减少启动后从解释执行到即时编译的等待时间。Zing自带的Z Vision/ZVRobot功能可以方便用户监控Java虛拟机的运行状态,从找出代码热点到对象分配监控、锁竞争监控等。Zing能让普通用户无须了解垃圾收集等底层调优,就可以使得Java应用享有低延迟、快速预热、易于监控的功能,这是Zing的核心价值和卖点,很多Java应用都可以通过长期努力在应用、框架层面优化来提升性能,但使用Zing的话就可以把精力更多集中在业务方面。

1.4.6 挑战者:Apache Harmony/Google Android Dalvik

Apache Harmony是一个Apache软 件基金会旗下以Apache License协议开源的实际兼容于JDK 5和JDK 6的Java程序运行平台,它含有自己的虚拟机和Java类库API,用户可以在上面运行Eclipse、Tomcat、M aven等常用的Java程序。但是,它并没有通过TCK认证,所以我们不得不用一长串冗长拗:口的语言来介绍它,而不能用一句“Apache的JDK ”或者“Apache的Java虚拟机”来直接代指。

如果一个公司要宣称自己的运行平台“兼容于Java技术体系”,那该运行平台就必须要通过TCK (Technology Compatility Kit)的兼容性测试,Apache基 金会曾要求当时的Sun公司提供TCK的使用授权,但是一-直遭到各种理由的拖延和搪塞,直到Oracle收购了Sun公司之后,双方关系越闹越.僵,最终导致Apache基金会愤然退出JCP组织,这是Java社区有史以来最严重的分裂事件之一-。.

当Sun公司把自家的JDK开源形成OpenJDK项目之后,Apache Harmony开源的优势被极大地抵消,以至于连Harmony项目的最大参与者IBM公司也宣布辞去Harmony项目管理主席的职位,转而参与OpenJDK的开发。虽然Harmony 没有真正地被大规模商业运用过,但是它的许多代码(主要是Java 类库部分的代码)被吸纳进IBM的JDK 7实现以及Google Android SDK之中,尤其是对Android的发展起了很大推动作用。

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

在Java语言诞生的初期( 1996年~ 1998年,以JDK1.2发布之前为分界),它的主要应用之- -是在浏览器中运行Java App lets程序,微软为了在Internet Exp lorer 3浏览器中支持Java Applets应用而开发了自己的Java虚拟机,虽然这款虚拟机只有Windows平台的版本,“- 次编译,到处运行”根本无从谈起,但却是当时Windows系统下性能最好的Java虚拟机,它在1997年和1998年连续获得了《PC Magazine》杂志的“编辑选择奖”。但是好景不长,在1997年10月, Sun公司正式以侵犯商标、不正当竞争等罪名控告微软,在随后对微软公司的垄断调查之中,这款虚拟机也曾作为证据之一被呈送法庭。官司的结果是微软向Sun公司(最终微软因垄断赔偿给Sun公司的总金额高达10亿美元)赔偿2000万美金,承诺终止其Java虚拟机的发展,并逐步在产品中移除Java虚拟机相关功能。而最令人感到讽刺的是,到后来在WindowsXPSP3中Java虛拟机被完全抹去的时候,Sun公司却又到处登报希望微软不要这样做]。Windows XP高级产品经理Jim Cullinan称:“我们花费了三年的时间和Sun公司打官司,当时他们试图阻止我们在Windows中支持Java,现在我们这样做了,可他们又在抱怨,这太具有讽刺意味了。”

1.4.8 百家争鸣

KVM

KVM中的K是”Kilobyte”的意思,它强调简单、轻量、高度可移植,但是运行速度比较慢。在Android、iOS等智能手机操作系统出现前曾经在手机平台上得到非常广泛应用。

Java Card VM

JCVM是Java虚拟机很小的一一个 子集,裁减了许多模块但通常支持绝大多数的常用加密算法。
JCVM必须精简到能放入智能卡、SIM卡、银行信用卡、借记卡内,负责对Java Applet程序进行解释执

Squawk VM

Squawk VM是由Sun开发,运行于Sun SPOT ( Sun Small Programmable Object Tech-nology,一种手
持的Wi-Fi设备),也曾经运用于Java Card。这是- -个Java代码比重很高的嵌入式虚拟机实现,其中诸
如类加载器、字节码验证器、垃圾收集器、解释器、编译器和线程调度都是用Java语言完成的,仅仅
靠C语言来编写设备I/O和必要的本地代码。

JavaInJava

JavaInJava是Sun公司在1997年~ 1998年间所研发的一个实验室性质的虚拟机,从名字就可以看
出,它试图以ava语言来实现Java语言本身的运行环境,既所谓的“元循环”(Meta-Circular, 是指使用
语言自身来实现其运行环境)虚拟机。它必须运行在另外一个宿主虚拟机之上,内部没有即时编译
器,代码只能以解释模式执行。在上世纪末主流原生的Java虛拟机都未能很好解决性能问题的时代,
开发这种项目,其执行速度大家可想而知,不过通过元循环证明-一门语言可以自举,是具有它的研究
价值的。

Maxine VM

Maxine VM和上面的JavaInJava非常相似,它也是一个几乎全部以Java代码实现(只有用于启动
Java虛拟机的加载器使用C语言编写)的元循环Java虚拟机。这个项目于2005年开始,到现在仍然在发
展之中,比起JavaInJava,MaxineVM的执行效率就显得靠谱得多,它有先进的即时编译器和垃圾收集
器,可在宿主模式或独立模式下执行,其执行效率已经接近HotSpot虛拟机Client模式的水平。后来有
了从C1X编译器演进而来的Graal编译器的支持,就更加如虎添翼,执行效率有了进一步飞跃。Graal编
译器现在已经是HotSpot的默认组件,是未来代替HotSpot中服务端编译器的希望。

Jikes RVM

Jikes RVM是IBM开发的专门用来研究Java虚拟机实现技术的项目。曾用名为Jalapeno。与JavaInJava和M axine-样,它也是一个元循环虚拟机。

IKVM.NET

这是一个基于微软.NET框架实现的Java虛拟机,并借助Mono获得–定的跨平台能力。IKVM.NET的目标第一眼看起来的确很奇怪,可能在某些特殊情况下,在.NET上使用某些流行的Java库也许真的不算是伪需求? IKVM .NET可以将Class文件编译成.NET Assembly,在任意的CLI上运行。

1.5 展望Java技术的未来

1.5.1 无语言倾向

如果Java有拟人化的思维,它应该从来没有惧怕过被哪-门语言所取代,Java“天下第一 ”的底气不在于语法多么先进好用,而是来自它庞大的用户群和极其成熟的软件生态,这在朝夕之间难以撼动。不过,既然有那么多新、1旧编程语言的兴起躁动,说明必然有其需求动力所在,譬如互联网之于JavaScript、人工智能之于Python,微服务风潮之于Golang等。大家都清楚不太可能有哪门语言能在每一个领域都尽占优势,Java已是距离这个目标最接近的选项,但若“天下第一”还要百尺竿头更进 一步的话,似乎就只能忘掉Java语言本身,踏入无招胜有招的境界。

2018年4月,Oracle Labs新公开了一项黑科技: Graal VM,如图1-4所示, 从它的口号”RunProgams Faster Anywhere”就能感觉到一颗蓬勃的野心,这句话显然是与1995年Java刚涎生时的“Write Once,Run Anywhere”在遥相呼应。Graal VM被官方称为“Universal VM”和”Poly glot VM”,这是一个在HotSpot虛拟机基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用,这里“任何语言”包括了Java、Scala、Groovy、Kotlin等基于Java虚拟机之上的语言,还包括了C、C++、 Rust等基于LLVM的语言,同时支持其他像JavaScript、Ruby、 Py thon和R语言等。Graal VM可以无额外开销地混合使用这些编程语言,支持不同语言中混用对方的接口和对象,也能够支持这些语言使用已经编写好的本地库文件。

image.png

1.5.2 新一代即时编译器

HotSpot虚拟机中含有两个即时编译器,分别是编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)以及编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2),通常它们会在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统.

自JDK 10起,HotSpot中又 加入了一个全新的即时编译器: Graal编译器, 看名字就可以联想到它是来自于前一节提到的Graal VM。Graal编 译器是以C2编译器替代者的身份登场的。C2的历史已经非常长了,可以追溯到CliffClick大神读博士期间的作品,这个由C++写成的编译器尽管目前依然效果拔群,但已经复杂到连CliffClick本人都不愿意继续维护的程度。而Graal编译器本身就是由Java语言写成,实现时又刻意与C2采用了同- -种名为“Sea-of-Nodes”的高级中间表示(HighIR) 形式,使其能够更容易借鉴C2的优点。Graal|编译 器比C2编译器晚了足足二十年面世,有着极其充沛的后发优势,在保持输出相近质量的编译代码的同时,开发效率和扩展性上都要显著优于C2编译器,这决定了C2编译器中优秀的代码优化技术可以轻易地移植到Graal编译器上,但是反过来Graal编译器中行之有效的优化在C2编译器里实现起来则异常艰难。这种情况下,Graal的编译效果短短几年间迅速追平了C2,甚至某些测试项中开始逐渐反超C2编译器。Graal能够做比C2更加复杂的优化,如“部分逃逸分析”(Partial Escape Analysis),也拥有比C2更容易使用激进预测性优化(Aggressive Sp eculative Op timization)的策略,支持自定义的预测性假设等。

今天的Graal编译器尚且年幼,还未经过足够多的实践验证,所以仍然带着“实验状态”的标签,需要用开关参数去激活。
Graal编译器未来的前途可期,作为Java虛 拟机执行代码的最新引擎,它的持续改进,会同时为HotSpot与GraalVM注入更快更强的驱动力。

使用参数来启用Graal编译器。

1
-XX: +UnlockExperimentalVM Options-XX: +UseJVM CICompiler

1.5.3 向Native迈进

image.png
Substrate VM补全了Graal VM“Run Programs Faster Anywhere“愿景蓝图里的最后-块拼图,让GraalVM支持其他语言时不会有重量级的运行负担。譬如运行JavaScript代码,Node,js的V8引擎执行效率非常高,但即使是最简单的HelloWorld,它也要使用约20M B的内存,而运行在Substrate VM上的Graaljs,跑一个HelloWorld则只需要4.2MB内存,且运行速度与V8持平。Substrate VM的轻量特性,使得它十分适合嵌入其他系统,譬如Oracle自家的数据库就已经开始使用这种方式支持用不同的语言代替PLSQL来编写存储过程。

1.5.4 灵活的胖子

HotSpot的定位是面向各种不同应用场景的全功能Java虛拟机[],这是一个极高的要求,仿佛是让一个胖子能拥有敏捷的身手-样的矛盾。如果是持续跟踪近几年OpenJDK的代码变化的人,相信都感觉到了HotSpot开发团队正在持续地重构着HotSpot的架构,让它具有模块化的能力和足够的开放性。模块化方面原本是HotSpot的弱项,监控、执行、编译、内存管理等多个子系统的代码相互纠缠。而IBM的J9就一-直做得就非常好,面向JavaME的J9虚拟机与面向JavaEE的J9虚拟机可以是完全由同一套代码库编译出来的产品,只有编译时选择的模块配置有所差别。

1.5.5 语言语法持续增强

现在的Java做并发处理的最小调度单位是线程,Java线程的调度是直接由操作系统内核提供的,会有核心态、用户态的切换开销。而很多其他语言都提供了更加轻量级的、由软件自身进行调度的用户线程(曾经非常早期的Java也有绿色线程),譬如Golang的Groutine、D语言的Fiber等。Loom项目就准备提供一套 与目前Thread类API非常接近的Fiber实现。

1.6 实战:自己编译JDK

image.png

1.6.1 获取源码

image.png

1
git clone https://github.com/openjdk/jdk.git

1.6.2 系统需求

  • 尽量选择Ubuntu
  • 先读doc/building.html文档
  • 所有的文件都不要放在带有中文的路径下

1.6.3 构建编译环境

  • 安装GCC
    1
    sudo apt-get install build-essential -y
    image.png
    最后,假设要编译大版本号为N的JDK,我们还要另外准备一一个大版本号 至少为N-1的、已经编译好的JDK,这是因为OpenJDK由多个部分( HotSpot、JDK 类库、JAXWS、JA……构成,其中部分(HotSpot) 代码使用C、C++编写,而更多的代码则是使用Java语言来实现,因此编译这些Java代码就需要用到另一个编译期可用的JDK,官方称这个JDK为”Bootstrap JDK”。编译OpenJDK 12时,Bootstrap JDK必须使用JDK 11及之后的版本。在Ubuntu中使用以下命令安装OpenJDK 11:
    1
    2
    3
    4
    #ubuntu
    sudo apt-get install openjdk-11-jdk
    #centos
    yum install -y java-1.8.0-openjdk-devel.x86_64

1.6.4 进行编译

–with-debug-level=:设置编译的级别,可选值为release、fastdebug、 slowde-bug, 越往后进行的优化措施就越少,带的调试信息就越多。还有一些虚拟机调试参数必须在特定模式下才可以使用。默认值为release.
–enable-debug:等效于–With-debuglevel= fastdebug。
–with-native- debug symbols=:确定调试符号信息的编译方式,可选值为none、intemal、xternal、 zip ped。
–with-version-string=:设置编译JDK的版本号,譬如java-vers ion的输出就会显示该信息。这个参数还有–with-version-=的形式,其中p art可以是pre、opt、 build、 major、 minor、security、 patch之- 一, 用于设置版本号的某一一个 部分。
–with-jvm-variants=[]:.编译特定模式( Variants)的HotSpot虚拟机,可以多个模式并存,可选值为server、client 、minimal、 core、 zero、 custom。
–with-jvm-features=< feature>[, ] :.针对–with-jvm-variants=custom时的 自定义虚拟机特性列表( Features),可以多个特性并存,由于可选值较多,请参见help命令输出。
–with-target-bits=:指明要编译32位还是64位的Java虚拟机,在64位机器上也可以通过交叉编译生成32位的虚拟机。
–with-=: 用于指明依赖包的具体路径,通常使用在安装了多个不同版本的Bootstrap JDK和依赖包的情况。其中ib的可选值包括boot-jd、freetype、 cups、 x、alsa、 liff、 jtreg、 libjpeg.
gfib、libpng、 lcns、 zlib。
–with-extra-flagtype>=: 用于设定C、C++和Java代码编 译时的额外编译器参数,其中flagtype可选值为cflags、cxtflags、 ldflags, 分别代表C、C++ 和Java代码的参数。
–with-conf-name= :指定编译配置名称,OpenJDK 支持使用不同的配置进行编译,默认会根据编译的操作系统、指令集架构、调试级别自动生成一个配置名称,譬如”linux-x86_64-server-release”,如果在这些信息都相同的情况下保存不同的编译参数配置,就需要使用这个参数来自定义配置名称。

1
bash configurate

image.png
image.png
进行编译

1
2
3
make images
#如果出现编译失败先进行clean再进行编译
make clean

image.png
image.png
编译完成后的文件位于jdk目录下

如果没有人为设置 过JDK开发版本的话,这个JDK的开发版本号里默认会带上编译的机器名

进入 build/配置名称/jdk 查看编译后的java版本

1
./java -version

image.png

1.6.5 在IDE工具中进行源码调试

工具

  • JetBrains的CLion 2019.1

    CLion安装后,新建一个项目,选择“New CMake Project from Sources”,在源码文件夹中填入 OpenJDK源码根目录,此时,CLion已经自动选择好了需要导入的源码,如下图所示。点击OK按钮 就会导入源码并自动创建好CMakeLists.txt文件。

image.png
这份自动生成的CMakeLists.txt并不能直接使用,OpenJDK本身也没有为任何IDE提供支持,但如 果只是为了能够在CLion中跟踪、阅读源码,而不需要修改重新编译的话,那直接在Run/Debug Configurations中增加一个CMake Application,然后Executable选择我们刚才编译出来的FastDebug或者 SlowDebug版的java命令,运行参数加上-version或者某个Class文件的路径,再把Before launch里面的 Build去掉,就可以开始运行调试了。
image.png
不过如果读者需要在CLion中修改源码,并重新编译产生新的JDK,又或者不想阅读时看见一堆头 文件缺失提示的话,那还是需要把CMakeLists.txt修好,在GitHub上已经有现成的参考,读者可以 直接下载,内容较多,篇幅所限,笔者就不在本文中列出了。 读者在调试Java代码执行时,如果要跟踪具体Java代码在虚拟机中是如何执行的,一开始可能会觉 得有些无处入手,因为目前HotSpot在主流的操作系统上,都采用模板解释器来执行字节码,它与即时 编译器一样,最终执行的汇编代码都是运行期间产生的,无法直接设置断点,所以HotSpot增加了以下 参数来方便开发人员调试解释器:

1
- XX:+TraceBytecodes - XX:StopInterpreterAt=<n>

这组参数的作用是当遇到序号为的字节码指令时,便会中断程序执行,进入断点调试。调试 解释器部分代码时,把这两个参数加到java命令的参数后面即可。 完成以上配置之后,一个可修改、编译、调试的HotSpot工程就完全建立起来了,Hot-Spot虚拟机 启动器的执行入口是java.c的JavaMain()方法,读者可以设置断点单步跟踪,

image.png

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

2.1 概述
2.2 运行时数据区域
image.png

2.2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器。在Java虚拟机的概念模型里[1],字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。

2.2.2 Java虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期 与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

2.2.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。

2.2.4 Java堆

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有 的对象实例以及数组都应当在堆上分配[1]”,而这里笔者写的“几乎”是指从实现角度来看,随着Java语 言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编 译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换[2]优化手段已经导致一些微妙 的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了

2.2.5 方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把 方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区 分开来。

2.2.6 运行时常量池

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

2.2.7 直接内存

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

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务 器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError异常。

2.3 HotSpot虚拟机对象探秘

对象的创建

创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,本书第7章将探讨这部分细节。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。

除如何划分可用空间之外,还有另外一个需要考虑的问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。 接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行 ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

image.png

强调“理论上”是因为在CMS的实现里面,为了能在多数情况下分配得更快,设计了一个叫作Linear Allocation Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区之后,在它里面仍然可以使用指针碰撞方式来分配。

对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标 记、可偏向)下对象的存储内容如下所示。
image.png

对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
image.png
接下来实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。

HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。

对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

2.3.3 对象的访问定位

创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

  • 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示。

image.png

  • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图所示。

image.png
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

句柄是操作系统在生成对象时分配给对象的唯一标识! 通过句柄可以获取操作系统提供的服务! 句柄不同于指针,如果你得到一个对象的指针,那你就可以在此对象上为所欲为!那操作系统就惨了!于是系统不给你指针,而给你一个加了限制的,用于跟踪对象的指针的标识——句柄!系统使用句柄向外提供服务就相对安全了! 操作系统是通过API提供服务的,对于用户来说,句柄等同于对象指针,但实际上句柄和指针根本不是一回事!

2.4 实战:OutOfMemoryError异常

在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError

2.4.1 Java堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

2.4.2 虚拟机栈和本地方法栈溢出

2.4.3 方法区和运行时常量池溢出

2.4.4 本机直接内存溢出

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

3.1 概述
3.2 对象已死?
3.2.1 引用计数算法
3.2.2 可达性分析算法
3.2.3 再谈引用
3.2.4 生存还是死亡?
3.2.5 回收方法区
3.3 垃圾收集算法
3.3.1 分代收集理论
3.3.2 标记-清除算法
3.3.3 标记-复制算法
3.3.4 标记-整理算法
3.4 HotSpot的算法细节实现
3.4.1 根节点枚举
3.4.2 安全点
3.4.3 安全区域
3.4.4 记忆集与卡表
3.4.5 写屏障
3.4.6 并发的可达性分析
3.5 经典垃圾收集器
3.5.1 Serial收集器
3.5.2 ParNew收集器
3.5.3 Parallel Scavenge收集器
3.5.4 Serial Old收集器
3.5.5 Parallel Old收集器
3.5.6 CM S收集器
3.5.7 Garbage First收集器
3.6 低延迟垃圾收集器
3.6.1 Shenandoah收集器
3.6.2 ZGC收集器
3.7 选择合适的垃圾收集器
3.7.1 Epsilon收集器
3.7.2 收集器的权衡3.7.3 虚拟机及垃圾收集器日志
3.7.4 垃圾收集器参数总结
3.8 实战:内存分配与回收策略
3.8.1 对象优先在Eden分配
3.8.2 大对象直接进入老年代
3.8.3 长期存活的对象将进入老年代
3.8.4 动态对象年龄判定
3.8.5 空间分配担保
3.9 本章小结

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

4.1 概述
4.2 基础故障处理工具
4.2.1 jps:虚拟机进程状况工具
4.2.2 jstat:虚拟机统计信息监视工具
4.2.3 jinfo:Java配置信息工具
4.2.4 jmap:Java内存映像工具
4.2.5 jhat:虚拟机堆转储快照分析工具
4.2.6 jstack:Java堆栈跟踪工具
4.2.7 基础工具总结
4.3 可视化故障处理工具
4.3.1 JHSDB:基于服务性代理的调试工具
4.3.2 JConsole:Java监视与管理控制台
4.3.3 VisualVM :多合-故障处理工具
4.3.4 Java M ission Control:可持续在线的监控工具
4.4 HotSpot虚拟机插件及工具
4.5 本章小结

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

5.1 概述
5.2 案例分析
5.2.1 大内存硬件上的程序部署策略
5.2.2 集群间同步导致的内存溢出
5.2.3 堆外内存导致的溢出错误
5.2.4 外部命令导致系统缓慢
5.2.5 服务器虚拟机进程崩溃
5.2.6 不恰当数据结构导致内存占用过大
5.2.7 由Windows虚拟内存导致的长时间停顿
5.2.8 由安全点导致长时间停顿
5.3 实战:Eclipse运行速度调优
5.3.1 调优前的程序运行状态
5.3.2 升级JDK版本的性能变化及兼容问题
5.3.3 编译时间和类加载时间的优化
5.3.4 调整内存设置控制垃圾收集频率
5.3.5 选择收集器降低延迟5.4 本章小结
第三部分 虚拟机执行子系统

第6章 类文件结构

6.1 概述
6.2 无关性的基石
6.3 Class类文件的结构
6.3.1 魔数与Class文件的版本
6.3.2 常量池
6.3.3 访问标志
6.3.4 类索引、父类索引与接口索引集合
6.3.5 字段表集合
6.3.6 方法表集合
6.3.7 属性表集合
6.4 字节码指令简介
6.4.1 字节码与数据类型
6.4.2 加载和存储指令
6.4.3 运算指令
6.4.4 类型转换指令
6.4.5 对象创建与访问指令
6.4.6 操作数栈管理指令
6.4.7 控制转移指令
6.4.8 方法调用和返回指令
6.4.9 异常处理指令
6.4.10 同步指令
6.5 公有设计,私有实现
6.6 Class文件结构的发展
6.7 本章小结

第7章 虚拟机类加载机制

7.1 概述
7.2 类加载的时机
7.3 类加载的过程
7.3.1 加载
7.3.2 验证
7.3.3 准备
7.3.4 解析
7.3.5 初始化
7.4 类加载器
7.4.1 类与类加载器
7.4.2 双亲委派模型
7.4.3 破坏双亲委派模型
7.5 Java模块化系统
7.5.1 模块的兼容性
7.5.2 模块化下的类加载器7.6 本章小结

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

8.1 概述
8.2 运行时栈帧结构
8.2.1 局部变量表
8.2.2 操作数栈
8.2.3 动态连接
8.2.4 方法返回地址
8.2.5 附加信息
8.3 方法调用
8.3.1 解析
8.3.2 分派
8.4 动态类型语言支持
8.4.1 动态类型语言
8.4.2 Java与动态类型
8.4.3 java.lang.invoke包
8.4.4 invokedynamic指令
8.4.5 实战:掌控方法分派规则
8.5 基于栈的字节码解释执行引擎
8.5.1 解释执行
8.5.2 基于栈的指令集与基于寄存器的指令集
8.5.3 基于栈的解释器执行过程
8.6 本章小结

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

9.1 概述
9.2 案例分析
9.2.1 Tomcat:正统的类加载器架构
9.2.2 OSGi:灵活的类加载器架构
9.2.3 字节码生成技术与动态代理的实现
9.2.4 Backport工具:Java的时光机器
9.3 实战:自己动手实现远程执行功能
9.3.1 目标
9.3.2 思路
9.3.3 实现
9.3.4 验证
9.4 本章小结

第10章 前端编译与优化

10.1 概述
10.2 Javac编译器
10.2.1 Javac的源码与调试
10.2.2 解析与填充符号表
10.2.3 注解处理器10.2.4 语义分析与字节码生成
10.3 Java语法糖的味道
10.3.1 泛型
10.3.2 自动装箱、拆箱与遍历循环
10.3.3 条件编译
10.4 实战:插入式注解处理器
10.4.1 实战目标
10.4.2 代码实现
10.4.3 运行与测试
10.4.4 其他应用案例
10.5 本章小结

第11章 后端编译与优化

11.1 概述
11.2 即时编译器
11.2.1 解释器与编译器
11.2.2 编译对象与触发条件
11.2.3 编译过程
11.2.4 实战:查看及分析即时编译结果
11.3 提前编译器
11.3.1 提前编译的优劣得失
11.3.2 实战:Jaotc的提前编译
11.4 编译器优化技术
11.4.1 优化技术概览
11.4.2 方法内联
11.4.3 逃逸分析
11.4.4 公共子表达式消除
11.4.5 数组边界检查消除
11.5 实战:深入理解Graal编译器
11.5.1 历史背景
11.5.2 构建编译调试环境
11.5.3 JVM CI编译器接口
11.5.4 代码中间表示
11.5.5 代码优化与生成
11.6 本章小结

第12章 Java内存模型与线程

12.1 概述
12.2 硬件的效率与一致性
12.3 Java内存模型
12.3.1 主内存与工作内存
12.3.2 内存间交互操作
12.3.3 对于volatile型变量的特殊规则
12.3.4 针对long和double型变量的特殊规则12.3.5 原子性、可见性与有序性
12.3.6 先行发生原则
12.4 Java与线程
12.4.1 线程的实现
12.4.2 Java线程调度
12.4.3 状态转换
12.5 Java与协程
12.5.1 内核线程的局限
12.5.2 协程的复苏
12.5.3 Java的解决方案
12.6 本章小结

第13章 线程安全与锁优化

13.1 概述
13.2 线程安全
13.2.1 Java语言中的线程安全
13.2.2 线程安全的实现方法
13.3 锁优化
13.3.1 自旋锁与自适应自旋
13.3.2 锁消除
13.3.3 锁粗化
13.3.4 轻量级锁
13.3.5 偏向锁
13.4 本章小结

资料

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


读书笔记篇-深入理解Java虚拟机
https://mikeygithub.github.io/2021/06/19/yuque/读书笔记篇-深入理解Java虚拟机/
作者
Mikey
发布于
2021年6月19日
许可协议