深入理解 JVM 垃圾回收机制
1, 垃圾回收需要解决的问题及解决的办法总览
1, 如何判定对象为垃圾对象
引用计数法
可达性分析法
2, 如何回收
回收策略
标记 - 清除算法
复制算法
标记 - 整理算法
分带收集算法
垃圾回收器
- serial
- parnew
- Cms
- G1
3, 何时回收
下面就是如何判定对象为垃圾对象
***
2, 引用计数法
在对象中添加一个引用计数器, 当有地方引用这个对象的时候, 引用技术器得值就 + 1, 当引用失效的时候, 计数器得值就 - 1
算法缺点: 当某个引用被收集时, 下个引用并不会清 0, 因此不被回收造成内存泄露.
下面我们运行实例代码来看, JVM 在循环引用时, 是否能被收集 (如果回收了就说明垃圾回收器用的不是引用计数法).
如果想打印日志信息, 请填入如下参数.
-verbose:gc -XX:+PrintGCDetails
其中我们需要将每个对象的所占内存扩大, 因此我们声明一个大点的空间.
测试实验代码如下:
- public class A {
- private Object instance;
- public A() {
- byte[] m = new byte[20*1024*1024];
- }
- public static void main(String[] args) {
- A a1 = new A();
- A a2 = new A();
- a1.instance=a2;
- a2.instance=a1;
- a1=null;
- a2=null;
- System.gc();
- //parallel 默认采用的垃圾回收器
- }
- }
运行结果如下所示:
- [GC (System.gc()) [PSYoungGen: 22446K->648K(37888K)] 42926K->21136K(123904K), 0.0011193 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- [Full GC (System.gc()) [PSYoungGen: 648K->0K(37888K)] [ParOldGen: 20488K->519K(86016K)] 21136K->519K(123904K), [Metaspace: 2632K->2632K(1056768K)], 0.0074751 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
- Heap
- PSYoungGen total 37888K, used 328K [0x00000000d6000000, 0x00000000d8a00000, 0x0000000100000000)
- eden space 32768K, 1% used [0x00000000d6000000,0x00000000d6052030,0x00000000d8000000)
- from space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)
- to space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)
- ParOldGen total 86016K, used 519K [0x0000000082000000, 0x0000000087400000, 0x00000000d6000000)
- object space 86016K, 0% used [0x0000000082000000,0x0000000082081fd8,0x0000000087400000)
- Metaspace used 2638K, capacity 4486K, committed 4864K, reserved 1056768K
- class space used 281K, capacity 386K, committed 512K, reserved 1048576K
这里我们会看到 22446K->648J 这里, 我们的对象被回收了, 这就说明我们 JVM 采用的垃圾回收算法并不是引用计数法.
3, 可达性分析法
算法如名, 可达性分析法就是从 GCroot 结点开始, 看能否找到对象.
GCroot 结点开始向下搜索, 路径称为引用链, 当对象没有任何一条引用链链接的时候, 就认为这个对象是垃圾, 并进行回收.
那么什么是 GCroot 呢 (虚拟机在哪查找 GCroot).
虚拟机栈 (局部变量表)
方法区的类属性所引用的对象.
方法区中常量所引用的对象.
本地方法栈中引用的对象.
目前主流 JVM 采用的垃圾判定算法就是可达性分析法.
至此垃圾判定算法结束
垃圾回收算法开始
4, 标记 - 清除算法
存在的问题:
效率问题.
内存小块过多.
如图所示: 黄色的就是被标记清除的. 清除后会发现有很多多余的小块.
5, 复制算法
下面是 java 内存常规划分
(线程共有) 堆内存 方法区
(栈内存 本地方法栈) 程序计数器
下面是堆内存的划分
新生代
Eden 伊甸园
Survivor 存活区
Tenured Gen 养老区
老年代
下面就是过程:
被标记的黑色就是需要回收的
将白色区域复制下面, 然后清空上面的
这样就完成了内存的连续分配, 但是引来一个问题.
每次只能使用一半的内存. 是不是有点少..
为了解决这个问题, 我们对内存就进行了划分.
我们对内存分为了三块区域.
内存区域 | 所占百分比 |
---|---|
Eden | 80% |
survivor | 10% |
Tenured Gen | 一点点 |
复制算法, 我们需要将上面的思路, 将 Eden 中需要回收的对象放到 Survivor, 然后清除.
也就是俩个 Survivor 中进行复制与清除.
这里我们即提高了效率, 又减少了内存分配.
如果 Survivor 不够放, 那就扔到老年代里, 或者其他方法, 反正有内存担保.
6, 标记 - 整理算法
复制算法主要针对新生代内存收集方法.
标记 - 整理算法主要针对的是老年代内存收集方法.
主要步骤: 标记 - 整理 - 清除
如下图所示
然后将右面的进行删除计科达到回收效果.
7, 分代收集算法
分代收集算法是根据内存的分代选择不同的算法.
对于新生代, 一般选择复制算法.
对于老年代, 一般选择标记 - 整理 - 清除算法.
显而易见, 这是上面俩种算法的优点糅合在一起的应用.
至此我们总结了所有垃圾回收算法.
下面就是各种出名的垃圾收集器
8,Serial 收集器
特点:
出现的最早的, 发展最悠久的垃圾收集器.
单线程垃圾收集器.
主要针对新生代内存进行收集
运行机制如下所示
缺点: 慢
用处: 在客户端上运行还是比较有效. 没有线程的开销, 所以在客户端还是比较好用的.
9,ParNew 收集器
特点:
由单线程变成了多线程垃圾收集器.
如果要用 CMS 进行收集的话, 最好采用 ParNew 收集器.
实现原理都是复制算法.
缺点:
性能较慢
10,Parallel Scavenge 收集器
主用算法: 复制算法 (新生代收集器)
吞吐量 = (执行用户代码消耗的时间)/(执行用户代码的时间)+ 垃圾回收时所占用的时间
优点: 吞吐量优化 (CPU 用于运行用户代码的时间与 CPU 消耗的总时间的比值)
关于控制吞吐量的参数如下
- -XX:MaxGCPauseMills #垃圾收集器的停顿时间
- -XX:GCTimeRatio #吞吐量大小
当停顿时间过小时, 内存对应变小, 回收的频率增大. 因此第一个参数需要设置的合理才比较好.
第二个参数值越大, 吞吐量越大, 默认是 99,(垃圾回收时间最多只能占到 1%)
总的来说: 客户端可用, 服务端最好不用.
11,CMS 收集器 (Concurrent Mark Sweep)
采用算法: 标记清除算法.
工作过程:
初始标记
并发标记
重新标记
并发清理
优点:
并发收集
低停顿
缺点:
占用大量的 CPU 资源
无法处理浮动垃圾
出现 ConcurrentMode Failure
空间碎片
CMS 是一个并发的收集器.
目标是: 减少延迟, 增加响应速度
执行效果如下所示:
初始标记
可达性分析法
重新标记
为了修正并发期间, 因对象重新运作而修正
并发清理
直接清除了
12,G1 收集器 (面向服务端)
最牛的垃圾收集器.
历史
-2004 年 Sun 发表了第一篇 G1 的论文, 到 2006 年左右, 在 JDK6 内集成进去了. JDK7 才放出来.
优势
集中了前面所有收集器的优点
G1 能充分利用了多核的并行特点, 能缩短停顿时间.
分代收集 (分成各种 Region)
空间整合 (类似于标记清理算法)
可预测的停顿 ().
步骤
初始标记
并发标记
最终标记
筛选回收
13, 小结:
至此我们就已经掌握了大部分 GC 的知识. 这可不是一个小工程, 希望要好好吸收知识..
来源: https://www.cnblogs.com/godoforange/p/11552865.html