1. 垃圾回收与内存分配
为啥要了解垃圾回收集和内存分配?
排查各种内存溢出、内存泄漏以及达到高并发的瓶颈时,需要对这些自动化的技术进行必要的手动调节与监控。
要解决的三个问题
- Who—哪些对象需要回收?
- When—什么时候回收?
- How—如何回收?
内存分配与回收的主要战场
线程共享的部分
- Java堆
- 方法区
因为太多不确定性,运行时才知道创建哪些对象,创建多少个对象。
而线程私有的部分,需要的内存在编译的时候就确定了,不需要过多的干预。
2. 对象你死没死?
1.引用计数算法
给予引用时+1,
引用失效时-1,
为0视为该对象已死
简单,但Java不用,因为需要考虑很多额外的情况,例如相互循环引用。
2.可达性分析算法
通过一系列GC Roots为根对象的树,从GC Roots出发,搜索对象是否可达(不可达的就是那些根不是GC Roots的树)
不可达的,就说明对象进入缓刑,再给一次执行finalize()方法的自我救赎的机会。
如果没抓住机会(上链),就执行死刑。

对象的引用
JDK1.2之前,对象有两种状态:
- 被引用
- 未被引用
之后,四种状态
-
强引用
不回收
-
软引用
内存实在不够了,再回收
-
弱引用
要回收的时候就回收
-
虚引用
肯定会回收,虚引用只是方便通知这个对象而已,其他没啥用,也没办法用这个引用找到该对象。
3. 垃圾回收算法
大体可分为两类:
- 引用计数式垃圾收集(直接垃圾收集)
- 追踪式垃圾收集(间接垃圾收集)
分代假说
- 弱分代假说:绝大多数对象都是朝生夕灭的(命短)。
- 强分代假说:熬过越多次垃圾回收的对象就越难以消亡(打不死的小强,老油条)。
- 跨代引用假说:老年代带新生代,新生代会逐渐晋升为老年代(师傅带徒弟)。
因此,Java堆要划分区域,以便更好的管理,同时兼具了垃圾回收的时间开销与内存有效利用。
-
新生代区域
每次只关注如何保留少量对象存活,而不是去标记大量将要被回收的对象(挑强壮的)。
-
老年代区域
较低频率去扫描(管得松点)。
3.1 标记—清除算法
人如其名,先标记,后清除,标记待清除对象或反之都行。
缺点:
-
执行效率不稳定
对象越多,效率越低
-
内存空间碎片化问题
3.2 标记—复制算法
可用内存,一分为二,一半用来使用,另一半用来存放存活下来的对象(复制过去)
优点:
- 解决了内存空间碎片化的问题
缺点:
- 如果大量的对象存活了下来,那么复制的开销会很大
- 太浪费空间,可使用空间只有原来的一半
现有一些解决方案,如“Appel式回收”,把新生代划分为Eden和两个Survivor,比例8:1。
但这也需要有“担保人”去担保(万一Survivor不够了,让担保人出资)。
3.3 标记—整理算法
与标记—清除算法类似,都需要先做标记。
不同的是,把要存活的对象移动到内存区域的一端,然后清除边界外的对象。
优点:
- 也解决了空间碎片化的问题
缺点:
- 存活对象多时,移动操作极为负重(移动时需要在这停顿)
移动还是不移动,各有优缺点。
-
移动 ,在当前垃圾回收时难,在以后内存访问时爽。(先苦后甜)
-
不移动,在当前垃圾回收时爽,在以后内存访问时难。(先甜后苦)
也有**“和稀泥”**的解决方案:先采用标记—清除的算法,等到空间碎片化过于严重时,再采用标记—整理的算法。
先忍着不整理,乱到不行了,看着难受了,再整理。
4. 经典的垃圾收集器
4.1 Serial收集器
类型
新生代收集器
最基础、历史最悠久的收集器。
- 缺点:“Stop The World!”
单线程工作,垃圾收集时,需要暂停其他所有的工作线程,体验太差。
工作模式
- 新生代采用:复制算法
- 老年代采用:标记—整理算法(Serial Old的工作)
应用场景
客户端模式下的虚拟机(分配管理的内存较小,停顿时间可以接受)。
4.2 ParNew收集器
类型
新生代收集器
就是多线程并行的Serial收集器版本。
应用场景
有可能会用到服务器端,因为可以和CMS配合着使用。
CMS收集老年代,ParNew收集新生代。
JDK9之后,ParNew就合并到CMS中了。
4.3 Parallel Scavenge收集器
类型
新生代收集器
也是多线程的,和ParNew类似
不同的是,别都是的关注缩小垃圾回收时的**“停顿时间”**
而它是关注如何达到一个可控制的**“吞吐量”**,拥有大局观
吞吐量=运行用户时间代码 / (运行用户代码时间+垃圾回收时间)
两个参数:
- -XX:MaxGCPauseMillis :垃圾回收时间的阈值
- -XX:GCTimeRatio : 吞吐量的倒数,默认99(1/(1+99)),即1%的垃圾回收时间。
4.4 Serial Old收集器
Serial收集器的老年代版本。
4.5 Parallel Old
是Parallel Scavenge收集器的老年代版本。
基于标记—整理算法实现。
4.6 CMS收集器(重点)
Concurrent Mark Sweep
目标
获取最短回收停顿时间(Stop The World!)
过程
基于 标记—清除 算法
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记:只标记GC Roots直接关联的对象。(Stop The World!)
并发标记:从GC Roots直接关联的对象开始遍历整个对象图的过程。(耗时较长)
重新标记:标记上一步过程中,用户操作导致标记变动的那部分对象的标记记录。(Stop The World!)
并发清除:清除判死刑的对象。(耗时较长)
优点
- 并发收集
- 低停顿
缺点:
-
对处理器资源非常敏感
处理器核心数量<4时,CMS对用户程序的影响十分的大。
-
无法处理浮动垃圾
-
会产生大量空间碎片
4.7 G1收集器(Garbage First)
创新一:Region堆布局
传统的GC是把Java堆分为固定大小的新生代和老年代
G1:
先划分为多个大小相等的Region,再根据需要,赋予每个Region不同的角色——化整为零
创新二:目标范围
Mixed GC
不再已分代为基准收集(新生代、老年代、或整个Java堆)——每次都是大扫除。
而是看:哪块内存垃圾多,回收的收益最大——能应付新对象分配就行了,不图每次都是大扫除。
创新三:建立可预测的停顿时间模型
因为G1把Region作为垃圾回收的最小单位,所以,每次回收的内存都是Region的整数倍。
这样就能有计划的避免在整个Java堆中回收垃圾。—需要多少收多少,没必要全部收集
创新四:优先级列表(Garbage First名字的由来)
根据Region的“价值”建立优先级列表,每次优先处理回收价值大的Region。
价值:可回收内存的大小以及回收时间的经验值
创新五:可以指定收集停顿时间
不能太大,也不能太小
太大很好理解,停顿时间太长,用户体验变差
太小的话,每次只能回收一点垃圾,时间长了,垃圾堆的越来越多,出现Full GC,得不偿失
运作过程
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
前三个和CMS类似
筛选回收:根据Region的优先级进行回收,采用的是“标记—复制”算法,把待回收Region中存货的对象,复制到空的Region中
因为要移动对象,所以要停顿**(Stop The World!)**
与CMS比较
不同:
收集算法不同:
G1:
整理看是:标记—整理(把存活的对象整理到空的Region区域)
局部看是:标记—复制(从一个Region复制到另外一个Region)
两者反正都不会产生空间碎片
CMS:
标记清除
G1优势:
- 可指定最大停顿时间
- Region的内存布局
- 按收益动态确定回收集(优先级列表)
- 将“行为”与“实现“进行分离
G1弱势:
- 内存占用高
- 额外执行负载高
本文探讨了垃圾回收与内存分配的重要性,涉及对象生存分析、引用计数与可达性算法,以及各类垃圾收集器(如Serial、ParNew、CMS和G1)的工作原理和优缺点。重点讲解了分代理论和不同收集器在并发、停顿时间和内存效率上的策略。
308

被折叠的 条评论
为什么被折叠?



