【JVM神秘大门】Java虚拟机原理保姆式教学,零基础速成GC机制(下篇)
本篇会加入个人的所谓鱼式疯言
❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言
而是理解过并总结出来通俗易懂的大白话,
小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.
🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!
引言
当我们谈论 Java 时,JVM 原理就像是一扇神秘的大门,通向程序运行的深层世界。在这扇门后,隐藏着无数的奥秘和惊喜,等待着我们去探索。让我们一起推开这扇门,揭开 JVM 原理的神秘面纱吧!
目录
-
Java的垃圾回收机制
-
怎么找出 “垃圾”
-
如何释放资源空间
-
JVM 的垃圾回收器
一. Java 的垃圾回收机制
1. 垃圾回收的引入
试想一下, 如果像之前学过的C语言当我们开辟一定的内存, 而忘记了 free 释放内存, 就很容易 让这一段内存空间无法利用, 而浪费掉, 从而出现 内存泄漏问题 。
那么在Java 中, JVM 就实现这样的机制: 可以让程序猿大胆的 new 对象 , 不用考虑内存泄漏问题, JVM 会自动的去释放内存中 == 已经不再使用的内存空间==。 而我们把这样的机制就称之为: 垃圾回收机制 简称为:
GC
。
2. GC的回收区域
我们知道在一块内存区域中, 最占内存空间的当然是对象, 而GC 的维度也是以 对象
为维度进行回收释放的。
这时我们就需要考虑一点, 竟然GC 是 以对象为维度的 , 那么我们可以思考下GC 的主战场是:
程序计数器和栈都是 随着线程的创建而创建的 , 只要 线程结束就自动销毁 的。 这里就不需要考虑。
而方法区一般是 存放静态成员变量的 , 创建的内存空间很小。 不需要去
GC
去释放。
而真正要
GC
释放的就是 主要存放着对象的堆
, 才是GC
的主战场。
是的,入上图, 不在使用的对象就会被GC 一点一点的回收 , 其中有一部分骑墙派(处在边界的) 也认为 不回收
。
虽然
GC
很方便 , 有利于开发人员的开发效率, 但是 GC 的创建和使用也是需要消耗一定的时间的 , 很有可能在程序 正在执行的过程JVM 进行GC 操作 , 就会产生STW
问题。 STW 就相当于 《黑神话. 悟空》 中的定身术,也就是说会使 程序出现卡顿 , 会额外产生
因为GC
的 额外时间消耗 。
所以在一些对时间要求比较高的场景, 例如C++ 的开发就没有用到 GC 机制
。
鱼式疯言
总而言之,言而总之:
GC 能 提高开发效率
, 但是 影响运行效率。
垃圾
是什么? 不是我们日常生活中的垃圾, 而是在 内存区域中不再使用的对象 , 我们称之为 垃圾
。
在JVM 实现 GC 机制的原理中, 我们需要探讨的两个过程:
-
怎么找出垃圾
-
怎么找到垃圾后释放
下面 让我们来学习学习吧~
二. 怎么找出 “垃圾”
关于怎么找出 “垃圾”
, 这里小编讲解两个平常开发中常用的两种寻找 “垃圾” 的方案:
引用计数法
可达性分析
1. 引用计数法
<1>. 原理流程
引用计数的过程就是:
给 每一个对象 分配一个 计算器count ;
如果一个对象 每添加一个引用了就 count++
, 每释放一个引用 count--
;
当这个对象的 count
为 0
时, 就说明 这个对象没有被引用 , 就把这个对象标记为 “垃圾” 。
<2>. 原理分析
如上图, 给每一个对象都分配内存资源, 只要 count为0 就释放该对象的内存资源。
但是小伙伴们有没有 考虑一点就是: 要给每一个对象都分配一个 count 这样的计数器。
但是如果对象很多呢? 是不是会消耗额外的很多内存资源来给计数器空间, 比如我只是一个
Integer
类型的数据, 但是却是用short
来计数, 就会额外消耗 2 byte 的内存空间。
并且有可能会出现两个对象互相引用的情况, 当出现这样的情况 计数器就可能 count 就不可能为 0 ;
所以上面的情况引用计数的说明问题:
消耗 额外的内存资源
出现 对象成环 的现象
出现像上面互相引用的情况, JVM 就没有采用这样的 查找 “垃圾” 的机制 。
所以下面小编就要介绍 =第二个排查垃圾 的方法: 可达性分析
2. 可达性分析
<1>. 原理流程
可达性分析是什么?
简单理解就是 点名
, 就是 班级上课的点名
比如现在班级的纪委有一个 班上所有人的名单 , 现在要开始上课了
纪委开始点名, 如果 这个对象答到了 , 就说明 这个对象被引用上了 , 不是 “垃圾” 。
如果这个 对象没有答道 , 就说明 这个对象没有被引用上 , 就标记为 “垃圾”
, 后面就需要被 释放内存空间 。
有了这些理解, 下面我们就可以正式讲解 JVM 的GC 的可达性分析 了。
TreeNode root = new TreeNode(a);
root.left = new TreeNode(b);
root.right = new TreeNode(c);
root.left.left = new TreeNode(d);
root.left.right = new TreeNode(e);
root.right.left = new TreeNode(f);
root.right.right = new TreeNode(g);
如上图, 这棵树的右子树断开连接了。
首先, 在JVM 中存有一份所有对象的名单 , JVM 会创建一个线程 , 让这个线程从 root 根节点开始遍历对象, 只要遍历过的对象, 就相当于喊到了 , 没有遍历到的对象就相当于没有喊到。
那么上图也就是 root.right = null , 那么 c, g 和 f 也就和 root 断开了, 没有被引用到 , 相当于 c , f , g 都没有被引用到 , 都视为
“垃圾”
, 这样的方式就称之为: 可达性分析。
鱼式疯言
栗子理解:
想以前的抗日战争, 在地下党中, 一般都是从 上级去联系下级的 , 一旦上级联系不到下级了, 为了保守组织安全 , 就会把没有联系到的视为已经被敌人给抓捕了, 也就视为
“垃圾”
, 那么换做我们的 可达性分析 也是如此。
<2>. 原理分析
虽然可达性分析不是很占内存空间 , 但是从
GC root
从上往下搜索, 如果对象很多的话, 就会 消耗大量的时间 。
所以一般而言 ,
GC root
会有很多个, 一般来自栈的局部变量
, 方法区的静态变量 , 常量池的引用 都可以作为 GC root , 并且准确来说并不是以上面树的方法来搜索, 而是以 图的方式来遍历 。
鱼式疯言
对于上述的两种查找 “垃圾”的方案。
Java
采用的 GC 是 可达性分析
像 Python
, PHP
等其他 编程语言的GC 采用的就是 引用计数法。
所以当我们谈及 GC
的时候, 一定要指明是 哪个编程语言 的 !
谈及完如何找
“垃圾”
,下面我们就谈谈找到垃圾之后, 如何去 清理垃圾, 释放内存空间的方案 吧 。
三. 如何释放资源空间
释放内存空间的方式很多种, 其中主流的有三种:
标记清除
复制算法
标记整理
分代回收
1.标记清除
所以如上图: 假设
JVM 已经通过可达性分析来确定了哪些对象是 “垃圾”
, 并做好的标志 , 所以接下来只需要 把标记好的对象进行释放 即可。
标记清除 虽然是一个不是很复杂的事情, 但是 释放完这段内存空间会不会造成新开辟的内存空间造成影响 , 这是我们需要考虑的问题 ? ? ?
答案: 会的
当我们把一段 需要释放的内存空间释放掉 之后, 就会使
整个内存空间不连续
, 就会使内存被 分割成一段一段的碎片 , 而我们通常把这种问题称之为: 内存碎片问题 。
内存碎片问题就很有可能会使 需要新开辟的内存申请不下来 , 就好比现有 3MB 的内存, 但是由于 内存碎片问题 , 当我需要申请2 MB 的内存时, 这2 MB 的内存中 有部分的内存是正在使用的 , 并
没有被释放掉
, 就会 申请失败 。
知道了内存碎片问题, 接下来的几个方案就是针对内存碎片问题所采取的。
2. 复制算法
如上图: 针对标记清除, 我们采取了复杂算法:
- 首先将整个内存空一分为二 , 左边为 正在使用的内存区域 , 右边为 剩余区域 。
- 然后将左边 没有标记为 “垃圾” 的对象 按
顺序的转移到右边
。
- 然后 左边一整块的区域全部释放掉 即可。
这里就有效的解决 内存碎片问题 ~
但是小伙伴们有没有思考过, 如果 剩余的对象很多 , 那么需要搬运的这些 对象所消耗的时间岂不是很大 , 那么造成的
STW
是很明显的。
3. 标记整理
标记整理 就好似顺序表的 删除操作 , 也就是每 GC 一次 “对象” , 就整理一次(像
顺序表
一样进行 挪动数据 ,是内存空间连续
)。
上述两种方案在 对象量级很大 的情况, 就会产生 大量时间的消耗
, 那么这最后一种方案就成为了 救世主
。
4. 分代回收
关于分代回收的过程就在上面这张图中展示着:
-
首先把整个堆区域分为三个部分:
伊甸区(新生区)
,幸存区
,老年区
。 -
把 年龄小的为新生代 放在
伊甸区
, 年龄中等的中年代 放在幸存区
, 年龄比较大的老年代 放在 老年区。 -
然后JVM 就进行对 伊甸区和幸存区 每轮GC 的调整:当新生代的对象经历过一轮GC 就会把不是 “垃圾” 的对象拷贝到幸存区, 此时
age + 1
。 -
而 幸存区分为两个相同的小区域 , 每一轮GC 就会进行上述的 复制算法 , 进行 逐步清除 。此时
age + 1
, 当 age 达到一定的程度之后,就会 调整到老年区 。 -
在 老年区的老年代对象 ,就可以认为是 经常被引用的对象 , 就不会轻易的释放,并且对象也比较
少
, 只需要偶尔的标记整理, 并不会产生 很大的时间消耗 。
上述过程可能小伙伴们还不是很能理解吧 , 下面小编举个咱找工作的过程举举栗吧 ~
分代回收就好比我们找工作流程:
伊甸区——》 幸存区 : 投简历(筛掉大部分对象)
幸存区——》 老年区 : 面试(经历 一面 二面, 三面,HR面) 多层筛选, 也会筛掉一部分对象。
老年区 : 成为正式员工(只是公司偶尔会考察, 筛掉很少的一部分对象)
鱼式疯言
补充说明:
其实分代回收的思想就是 前三种方案的结合 : 伊甸区和幸存区的复杂算法 , 以及 利用到老年区的标记整理
。
四. JVM 的垃圾回收器
如上图,分代回收只是GC 的思想, 但是在真正用于实现GC 的工作流程的还得是 垃圾回收器 。
但现在常用的 垃圾回收器 : G1
和 CMS
。
1. CMS
其中上图是
CMS
的 垃圾回收器 : 把 整个GC 都拆成多个阶段 , 能和业务线程
进行 并发执行就并发执行 , 经可能的 减少STW 的时间 。
2. G1
如上图这是G1 的结构图, G1 就是把整个内存分为很多块, 不同的颜色就代表
伊甸区
,幸存区
,老年区
。
进行GC 的时候, 不要求把所有的内存都回收掉 , 而是只回收一部分就好了, 让STW 控制在一定的时间内。
降低 STW 的影响,目前能将
Java STW
降到 1ms 以内。
总结
Java的垃圾回收机制: 初识 垃圾回收
的机制: 在 软件层面上实现自动回收不需要使用内存空间, 但是会容易造成STW 问题 。
怎么找出 “垃圾”: 两个找出垃圾的方案: Java 的 GC
利用的可达性分析 和 其他编程语言GC 利用的 引用计数 。
如何释放资源空间 : 介绍了五种释放资源空间的方案: 标记清除, 复制算法, 标记整理, 分代回收。 其中标记清除会产生内存碎片问题, 复制算法和标记整理会产生大量的 STW , 最终采用 前三种结合的分代回收 。
JVM 的垃圾回收器: 主要接收 CMS
和 G1
这 两种垃圾回收器 。
如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖
作者:邂逅岁月