

深入JVM(二)JVM垃圾回收器
source link: 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/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

¶ 深入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方法)引用的对象。
¶ GC 算法
在对对象的存活分析后,知道了哪些对象已死亡,哪些对象还存活,会通过下面三种方法对死亡的对象进行清除。
¶ 标记清除(Mark-Sweep)
首先会为死亡的对象添加标记,然后在完成标记后,清除标记的对象。此算法为基础的清除算法,因为后面的算法都是基于此改进而来。
这种算法产生两个问题,一是效率问题,标记和清除两步的效率都不高,二是空间碎片化问题,在内存中产生大量的内存碎片。
¶ 拷贝算法(Copying)
将内存一分为二,每次分配内存只分配在第一块里面,将存活的对象顺序拷贝的另外一块中,然后将第一块中的对象全部抹除掉。这种算法实现简单运行高效,但是带来了一个问题就是内存的利用率不高,每次只能用一半。
¶ 标记压缩(Mark-Compact)
和标记清除相似,同样是做标记,但是后续步骤却有大不同,标记压缩不同于标记清除,直接将死亡对象进行清除,而是在标记死亡的对象后,将存活的对象向一端移动,最后清理到端边界以外的内存。相当于将存活的对象压缩到顺序的内存中后,清理掉其他内容。
¶ 垃圾回收器
现在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区,一次
¶ 老年代
当对象经历了多次GC仍然没有被回收掉时,会被挪到老年代中,因为老年代中的回收次数和回收效率并不想新生代一样,经历一次gc只有少量的对象被回收掉。所以老年代采用标记整理算法,能够减少老年代中出现内存碎片的概率。
TLAB
多线程同时在堆中分配内存时,为了避免多线程冲突,操作同一地址,所以需要对整堆进行加锁,进而影响分配速度。TLAB 能够解决这个问题。
通过对 Eden 区域再进行划分, Thread Local Allocation Buffer(TLAB),这是 JVM 在eden区中为每个线程分配的一个私有缓存区域。多线程分配时,会优先在自己的TLAB中分配,分配不下再在Eden区中分配。
¶ 对象在内存中的生命周期
- 首先会尝试在栈上分配。在栈上分配的好处:执行速度快,不需要的对象直接弹出,不需要垃圾回收器的介入即可完成内存的释放。
- 小对象会现在Eden中的TLAB中进行分配,如果TLAB中分配不下会在Eden区域公共部分进行分配。
- 如果对象太大,无法在新生代找到足够长的连续空闲空间, JVM 会直接将对象分配到老年代。
- GC过程,GC过程可以分为对新生代、老年代的回收、和全部回收的过程。其中新生代的YGC(young GC)的发生频率最高。
¶ 垃圾回收器的类型
在列举垃圾回收器的类型时,先强调几个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中垃圾回收器,使用分代模型的共有六种,三种新生代的、三种老年代的,需要配对使用。
一般的配对类型为:
- Serial&Serial-Old 在暂停所有工作线程后,使用单个GC线程进行垃圾回收
- Parallel Scavenge & Parallel Old 暂停所有工作线程使用多个GC线程进行回收
ParNew & CMS , ParNew和 Parallel Scavenge 其实在功能上是相同的,只是名字不同,CMS在回收老年代时,会经历下图中的三个标记阶段、一个清理阶段:
CMS无法清理**浮动垃圾,**只能等到下一次清理时才能清除。
因为CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量 空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有 很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,当遇到这种问题时,会通过Serial Old,也就是单线程使用标记-整理在进行垃圾回收,此时的回收效率非常低。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”
¶ 不分代模型
所谓的不分代模型指的是物理区域不分代,但是在逻辑上风染保留年轻代老年代的概念
¶ 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中进行垃圾回收可以避免对整个区域回收,避免了停顿和增加了回收的效率。
Recommend
-
42
-
82
JVM的GC经过多年的发展,大家对 Minor GC、 major GC的理解并不完全一致,所以我不打算在本文中使用这个概念。我把GC大概分为一下4类: Young GC:只是负责回收年轻代对象的GC; O...
-
62
前言 java相较于c、c++语言的优势之一是自带垃圾回收器,程序开发人员不用手动管理内存,内存的分配和释放完全由gc(Garbage Collector)来做,极大地提高了软件开发效率及程序健壮性(手动管理内存容易造成内存泄漏)。凡事皆有...
-
49
整理了JVM垃圾回收的一些问题 为什么Young Generation适合使用复制算法 一句话:因为YGen的特点是大批对象快速死去,仅有少量对象存活。对于复制算法来说,每次复制的内容并不多,成本较低。 为什么是复制...
-
25
Java 中的垃圾回收,常常是由 JVM 帮我们做好的。虽然这节省了大家很多的学习的成本,提高了项目的执行效率,但是当项目变得越来越复杂,用户量越来越大时,还是需要我们懂得垃圾回收机制,这样也能进行更深一步的优化。 <!--...
-
18
垃圾回收与内存分配策略 垃圾回收与内存分配策略 “垃圾”的定义 对象是否为“垃圾” 判断对象是否已成为“垃圾”的两种方法: 引用计数法 、 可达性分析算...
-
15
JVM 的自动内存管理,让原本应该是开发人员去做的事情,变成了垃圾回收器来做的事情既然是别人帮忙做的事情,那么可能就不是自己想要的,所以就需要我们了解一下垃圾回收相关的内容 引用计数法与可达性分析垃圾回收,垃圾回收,那就是有...
-
7
开往虚拟机的车已经出发,关注上车 那些回收 JVM 垃圾的家伙 ❝
-
9
背景 在Java中有一个很重要的概念,即 一切皆对象
-
5
介绍JVM内存回收机制。对什么区域进行垃圾回收? 栈是线程私有的,所有不进行回收。 查看GC回收情况:-XX:+PrintGC。 查看GC回收详情:-XX:+PrintGCDetails。 一、什么情况下回收? ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK