深入浅出JVM

/ JVM

JVM是什么

JVM(Java Virtual Machine,java虚拟机)是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的

众所周知,Java语言有个响亮的口号“Wirte Once,Run Anywhere”,实现了跨平台的语言编写。其中屏蔽平台操作系统的差异就是通过虚拟机实现的。

JVM主要由以下几部分组成。

JVM组成部分

Java中由native修饰的方法,就是本地方法接口了。本地方法通常由C/C++编写,也就是JVM的源码了,具体查看可以通过自己反编译OpenJdk,也可以通过hg.openjdk.java.net查看需要的源码内存。这些源码其实就是屏蔽平台系统差异的关键了。

基于本地方法和执行引擎之上,就是Java的运行时数据区了。我们编写的代码,建立的对象都会在这个区中进行创建、使用和销毁。这部分也是我们需要了解的重点部分。

运行时的数据是怎么来的呢?这个就是类加载器的作用了。它可以把编译器编译的Class文件,加载到运行时数据区中进行使用。

以上就是JVM的总览了,就让我自上而下来了解下JVM是如何工作的。

类加载器

类加载器其实就是将编译好的Class文件加载到内存中,下面让我们来了解下加载的主要流程。

类加载过程

由图可知主要流程如下。

这里我们重点研究下加载的过程。先来让我们看下jdk源码中是如何实现加载的。

loadClass

我们可以看到,让进行加载时,当前类会首先委托给父类进行加载,直到最顶层的Bootstrap加载器。这种加载模式就被我们称为双亲委派模型

双亲委派模型

此时有个问题产生了,为什么要使用这种加载模式呢?

我们知道Java的类名是可以重复的,如果类都是各自加载各自的,那么势必会出现很多重复的类,那么程序岂不就是乱套了......

定义双亲加载模型,可以很好的显示出类的优先级,这样就会避免了类的重复加载

运行时数据区

当类被加载完成后,接下来就是重要的运行部分了。首先,让我们了解下运行时数据区的主要组成部分。

运行时数据区

这里的Java虚拟机栈、本地方法栈和PC寄存器都是线程独享的数据,它的内存会随线程销毁而释放。

而堆和方法区是共享内存,他们的内存会通过GC得到释放。

接下来让我们再看看GC是怎么进行回收的。

垃圾收集模块

垃圾收集器(Garbage Collection,GC),主要是负责共享内存的回收。

在了解GC是如何工作的之前,我们需要知道什么是垃圾?

什么是垃圾

判断垃圾的算法通常有两种。

引用计数法:给对象添加一个引用计数器,每当一个地方引用它时,计数器加1,每当引用失效时,计数器减少1.当计数器的数值为0时,也就是对象无法被引用时,表明对象不可在使用。此时就可以被回收了。

乍一看简单明了,但是当两个可以成为垃圾的对象互相引用时(循环引用),此时用这种算法就永远不会回收,造成内存泄漏。

因此,我们现在JVM使用的是另一种,可达性分析法。

可达性分析法:通过一些被称为引用链“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为“Reference Chain”,当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

那么什么对象可以为GC Roots呢?通常会有以下几种对象。

根据可达性分析法我们把对象的引用分为以下4种。

至此,我们知道了什么是垃圾,接下来就让我了解下垃圾是如何回收的。

垃圾回收算法

通常垃圾回收算法有以下几种。

标记-清除算法

标记清除算法(图片来源自网络https://blog.csdn.net/yrwan95/article/details/82829186)

该算法简单易实现,主要分为两个阶段。

但是我们可以很容易看到清除的内存有很多内存碎片,这样极不利于JVM对内存的再次分配(不连续的空间会使大对象无法分配)。

复制算法

复制算法(图片来源自网络https://blog.csdn.net/yrwan95/article/details/82829186)

为了解决标记清除算法产生内存碎片问题,复制算法将内存按容量划分了相同的两块。每次只是用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样就不会产生内存碎片了。

但是这种算法很浪费内存空间。

标记-整理算法

标记-整理算法(图片来源自网络https://blog.csdn.net/yrwan95/article/details/82829186)

该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

这种算法遇到对象存活较多时,就会有很多复制操作,极大的降低的系统效率。

分代收集算法

海纳百川,有容乃大。好的东西往往都是集百家之长。目前JVM使用的垃圾回收算法就是这种分代收集算法。

分代收集算法按对象时间存活长短把内存空间化分为新生代与老年代。

新生代的对象朝生夕死,使用复制算法提升回收效率。然后将多次回收后存活的对象放入老年代。

老年代的对象存活概率较高,使用标记-清除算法或者标记-整理算法来节约空间。

有了这些算法思想的指导,接下来就让我们看下算法是如何实现的。

垃圾收集器

常见的垃圾收集器主要分为以下几种。

串行收集:仅用一个线程进行垃圾回收,回收时进入STW(Stop The World)状态,常见于嵌入式

并行收集:多条垃圾收集线程并行工作,但用户线程仍为等待状态,注重吞吐量

并行收集:用户线程和垃圾收集线程同时执行,有利于并发

现在的业务量越来越大,并发越来越来成为人们关注的点,因此一般业务上使用的都是并发收集器。

并发收集器的特点就是并发好,停顿小。这里的CMS算法是最初的并发收集算法,Java 8推出了性能更为优秀的G1收集器,且Java 9后G1收集器已经为JVM默认收集器了。ZGC是Java 11版本推出来的收集器,它在G1收集器的基础上进行了改进,号称可以达到10ms以下的GC停顿。这三种收集器实现原理后面有时间我将单独接受下,这里就不展开了。

由此可见,在Java 11普及之前,G1是大家的首选收集器,ZGC将是未来的主流。

总结

至此我们已经了解了JVM的大部分内容,还剩下执行引擎与本地方法库没有深入了解。

执行引擎是JVM的“硬核”驱动,实现了Class文件的编译与执行过程,偏向底层,理解起来不是很容易,待我研究一哈再来介绍。

本地方法库其实就是JNI部分,此部分是JVM源码部分由C/C++实现,只能放到最后再来研究了。不过在看源码的时候我们可以通过hg.openjdk.java.net来大致了解下也是不错的。