深入JVM(二)JVM垃圾回收器

深入JVM(二)JVM垃圾回收器

什么垃圾?为什么要回收?

前提:在java中创建对象的过程就是在内存中分配区域的过程,每次创建,虚拟机会为对象分配一块区域。

定义垃圾:和实际生活一样,没用用处的东西就叫做垃圾,对于Java虚拟机来说,如果这个对象不会被利用,那么这个对象就是垃圾对象。

为什么要回收:其实看完前面已经解释了为什么要回收,因为如果不回收,既不会被使用,而且还会占着内存空间,导致内存溢出,最终OOM异常,这是不允许出现的问题。所以需要对垃圾对象进行回收。在有些语言中需要使用命令、指令手动回收空间,如C、C++,一不小心就忘记回收或多次回收,都会产生问题。而在Java中使用的是垃圾回收器自动回收。

垃圾回收器如何找到垃圾?

有两种方式可以让垃圾回收器来寻找在到在堆中死亡(不能再被任何途径使用的对象)的对象。也就是对对象进行存活分析。

引用计数(Refrence Count)

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1,任何时候引用为 0 的对象就是不可能再被使用的。

这种方式即简单,效率又高,但现在的java虚拟机并没有采用这种方式,原因是这种方式有一个致命的缺陷就是无法找到循环引用的对象

根可达算法(Root Searching)

根可达算法根据一系列的GC Root 作为起点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。

在Java语言中,可作为GCRoots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/72762049.jpg

GC 算法

在对对象的存活分析后,知道了哪些对象已死亡,哪些对象还存活,会通过下面三种方法对死亡的对象进行清除。

标记清除(Mark-Sweep)

首先会为死亡的对象添加标记,然后在完成标记后,清除标记的对象。此算法为基础的清除算法,因为后面的算法都是基于此改进而来。

这种算法产生两个问题,一是效率问题,标记和清除两步的效率都不高,二是空间碎片化问题,在内存中产生大量的内存碎片

Untitled

拷贝算法(Copying)

将内存一分为二,每次分配内存只分配在第一块里面,将存活的对象顺序拷贝的另外一块中,然后将第一块中的对象全部抹除掉。这种算法实现简单运行高效,但是带来了一个问题就是内存的利用率不高,每次只能用一半。

Untitled 1

标记压缩(Mark-Compact)

和标记清除相似,同样是做标记,但是后续步骤却有大不同,标记压缩不同于标记清除,直接将死亡对象进行清除,而是在标记死亡的对象后,将存活的对象向一端移动,最后清理到端边界以外的内存。相当于将存活的对象压缩到顺序的内存中后,清理掉其他内容。

Untitled 2

垃圾回收器

现在JVM中一共有10中垃圾回收器,按照模型来分,可分为分代模型和不分代模型(.8之后)

分代模型

虚拟机的垃圾收集可能会采用“分代收集”(GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。根据不同的区域不同的特性使用不同的垃圾回收算法。

默认新生代与老年代的占比默认是1:2。新产生的对象默认会生成在新生代中,经过gc增加对象的年龄(age),当age超过一定数值时,将对象从新生代转移到老年代中。

新生代

因为在新生代产生的对象,大部分都会被回收掉,也就是说有很少的对象会存活下来,因此,新生代适合使用复制算法能够尽可能少的挪动活的对象,又能够尽可能快的清除死亡的对象。在这个基础上,来谈一谈新生代中的分区。

因为新生代的存活对象非常少,大概占比1/9,所以新生代的分区并不像上面介绍拷贝算法一样是均分为1:1,而是将新生代分为 Eden:survive1:survive2为 8:1:1。具体为何分为三个区域通过下面的图就能够了解:

第一次回收会扫描Eden区,将存活对象从eden区挪到S1中,清空Eden区,第二次会扫描Eden区和S1区,将存活的对象放到S2中,清空Eden和S1区,一次

Untitled 3

老年代

当对象经历了多次GC仍然没有被回收掉时,会被挪到老年代中,因为老年代中的回收次数和回收效率并不想新生代一样,经历一次gc只有少量的对象被回收掉。所以老年代采用标记整理算法,能够减少老年代中出现内存碎片的概率。

TLAB
多线程同时在堆中分配内存时,为了避免多线程冲突,操作同一地址,所以需要对整堆进行加锁,进而影响分配速度。TLAB 能够解决这个问题。
通过对 Eden 区域再进行划分, Thread Local Allocation Buffer(TLAB),这是 JVM 在eden区中为每个线程分配的一个私有缓存区域。多线程分配时,会优先在自己的TLAB中分配,分配不下再在Eden区中分配。

对象在内存中的生命周期

  1. 首先会尝试在栈上分配。在栈上分配的好处:执行速度快,不需要的对象直接弹出,不需要垃圾回收器的介入即可完成内存的释放。
  2. 小对象会现在Eden中的TLAB中进行分配,如果TLAB中分配不下会在Eden区域公共部分进行分配。
  3. 如果对象太大,无法在新生代找到足够长的连续空闲空间, JVM 会直接将对象分配到老年代。
  4. GC过程,GC过程可以分为对新生代、老年代的回收、和全部回收的过程。其中新生代的YGC(young GC)的发生频率最高。

Untitled 4

垃圾回收器的类型

在列举垃圾回收器的类型时,先强调几个Hotspot在垃圾器算法的实现:

  • OopMap:因为Hotspot在GC之前,查找内存中所有存活的对象使用可达性算法寻找死亡的对象,对于一个庞大的系统,每次都进行遍历查找引用是不可行的。因为在进行可达性算法的寻找对象时,需要将所有线程保持一个冻结的状态,Hotspot将这种状态成为”Stop The World“,也就是STW,来配合可达性分析,这样会停顿的时间会非常长。

    所以为了避免STW的时间过长,Hotspot使用一个OopMap的数据结构,让虚拟机知道哪些地址存放着对象引用。当类加载完成的时候,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

  • SafePoint:使用OopMap可以减少STW的时间,但是在运行过程中,因为引用关系随时会发生变化,OopMap的内容也要随之变化,但不是所有指令都会及时的更改内容,这时引入SafePoint的概念。

    程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

  • SafeRegion:SafePoint的扩充,停滞的线程,内容不会发生变化,所以这段代码区域内任何地方进行GC都是没有问题的。

JVM共有10中垃圾回收器,使用分代模型的共有六种,三种新生代的、三种老年代的,需要配对使用。

Untitled 5

一般的配对类型为:

  • Serial&Serial-Old 在暂停所有工作线程后,使用单个GC线程进行垃圾回收

Untitled 6

  • Parallel Scavenge & Parallel Old 暂停所有工作线程使用多个GC线程进行回收

Untitled 7

  • ParNew & CMS , ParNew和 Parallel Scavenge 其实在功能上是相同的,只是名字不同,CMS在回收老年代时,会经历下图中的三个标记阶段、一个清理阶段:
    • 初始标记
    • 并发标记
    • 重新标记
    • 并发清理

CMS无法清理浮动垃圾,只能等到下一次清理时才能清除。

因为CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量 空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有 很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,当遇到这种问题时,会通过Serial Old,也就是单线程使用标记-整理在进行垃圾回收,此时的回收效率非常低。

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”

Untitled 8

不分代模型

所谓的不分代模型指的是物理区域不分代,但是在逻辑上风染保留年轻代老年代的概念

G1

G1的出现是为了替代掉在1.5时发布的CMS垃圾回收器。

  • 并行与并发,和CMS相似,G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者 CPU核心)来缩短Stop-The-World停顿的时间。
  • 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其 他收集器配合就能独立管理整个GC堆。
  • 收集算法:CMS使用标记-清除算法,意味着会产生大量的空间碎片,而G1使用标记-整理,在收集后能提供规整的可用内存
  • 可预测的停顿:G1不像于分代模型中的收集器收集范围是整个年轻代或者老年代,在G1中产生一个概念叫做Region。G1会将堆分为大小相等的Region,在G1中所谓的老年代和年轻代就是不连续的Region区域。因为以上的特性,在G1中进行垃圾回收可以避免对整个区域回收,避免了停顿和增加了回收的效率。

Untitled 9

-------------本文结束感谢您的阅读-------------

本文标题:深入JVM(二)JVM垃圾回收器

文章作者:NanYin

发布时间:2020年05月15日 - 15:05

最后更新:2023年07月30日 - 17:07

原始链接:https://nanyiniu.github.io/2020/05/15/%E6%B7%B1%E5%85%A5JVM%EF%BC%88%E4%BA%8C%EF%BC%89JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。