主要为对象, 而提到对象, 我们需要知道对象什么时候被回收? 主要是引用失效的时候, 那什么时候引用失效, 下面就要讲讲对象的四种引用了
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
Object obj = new Object();
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
SoftReference<Object> softRef = new SoftReference<>(new Object());
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);
另外还有一种引用是被动引用, 被动引用不会触发类的初始化, 因此也不会占用多余的内存空间, 这里不做扩展, 感兴趣的评论区留言, 可以看到我们实际使用中发部分都是通过强引用的方式来引用对象的, 因此了解垃圾收集算法和垃圾收集器对我们代码的性能优化非常重要, 这也是为什么大家经常看到有人说while里面别new对象了
垃圾收集器主要针对堆中的垃圾, 有人问为什么不清除栈的垃圾, 下图为jvm的内存分布图
在java中栈是线程私有的区域, 会随着线程的结束而释放, 而方法区的对象一般声明之后都是不可变的, 不会产生大量垃圾, 因此也不要清理,而推内存的结构如下
新生代分为
Eden区与Survivo区, Survivo区又分为From区和To区 (PS: 永久代, 方法区, 元空间这三者其实都是一样的作用, 不同版本实现不同,这里不做过多讨论)
引用计数法有点类似于redis中通过setnx实现分布式锁一样, 当有一处引用的时候计数器+1, 当引用失效的时候计数器-1, 当一个计数器为0的时候, 代表该对象已"死",
这种方法实现简单, 效率也高, 但是解决不了对象循环依赖问题, 比如下面代码, 将对象置空后, 仍然不会被清理掉
Student a = new Student();
Student b = new Student();
a.friend = b;
b.friend = a;
a = null;
b = null;
//强制进行gc之后,上述空间仍然存在
System.gc();
主流的jvm所采用的方法, 此算法的核心思想为 : 通过一系列称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为" 引用链 " ,当 GC Roots到一个对象没有任何的引用链相连时 ( 从 GC Roots 到这个对象不可达) 时,证明此对象是不可用的。以下图为例:
GC Roots可以是什么?
其实就是类似于大家家里有时候厨房有厨房专用的拖把, 客厅有客厅专用的拖把(不同的区有不同的垃圾收集器), 有时候垃圾少, 用扫帚扫一下就行了, 垃圾多的话, 扫帚扫完拖把拖, 饮料洒地上了,直接用拖把(不同的垃圾收集算法), 不同房间的打扫方式不同, 客厅的沙发下面可能很少拖, 而卧室的桌子下面只要打扫的时候就可能会拖(不同的区使用不同的算法),而不同的家庭对应不同版本的jdk, 有时候可能上个家庭天天都拖, 而下个家庭可能只偶尔扫一下, 大扫除的时候才拖
下面只讲述常见的垃圾收集器CMS和G1以及常见的垃圾收集算法标记-清除, 标记-赋值, 标记-整理法
有人问为什么都有个标记开头啊?能不能直接去掉啊, 可以理解为标记就是先找到正在使用的空间,后面打扫的时候, 避免误伤友军, 主意啊这里标记的不是要清理的垃圾 ,是仍在使用的对象啊
这是最基础的收集算法, 作用于老年代, 通常被CMS垃圾收集器所使用(还会被其他老年代收集器使用, 本文这里不做讨论), 分为标记和清除两个阶段
标记阶段:
清除阶段:
标记 - 清除 算法的不足主要有两个 :
1. 效率问题 : 标记和清除这两个过程的效率都不高
2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
"复制"算法是为了解决"标记-清理"的效率
问题。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,工作量变小了,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
作用于新生代区域, 这种算法虽然效率高, 但是会浪费一般的内存空间用于复制对象, 不适用于老年代, 因为老年代存活的声明周期长, 复制的对象多了之后, 会增加复制成本, 另外如果对象之间存在引用关系, 那么引用指针也需要及进行更新, 这也是额外的开销
类似于快慢指针, 作用在老年代中,复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为 " 标记 - 整理算法 " 。标记过程仍与 " 标记 - 清除 " 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存, 用伪代码描述大致如下
int[] arr = {1,1,0,1,0,0,1}; //1表示已标记的对象, 0表示要清理的对象, 2表示可用空间
int fast = 0;
int slow = 0;
for(fast = 0; fast < arr.length; fast++){
if(arr[i] = 1){
arr[slow++] = arr[fast];
}
}
for(int i = slow; i < fast; i++){
arr[i] = 2;
}
实现连接的收集器,表示可以设置新生代和老年代的垃圾收集器配合使用。比如Serial能和Serial Old配合使用,而不能和Parallel Old配合使用
特点:多线程,内存分区(各分区大小相同,同一分区逻辑角色相同,都是新生代等,回收以分区为单位,复制到另一个分区),首先回收垃圾最多的分区,低停顿,使用暂停预测模型。新老年代,整体标记整理,局部复制,独自工作
哪块内存中放的垃圾数量最多, 回收收益大, 就回收哪块区域, 这就是G1收集器的Mixed GC模式
G1收集器将堆内存划分为多个大小相等的heap区, 每个heap区都是逻辑上连续的一段内存(virtual memory). 其中一部分区域被当成收集器相同的角色(eden, survivor, old), 但每个角色的区域个数都不是固定的。这在内存使用上提供了更多的灵活性。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,那如果一个对象超过了整个Region容量,G1会怎么处理?答案是 G1会将这个超大对象存放在N个连续Humongous Region区域, G1的大多数行为都会将该区域作为老年代来对待
这种分Region进行清理的方式相较于整个堆内存清理, 清理效率更高, 相当于有两个保洁阿姨, 一个保洁阿姨是等你家里垃圾多了之后在进行打扫, 另一个是看到你家里哪个地方有垃圾, 就进行清理, 对于阿姨来说, 第一种阿姨轻松点, 但作为居住的我们肯定喜欢第二种阿姨, 不知道这样说大家有没有感觉到G1带来的优点
垃圾比例:G1 会监控每个 Region 中的垃圾比例,如果某个 Region 中的垃圾比例超过了一定的阈值,就会将该 Region 列入垃圾回收的候选列表。
空闲度:G1 也会考虑每个 Region 的空闲度,如果某个 Region 中的空闲度很高,可能会降低该 Region 被回收的优先级。
优先级:G1 会根据一些策略来确定哪些 Region 优先进行回收,通常会优先选择包含最多垃圾的 Region 进行回收,以最大程度地减少垃圾回收的成本。
问题产生原因
我们需要知道, G1进行可达性算法分析的时候, 判断的区域不是一整个堆, 而是一个Region一个Region来判断哪些对象需要清理的, Region只是一个逻辑上的内存区域, 当一个Region中的对象中包含另一个Region中的引用时, 这样G1也不知道引用的对象是否可达, 就会触发堆扫描, 自己亲自去看看, 被引用的那块区域到底是不是可达的
G1如何解决的
首先我们需要知道一个概念, 记忆集(Remembered Set), 什么是记忆集呢? 记忆集在存储上本质上是哈希表, key是Region的起始地址, Value是一个集合, 这个集合里面存储的元素是卡表的索引号. 集合里面的卡表为双向卡表(一般的卡表记录的是我指向谁, 双向卡表额外记录了谁指向我)
知道了数据集之后,当一个对象在一个 Region 中引用了另一个 Region 中的对象时,这个引用关系会被记录在 Remembered Set 中。当垃圾回收器需要扫描某个 Region 时,它可以通过 Remembered Set 快速定位到其他 Region 中可能存在的引用关系,告诉G1, 那块地方我在使用, 你别操心了奥, 从而避免堆扫描
触发条件
等到Eden区满了之后,会触发Minor GC。Minor GC同样也是会发生Stop The World的
具体步骤
所以,G1也有可能会存在「浮动垃圾」的问题。但是总的来说,对于G1而言,问题不大(毕竟它不是追求一次把所有的垃圾都清除掉,而是注重 Stop The World时间)
标记阶段完成后,G1就可以知道哪些heap区的empty空间最大。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
G1使用暂停预测模型(pause prediction model)来达到用户定义的目标暂停时间,并根据目标暂停时间来选择此次进行垃圾回收的heap区域数量.
需要强调的是, G1并不是一款实时垃圾收集器(real-time collector). 能以极高的概率在设定的目标暂停时间内完成,但不保证绝对在这个时间内完成。 基于以前收集的各种监控数据, G1会根据用户指定的目标时间来预估能回收多少个heap区. 因此,收集器有一个相当精确的heap区耗时计算模型,并根据该模型来确定在给定时间内去回收哪些heap区
4. 筛选回收(Live Data Counting and Evacuation) :负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根 据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的 存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程, 由多条收集器线程并行完成的。
一般来说,Mixed GC会选定所有的年轻代Region,部分「回收价值高」的老年代Region(回收价值高其实就是垃圾多)进行采集
被G1标记为适合回收的heap区将使用转移(evacuation)的方式进行垃圾回收. G1将一个或多个heap区域中的对象拷贝到其他的单个区域中,并在此过程中压缩和释放内存,基于标记-整理。
特点: 多线程,初始标记(单),并发标记(并发),重新标记(多线程,但与用户线程不并发),并发清除(并发,失败使用Serial Old),老年代,标记清除,可与Serial,ParNew搭配。
CMS (Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间
为目标的收 集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用 尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收 集器就非常符合这类应用的需求。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
清理步骤 :
GC Roots能直接关联到的对象
, 什么是直接关联的对象呢就是假把根对象的引用作为一个树, 树的第二层就是直接关联的对象, 再往更深的层次之后就是间接关联的, 还有年轻代中存有老年代引用的对象
标记-清除
算法,在这个过程中, 同样不会STW,仍然会有用户线程不断产生垃圾, 因此在这步中没有被清理掉的垃圾只能留到下次清理, 这些垃圾叫做"浮动垃圾"其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。(第一个,第三个步骤暂停,第二个,第四个并发执行)
问题 :
在步骤二中我们说过由于并发执行可能出现本该被清理的对象没有被标记, 不该被清理的又被标记了
, 被清理的对象没有被标记这个问题可以在重新标记得到解决, 但是不该被清理的被标记这一个问题该如何解决呢?
G1收集器是垃圾收集器理论进一步发展的产物,它与前面的CMS收集器相比有两 个显著的改进:
当然没有任何东西是完美的, G1也有以下缺点
用户程序运行过程中, G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。
以上的优缺点对比仅仅是针对G1和CMS两款垃圾收集器单独某方面的实现细节的定性分析,通常我们说哪款收集器要更好、要好上多少,往往是针对具体场景才能做的定量比较。按照笔者的实践经验,目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也会让对比结果继续向G1倾斜。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。