《深入理解 Java 虚拟机》阅读笔记
本 repo 为《深入理解 Java 虚拟机 第2版》的阅读笔记,并对全书内容按照自己的理解进行了一定程度的整理。《深入理解 Java 虚拟机 第2版》原书主要分为了五个部分,这里仅对前四个部分进行讲解,第五部分(高效并发)整合进了另一个 repo:Java 并发编程实战的阅读笔记 中。
其中前两部分:Java 内存管理机制和 Java 虚拟机程序执行需要重点掌握,并且内容也是比较多的。本 repo 将原书中有关虚拟机性能监控及故障处理的部分单抽了出来,组成了本 repo 的第三部分。第四部分对应于原书的第四部分,程序编译与代码优化,不过仅对 Java 的运行期优化,也就是 JIT 时进行的优化进行了总结,编译器优化部分尚未进行深入研究。
阅读方法: 本 repo 的 README.md 从头读到尾就是一个虚拟机大部分知识点的框架,就像一颗搜索树一样,我们想要了解哪一部分知识,就从根节点开始搜索,直到找到我们想要了解的知识所在的叶节点或者子树。小伙伴们可以通过 README.md 回忆 JVM 相关的知识,遇到想不起来的点就点开相应的链接查看。这样像考试一样的学习方式,可以加深印象,记忆效果将远远好于盯着文字硬背。
以下为本 repo 文章的目录:
Content
Java 内存管理机制
Java 虚拟机程序执行
- 00-Class文件的组成结构
- 01-虚拟机的类加载机制
- 02-虚拟机字节码执行引擎
- 附录0-实现Java类的热替换
虚拟机性能监控及故障处理
Java 程序运行优化
“串一串” Java 虚拟机的知识点
本文将按照 Content 中给出的四个部分加上 Java 的内存模型部分进行说明,首先先来说说 Java 的内存管理机制。
说说 Java 的内存管理机制
和 C++ 相比,Java 的内存管理机制可谓是一大特色,程序员们不需要自己去写代码手动释放内存了,甚至你想自己干虚拟机都不给你干这个事情的机会(就是说,我们是没有办法自动触发 GC 的),虚拟机全权包办了 Java 的内存控制权力。这看起来挺美好的,不过也意味着,一旦虚拟机疏忽了(感觉不能赖虚拟机,毕竟虚拟机也不知道你能把程序写成那样啊……),发生了内存泄漏,问题都不好查,所以知道虚拟机到底是怎么管的内存就十分重要啦。
虚拟机对内存的管理,其实就是收拾那些存放我们不会再用的对象的内存,把它们清了拿来放新的对象。所以它首先需要研究下以下几个问题:
- 这堆报废了的对象到底被放哪了?(Java 堆和方法区)
- 这堆放报废对象的地方会不会内存泄漏?或者换一个洋气点的叫法,会不会 OOM?(每个区的 OOM)
- 对象是咋被放到这些地方的?(堆中对象的创建)
- 对象被安置好了之后虚拟机怎么再次找到它?(堆中对象的访问)
知道对象都放哪了,虚拟机就知道去哪里找报废的对象了,接下来就涉及到了 Java 的一大超级特色:垃圾收集(GC)了,垃圾收集,正如其名,就是把这些报废的对象给清了,腾出来地方放新对象,它主要关心以下几个事情:
- 哪些内存需要回收?
- 放对象的地方需要垃圾回收:Java 堆和方法区。
- 什么时候回收?(判断对象的生死)
- 如何回收?
- GC 算法原理(垃圾收集算法)
- GC 算法的真正实现:
- 7 个葫芦娃,哦不,垃圾收集器
- 新生代:Serial、ParNew、Parallel Scavenge
- 老年代:Serial Old、Parallel Old、CMS
- 全能:G1
- HotSpot 虚拟机如何高效实现 GC 算法
- 7 个葫芦娃,哦不,垃圾收集器
说完了对象是怎么被回收的,现在才算是把 Java 的内存管理机制需要用到的小零件给补全了。也就是说,Java 的内存管理流程应该是这样滴:
- 根据新对象是什么对象给对象找个地放
- 发现内存中没地放这个新对象了就进行 GC 清理出来点地方
- 真找不着地了就抛 OOM ……
虚拟机一般都用的是进化版的 GC 算法,也就是分代收集算法,也就是说,虚拟机 Java 堆中的内存是分为新生代和老年代的,那么给新对象找地方放的时候放哪呢?具体怎么放呢?放好了之后的对象会不会换个地呆呀?GC 什么时候进行?清理哪呢?……预知 Java 的内存管理机制的详情如何,请看:Java 内存分配策略。
到此为止,Java 的内存管理机制也就说的差不多了。现在,我们已经知道一个对象是如何在虚拟机的操控下,在内存中走一遭的了。可是首先,对象肯定是根据我们写的类创建的,那么我们写的类到底是如何变为内存中的对象的呢?而且,我们创建对象当然是为了执行它里面的方法呀,那么这个方法是怎么被执行的呢?想要回答这些问题,就需要我们研究一下 Java 虚拟机是如何执行我们的程序的了。
说说 Java 虚拟机程序执行
想要执行 Java 程序,必然要先将 Java 代码编译成字节码文件,也就是 Class 文件,这个编译的过程我们暂且不谈,主要说一下如何执行这个 Class 文件,所以首先我们要先来了解一下 Class 文件的组成结构。
在了解了组成结构之后,接下来需要考虑的事情是,我们该怎么把这个 .class 文件加载进内存,让它变成方法区(Java 8 后变为了 Metaspace 元空间)的一个 Class 对象呢?(类的加载)。
虚拟机的类加载机制说头可就多了,大家都喜欢揪着这问,其实主要就下面这 3 个过程:
- 类加载的时机:在程序第一次主动引用类的时候。
- 什么是主动引用和被动引用?
- 什么是显式加载和隐式加载?
- 类的生命周期:加载 —— 验证 —— 准备 —— 解析 —— 初始化 —— 使用 —— 卸载
- 类加载器
- 如何判断两个类 “相等”?
- 类加载器的分类?
- 什么双亲委派模型?
- 破坏双亲委派模型?
- 如何自定义类加载器?
- 需要保留双亲委派模型:
extends ClassLoader
,重写findClass()
- 破坏双亲委派模型:直接重写
loadClass()
- 需要保留双亲委派模型:
将类加载到内存之后,接下来就要考虑如何执行这个类中的方法了。我们知道 5 大内存区域中的 Java 虚拟机栈是服务与 Java 方法的内存模型,那么我们首先应该了解一下 虚拟机栈的栈帧到底是怎样的结构,虚拟机栈的栈帧结构包括如下几个部分:
了解了辅助方法执行的 Java 虚拟机栈的结构后,接下来就要考虑 Java 类中方法的调用了。就像将大象放进冰箱,方法的调用也不是上来就直接执行方法的,而是分为以下两个步骤:
为什么还要加一个方法调用的步骤呢?因为一切方法调用都是在 Class 文件中以常量池中的符号引用存储的,这就导致了不是我们想要执行哪个方法就能立刻执行的,因为我们首先需要根据这个符号引用(其实就一字符串)找到我们想要执行的方法,而这一过程就叫做方法调用。当找到这个方法之后,我们才会开始执行这个方法,也就是基于栈的解释执行。
想要调用一个方法,我们先来看一下虚拟机中有哪些指令可以进行方法调用:方法调用字节码指令。
这些字节码会触发不同的方法调用,总体来说,有以下几种:
确定了要调用的方法具体是哪一个了之后,就可开始基于栈的解释执行了,这个时候,方法才真正的被执行。
此外,还需要了解一下 Java 的动态类型语言支持。
说说虚拟机性能监控及故障处理
常用的 JDK 命令行工具:JDK 命令行工具。
JVM 常见的参数设置已经设置经验可见:JVM 常见参数设置。
虚拟机调优案例分析可见:虚拟机调优案例分析。
说说 JIT 优化
JIT (Just In Time),也就是即时编译,首先我们需要知道 什么是 JIT?
然后,对于 HotSpot 虚拟机内的即时编译器运作过程,我们可以通过以下 5 个问题来研究它:
- 为什么要使用解释器与编译器并存的架构?
- 为什么虚拟机要实现两个不同的 JIT 编译器?
- 什么是虚拟机的分层编译?
- 如何判断热点代码,触发编译?
- 什么是热点代码?(两种)
- 什么是 “多次” 执行?
- HotSpot 采用的是基于计数器的热点探测方法,并且为了对两种热点代码进行探测,每个方法有 2 个计数器:
- 方法调用计数器
- 回边计数器
- HotSpot 热点代码探测流程
- 热点代码编译的过程?
此外,JIT 并不是简单的将热点代码编译成机器码就收工的,它还会对代码的执行进行优化,主要有以下几种经典的优化技术:
说说 Java 的内存模型 (JMM)
这部分内容主要与并发编程的内容相关,所以详细介绍会跳到另一个 repo:Java-Concurrency-in-Practice。
Java 的内存模型主要就是研究一个变量的值是怎么在主内存、线程的工作内存和 Java 线程(执行引擎)之间倒腾的。就是说虽然 Java 内存模型规定了所有变量都存储在主内存中,但是每个线程都有一个自己的工作内存,里面存着从主内存拷贝来的变量副本,Java 线程要对变量进行修改,都是先在自己的工作内存中进行,然后再把变化同步回主内存中去。
这样做是由于计算机的存储设备和处理器的运算速度有着几个数量级的差距,所以需要在主内存和 Java 线程间加入一个工作内存作为缓冲,但这也同时会导致主内存和工作内存间的缓存一致性问题,所以当两个工作内存中关于同一个变量的值发生冲突时,需要一定的访问规则来确定主内存以怎样的顺序同步这个变量,也就是说该听哪个工作内存的。而 Java 的内存模型的主要目标就是定义这个规则,即虚拟机如何将变量存储到内存或是从内存中取出的。
简单的来讲,就是掌握 Java 内存模型中的 8 个原子操作,并且知道 Java 内存间是如何通过这 8 个操作进行变量传递的。
其实 Java 的内存模型就是围绕着在并发的过程中如何处理 原子性、可见性、有序性 这 3 个特征建立的。同时 Java 除了可以依靠 volatile 和 synchronized 来保证有序性外,它自己本身还有一个 Happens-Before 原则,依靠这个原则,我们就可以判断并发环境下的两个操作是否可能存在冲突了。
项目推荐
对 JVM 相关知识的考查几乎成为 Java 面试的必备科目了,但是,就在简历上写个对 Java 虚拟机有一定了解,那极有可能被问到知识盲区呀!所以最好能在简历上就清晰明白的告诉人家我们都会啥,正如忘了在哪里看到的一个很有道理的话所言:简历就是我们准备面试的复习大纲。此时,倘若在简历上有那么一个项目可以用上 JVM 的相关的知识,那么在面试的时候,我们就可以基于这个项目开始我们的表演啦。
不过老实讲,Java 虚拟机相关的知识还真的不太好用在项目中,或者说不太好在项目中体现出来。这个问题我也想了好久,最后终于在看《深入理解 Java 虚拟机》第 9 章中的实战:自己动手实现远程执行功能时找到了答案,个人认为该实战中用到的通过修改字节码来替换 Java 代码中对于 System 类中方法的调用的技术酷极了!可是如果只是写这么一个小模块作为一个项目写在简历上又太小了点,所以,在有一天刷 LeetCode 时,突然灵光一闪,想到可以基于这个实战做一个在线 Java IDE,就有了这个项目:基于 SpringBoot 的在线 Java IDE 。
该项目基于 SpringBoot 实现了一个在线的 Java IDE,可以远程运行客户端发来的 Java 代码的 main 方法,并将程序的标准输出内容、运行时异常信息反馈给客户端,并且会对客户端发来的程序的执行时间进行限制。涉及了 Java 类文件的结构,Java 类加载器和 Java 类的热替换等 JVM 相关的技术,十分适合作为《深入理解 Java 虚拟机》这本书的一个实战内容,用来加深对该书内容的理解。