《深入理解Java虚拟机》阅读笔记
本repo为《深入理解Java虚拟机 第2版》的阅读笔记,并对全书内容按照自己的理解进行了一定程度的整理。《深入理解Java虚拟机 第2版》原书主要分为了五个部分,这里仅对前四个部分进行讲解,第五部分(高效并发)整合进了另一个repo:Java并发编程实战的阅读笔记 中。
其中前两部分:Java内存管理机制和Java虚拟机程序执行需要重点掌握,并且内容也是比较多的。本repo将原书中有关虚拟机性能监控及故障处理的部分单抽了出来,组成了本repo的第三部分。第四部分对应于原书的第四部分,程序编译与代码优化,不过仅对Java的运行期优化,也就是JIT时进行的优化进行了总结,编译器优化部分尚未进行深入研究。
阅读方法: 本repo的README.md从头读到尾就是一个虚拟机大部分知识点的框架,就像一颗搜索树一样,我们想要了解哪一部分知识,就从根节点开始搜索,直到找到我们想要了解的知识所在的叶节点或者子树。小伙伴们可以通过README.md回忆JVM相关的知识,遇到想不起来的点就点开相应的链接查看。这样像考试一样的学习方式,可以加深印象,记忆效果将远远好于盯着文字硬背。
以下为本repo文章的目录:
Content
Java内存管理机制
Java虚拟机程序执行
虚拟机性能监控及故障处理
Java程序运行优化
“串一串” Java虚拟机的知识点
本文将按照Content中给出的四个部分加上Java的内存模型部分进行说明,首先先来说说Java的内存管理机制。
说说Java的内存管理机制
和C++相比,Java的内存管理机制可谓是一大特色,程序员们不需要自己去写代码手动释放内存了,甚至你想自己干虚拟机都不给你干这个事情的机会(就是说,我们是没有办法自动触发GC的),虚拟机全权包办了Java的内存控制权力。这看起来挺美好的,不过也意味着,一旦虚拟机疏忽了(感觉不能赖虚拟机,毕竟虚拟机也不知道你能把程序写成那样啊……),发生了内存泄漏,问题都不好查,所以知道虚拟机到底是怎么管的内存就十分重要啦。
虚拟机对内存的管理,其实就是收拾那些存放我们不会再用的对象的内存,把它们清了拿来放新的对象。所以它首先需要研究下以下几个问题:
- 这堆报废了的对象到底被放哪了?(Java堆和方法区)
- 这堆放报废对象的地方会不会内存泄漏?或者换一个洋气点的叫法,会不会OOM?(每个区的OOM)
- 对象是咋被放到这些地方的?(堆中对象的创建)
- 对象被安置好了之后虚拟机怎么再次找到它?(堆中对象的访问)
知道对象都放哪了,虚拟机就知道去哪里找报废的对象了,接下来就涉及到了Java的一大超级特色:垃圾收集(GC)了,垃圾收集,正如其名,就是把这些报废的对象给清了,腾出来地方放新对象,它主要关心以下几个事情:
说完了对象是怎么被回收的,现在才算是把Java的内存管理机制需要用到的小零件给补全了。也就是说,Java的内存管理流程应该是这样滴:
- 根据新对象是什么对象给对象找个地放
- 发现内存中没地放这个新对象了就进行GC清理出来点地方
- 真找不着地了就抛OOM ……
虚拟机一般都用的是进化版的GC算法,也就是分代收集算法,也就是说,虚拟机Java堆中的内存是分为新生代和老年代的,那么给新对象找地方放的时候放哪呢?具体怎么放呢?放好了之后的对象会不会换个地呆呀?GC什么时候进行?清理哪呢?……预知Java的内存管理机制的详情如何,请看:Java内存分配策略。
到此为止,Java的内存管理机制也就说的差不多了。现在,我们已经知道一个对象是如何在虚拟机的操控下,在内存中走一遭的了。可是首先,对象肯定是根据我们写的类创建的,那么我们写的类到底是如何变为内存中的对象的呢?而且,我们创建对象当然是为了执行它里面的方法呀,那么这个方法是怎么被执行的呢?想要回答这些问题,就需要我们研究一下Java虚拟机是如何执行我们的程序的了。
说说Java虚拟机程序执行
想要执行Java程序,必然要先将Java代码编译成字节码文件,也就是Class文件,这个编译的过程我们暂且不谈,主要说一下如何执行这个Class文件,所以首先我们要先来了解一下 Class文件的组成结构。
在了解了组成结构之后,接下来需要考虑的事情是,我们该怎么把这个.class文件加载进内存,让它变成方法区(Java 8后变为了Metaspace元空间)的一个Class对象呢?(类的加载)。
虚拟机的类加载机制说头可就多了,大家都喜欢揪着这问,其实主要就下面这3个过程:
将类加载到内存之后,接下来就要考虑如何执行这个类中的方法了。我们知道5大内存区域中的Java虚拟机栈是服务与Java方法的内存模型,那么我们首先应该了解一下 虚拟机栈的栈帧到底是怎样的结构,虚拟机栈的栈帧结构包括如下几个部分:
了解了辅助方法执行的Java虚拟机栈的结构后,接下来就要考虑Java类中方法的调用了。就像将大象放进冰箱,方法的调用也不是上来就直接执行方法的,而是分为以下两个步骤:
为什么还要加一个方法调用的步骤呢?因为一切方法调用都是在Class文件中以常量池中的符号引用存储的,这就导致了不是我们想要执行哪个方法就能立刻执行的,因为我们首先需要根据这个符号引用(其实就一字符串)找到我们想要执行的方法,而这一过程就叫做方法调用。当找到这个方法之后,我们才会开始执行这个方法,也就是基于栈的解释执行。
想要调用一个方法,我们先来看一下虚拟机中有哪些指令可以进行方法调用:方法调用字节码指令。
这些字节码会触发不同的方法调用,总体来说,有以下几种:
- 解析调用
- 分派调用(没有在解析调用中将符号引用转化为直接引用的方法就只能靠分派调用了)
确定了要调用的方法具体是哪一个了之后,就可开始基于栈的解释执行了,这个时候,方法才真正的被执行。
此外,还需要了解一下 Java的动态类型语言支持。
说说虚拟机性能监控及故障处理
常用的JDK命令行工具:JDK命令行工具。
JVM常见的参数设置已经设置经验可见:JVM常见参数设置。
虚拟机调优案例分析可见:虚拟机调优案例分析。
说说JIT优化
JIT (Just In Time),也就是即时编译,首先我们需要知道 什么是JIT?
然后,对于 HotSpot虚拟机内的即时编译器运作过程,我们可以通过以下5个问题来研究它:
此外,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虚拟机》这本书的一个实战内容,用来加深对该书内容的理解。