差不多就是这个样子,把被引用的次数记下来,当引用为0
时,就判定是垃圾。
不过存在一个问题,那就是对象引用对象的时候,这个办法就失效了。
class Node{
Node prev;
Node next;
int value;
}
public static void main(String []args){
Node head = new Node();
Node tail = new Node();
head.next = tail;
tail.prev = head;
head = null;
tail = null;
}
嗯,直白的看出来,这两个都是垃圾了,但是使用这种办法并不能判别这种情况的垃圾。
甚至不能够识别自引用
核心思想:引用源头gcRoot
必须是有效引用,而不是对象。
这样就避免了对象自引用的蒙蔽视听。
就像我们所说的伴生
,或者说链表,我们只需要能够索引到head
,双向链表的话,任意node
都可以。
这样哪怕只有一个保持引用,就不会是垃圾,但是不可达
的话,就必定是垃圾。
显示垃圾回收信息
-verbose:gc
:简单信息-XX:PrintGCDetails
:详细信息
前一篇说指针碰撞存在一个问题,就是空间不连续。
就是这样,分别对对象打上标记,然后分别对特征对象进行垃圾回收。
不过这里存在两个问题:
多个空间,把存活的对象转移到另一个区域当中,然后全部清理当前区域。
这样一来,就只是对象复制
的问题,不存在单个对象清理,同事变相的实现了空间压缩。
不存在空间固定大小的问题。
JVM
是这样划分的,首先对象创建都在伊甸园Eden
,然后垃圾回收一步步复制到Survivor
中。
这就是JVM
的新生代,同样的,还有老年代。
Eden
回收时,就会把存活对象复制到Surivor
,但是使用时只使用一个。
Survivor
中也会进行垃圾回收,向另一个Surviro
中进行复制,重复一定次数后,就迁移至老年代。
param | example | description |
---|---|---|
-XX:NewRation |
4 |
新生代:老年代( 1:? ) |
-XX:SurvivorRatio |
8 |
survivor*2 :Eden (2:? ) |
复制算法的确很好,但不得不说存在一个很大的弊端,那就是
空间不复用
。不断的递进,但是不可能存在无限的内存。
和标记清除对比:整理过后再清除,空间连续
和复制算法对比:复用空间,但是操作麻烦
复制转整理
- 复制算法需要更多的空间,但是节省时间。
- 标记整理节省了空间,但是要花费更多时间。
单方向来说,复制算法没有终点,似乎更偏向于标记整理。
但是可以如果回环的复制,就能得到标记整理的效果。
两个
Survivor
相互复制,就能够得到整理
后的连续空间,就是如此。
梳理一下
考核一下对象的状态
于是分代收集算法出现了
分代 | 算法 |
---|---|
新生代 | 复制算法 |
老年代 | 标记整理 |
通过复制算法,快速的回收新生代对象,并转移到老年代。
使用标记整理,应对回收率低的老年代,并为复制算法的空间开销做个终结。
分代收集,就是划分层次,然后特定处理。
Serial
红色:用户线程
绿色:垃圾回收
虚线:挂起
实线:运行
Serial
是单线程的垃圾回收器,使用单个线程完成垃圾回收操作。
在垃圾回收期间,其他的用户线程会被挂起,垃圾回收和用户线程是互斥状态,不能够并行运行。
采用的算法为复制算法
,属于新生代收集器。
parnew
相同点:
parallel
parallel
和parnew
大体相同,关键增加了垃圾回收时吞吐量的控制。
吞吐量 = 用户代码执行时间 / (用户代码执行事件 + 垃圾回收执行时间)
因为垃圾回收和用户时间互斥,所以总时间就是两者之和。
params | description |
---|---|
-XX:MaxGCPauseMillis |
垃圾回收停顿时间 |
-XX:GCTimeRatio |
吞吐量大小 ( 0 , 100 ) (0, 100) (0,100) |
默认吞吐量是最大的99(%)
,也就是说用户代码执行时间:垃圾回收占用事件 = 99 : 1
。
同时,还可以调节垃圾回收的停顿时间,不过这里有一个误区:并不是停顿时间越小越好
步子小,相同的路程同样的时间,就只能提高频率了。
所以过小的停顿时间,势必频繁的触发垃圾回收,说不上就是好东西,但是算得上精准
。
过大的停顿时间,不仅降低用户体验,如果没那么多垃圾,也只是浪费资源。
cms
并发与并行
并发:同时在做多件事,来回切换
并行:同时在做多件事,同时执行
就好比有三个跑道,同时跑的话:
并发:一个人跑,在跑道间来回切换,最终跑完三个跑道
并行:三个人跑,每个人负责一个跑道,最终跑完三个跑道
电脑同时运行多个软件程序,是我们并行的错觉,底层仍然是
CPU
快速的并发切换。追根究底,是
执行者
数量的限制。多核,意味着
多CPU
,这才是真正的并行,不过运行时是单CPU
并发,还是多CPU
并行,并看不到。
图中的直观感受:在某些时刻,垃圾回收和用户代码执行是并发的进行的,而不是独占的阻塞。
相对来说,垃圾回收时的停顿时间是有所下降的,并发的步骤,并不阻塞用户的代码执行,这是它的优点。
对应的,有如下缺点:
CPU
资源:并行操作抢占CPU
Concurrent Mode Failure
:回收时空间独占,新对象需到指定空间进行分配,空间不足会异常Concurrent Mark Sweep
,采用标记清除,会造成空间碎片耗费资源,它是老年代的垃圾回收器,不是新声代的菜。
不过老年代使用它的话,新声代一般配合使用parnew
。
G1
分代回收是在原来的基础上,加上了对象的存活时间的特征区分,然后分别采用高效回收方式。
对于G1
而言,采用分区
来替换了分代
。
因为分代有一个明显的瑕疵:分代区域划分明显。
即使是老年代,也有某个时刻需要大量回收的场景,而新对象存活时间长也需要进行反复复制。
这个时候,针对分代年龄进行的特定算法,就不再那么特定了。
G1
中对整块内存进行分区,分而治之,并记录每个区域的可回收数量,然后特定的进行垃圾回收。
也就是说,对于相同的区域,跟随时间变化,它可以即是新生代,也可以是老年代。
由新生代变为老年代的过程,并不需要进行移动操作。
步骤:
在分代中,是通过固定的内存区域进行对象年龄的划分,引出了分代的概念,但是不一定能够很好的判别。
G1
中进行多次标记,最终筛选回收
,通过回收数量来判定区域的分代年龄,更加精确。
优势:
追根究底,在于对分代
的准确判定,抛弃固定分区进而更加的精准操作。
加上详细分区,虽然是老年代或者新生代,但针对的不再是大块的区域,小范围管理方便且快速。
Serial
:复制算法,单线程parnew
:复制算法,多线程parallel
:复制算法,多线程,提供吞吐量控制cms
:标记清除,并发执行G1
:准确分代,取长补短