我所理解的JVM虚拟机(上)

我所理解的JVM虚拟机(上)

这是第一篇写JVM的博客,我会分上中下三篇文章,讲述我对JVM的理解。
第一篇也是本篇,主要是讲述JVM的整体架构以及垃圾收集的几种算法,先了解JVM是什么,它长什么样,我们要关注哪些地方,它是怎么运作的。
对于垃圾回收器以及不同的内存分代JVM调优参数配置会在后面讲述。


学习Java这门语言为什么一定要了解JVM,为什么虚拟机作为中高级开发人员必须修炼的知识,因为保障系统的性能、并发和伸缩等能力都与虚拟机的运作密不可分。作为一名Java研发,我也本着提升能力的态度学习了一下JVM虚拟机,同时写篇文章来介绍一下我对JVM的理解。

了解JVM虚拟机,首先要了解虚拟机的基本架构,如下图:

image

虚拟机的总体架构可以按照图片上分为五大块,分别为类加载系统、运行时数据、执行引擎、Java本地接口、本地方法库五大快,其中最核心的部分就是执行引擎,因为执行引擎的存在,才能把Java的编译文件在不同的平台上运行,说到Java这门语言跨平台,更准确的说,应该是JVM虚拟机跨平台了,因为任何平台上都要有对应平台的JVM虚拟机才能运行Java程序。

1.类加载系统

主要是负责加载jre中的类库文件、以及用户的编译文件,根据字节文件的编码做文件的版本、文件有效性校验等(字节码中有个很出名的词,叫做咖啡宝贝),最终做初始化操作。

2.运行时数据区

这个区域的方法区在jdk1.8时,已经移到了元数据空间了,目前可以分为堆区、栈区(我更喜欢叫它Java虚拟机栈)、PC寄存器、本地方法栈。

2.1 堆区

    对于大多数应用来说,堆是虚拟机管理的最大的一块,也是所有线程共享的一块区域,这个区域在虚拟机启动时,就创建了,这个区域做的事情就一件,就是存放所有的对象实例,也就是引用对象的内存分配都在这里实现了,这也是Java虚拟机的规范(所有的对象实例及数组都要分配在堆上)但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,这也变得不是那么绝对了,有时候在栈上分配,标量替换也会有一些微妙的变化发生。
    堆呢,也是垃圾收集器一直在管理的一块区域,也就是我们说的GC,也是JVM调优一定要掌握的知识(调优其实并没有想象中那么难,但其实还真的挺难得),对于堆区的具体运作方式下文会详细介绍。

2.2栈区(Java虚拟机栈)

    这个区域是线程私有的,也是与线程的生命周期相同的,因为每一个请求都会开始一个新的虚拟机栈,请求的开始与结束就意味着新的虚拟机栈的入栈到出栈的过程,没一个请求的方法都会有个栈帧的产生在栈中叠加,一个方法就会有一个新的栈帧的产生,栈帧中存储的就是对应方法的变量,基础数据类型以及对对象实例的引用指针,你可以先简单的理解为就是存储基础的数据类型和引用类型的指针,栈帧是有深度的,超过了一定的深度便会出现Stack Overflow Exp异常,当然也有可能会因为申请不到内存而OOM的,哈哈哈。

2.3 PC寄存器

    这是一块较小的空间,它能记录程序运行的行号,并做指示,字节码解释器的工作就是根据这个计数器的值来确定下一步是执行循环还是分支、跳转、异常处理、线程恢复等等,每个线程都有自己的独立的计数器,因此各个线程之间互相不受影响,独立储存,所以在寄存器内线程有自己的私有内存。

2.4 本地方法栈

    这个栈和Java虚拟机栈非常的相似,不同的事Java虚拟机栈是为字节码服务的,而本地方法栈是为Native方法服务的,或许研究多线程的时候,你就会发现Start()方法为什么可以启动多线程,因为调用了底层的Native方法,这个方法栈与Java虚拟机栈一样会出现OOM或者Stock Overflow Exp异常。

整个运行时数据区大致就是这样的结构。


了解了整个JVM的内存架构后,便开始了解垃圾是如何回收的。

3.对象可以回收了吗

首先要确认对象是否可以回收,那么需要明确对象是否还在被使用,对此虚拟机提供了两个方法来确定对象是否还存活。

3.1 引用计数法

    给每个对象添加一个引用计数器,一个对象被引用一次计数器就加1,引用失效时,就减1,当对象的引用计数为0时,代表对象可悲回收,当然不会立即就回收,回收前后虚拟机有一些其他工作需要处理。
    客观上来说,引用计数法的实现简单,效率也高,但是有一个缺陷,就是无法解决互相引用的问题,当对象两两互相引用,就能保持计数器一直大于0,便无法回收。

3.2 可达性算法

    这是一种枚举根节点的算法,什么是根节点?
    虚拟机栈
    方法区静态变量
    方法区常量引用
    本地方发栈引用的对象
    如果是上述对象引用的对象,并且通过引用的对象所引用的其他对象,根据路径查询能找到的对象,都是存活对象,其他对象都是死亡对象,以此来解决两两互相引用的缺陷,这也是CMS、以及G1垃圾回收器判断对象是否存活的算法。

4.垃圾收集算法

确定好已经死亡的对象,需要一些算法来整理内存中的垃圾,于是就有了垃圾手收集算法

4.1标记-清除算法

    这个算法就像名字一样显眼,先标记再清除,标记出需要回收的对象,然后清除,如图,灰色格子空间为存活对象,黑色为可回收,白色为空闲的空间,该算法虽然效率高,但是有个缺陷,回收后(中间部分的大方块区间)产生大量的内存碎片,导致申请新的内存空间时,比较耗时,因为不是所有的对象需要的内存都刚好和回收后新空出的内存空间一样大,这样会需要往后以此寻找,直到找到一块空间足够的内存空间。

image
标记-清除算法 效果图

4.2复制算法

   复制算法是将内存分为对称的两块区域,其中一块区域空闲着,另一块区域被使用,当被使用的区域满了,就将对象标记回收,然后将存活的全部对象一次性移动到另一半空闲的内存中,这样自己就变成空闲的内存接收了移动过来的对象的空间,就被使用起来了,如此循环,周而复始,这就是复制算法,虽然该算法解决了标记清楚带来了碎片空间太多的问题,但是造成了空间浪费,因为每次都有一半的空间在空闲着,虽然有次缺点,但却被广泛使用,当了解到新生代的时候,就能发现这个算法的奇妙之处,在新生代和老年代的回收过程中甚至还有分配担保机制等。

image
复制算法 效果图

4.3 标记-整理算法

标记整理算法和标记-清理算法有些类似,但是不同的是,标记-清除算法的步骤是 
1.标记回收对象
2.清除回收对象
而标记-整理算法则是
1.标记回收对象
2.整理对象空间
3.清除回收对象
第二步整理对象空间的操作是将所有存活对象往一边移动,然后将剩余的部分(即全部的回收对象)一次性清除,就可以避免内存碎片的产生,先整理再清除,并非先清除再整理。这种算法比较适合也是目前在老年代中使用较多的算法。

image
标记-整理算法 效果图

4.4 分代收集算法

分代收集算法其实就是分不同的内存区域,分为新生代、老年代,在不同的代中可以根据不同的特点采用不同的收集回收算法。

当我们知道了垃圾收集有哪些算法后,就需要了解垃圾收集器,在了解垃圾收集器之前要先了解下安全点、安全区域的概念。

程序不是在任何时候都可以GC的,所以有安全点和安全区域两个概念,他们是决定什么时候可以进行到GC的,比如代码for循环运行到当前循环的末尾,方法运行到返回之前等等,都是安全点的范围,此时才可以开始GC。

关于JVM的第一篇就介绍到这里,剩下的我们第二篇见。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!