基本就是《深入了解JVM虚拟机》的读书笔记,特点在于,我尽力说明了为什么这么设计。

1GC是什么及作用。

GC(Garbage Conllection(垃圾收集))当一个对象已经不会在使用了我们就需要及时的将他从堆内存中清楚防止堆内存的溢出,GC就是执行这个清楚垃圾的操作。

2GC需要清除哪些内容?

因为当引用指向对象时,其实引用保存的是一片内存地址,而JVM根据该内存地址找对象时找不到或找错就会出现一系列异常,所以GC只能清楚掉没有引用指向它的对象。那么我们该怎么设计堆内存空间?

3 分代收集理论

有三个根据经验的假说,确定了我们应该怎么分配设计堆内存
1)若分代假说(Weak Generation Hypothies)绝大多数对象朝生夕死。
2)强分代假说(Strong Generation Hypothies) 熬过越多次GC的对象就越是难以消亡
根据这两个假说,我们就应该将堆分为两部分,一部分用于存放朝生夕死的对象(新生代),一部分用于存放难以消亡的对象(老年代)。这样我们GC就可以先GC新生代,而不用每次都将整个堆内存进行扫描,当新生代老年代都满了之后在整个GC;这样无疑比每次都GC整个堆内存要提高很多效率。根据这个结论现在我们的堆内存应该是这个样子
image.png
3)跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说占比为极少数,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使新生代对象也难以消亡而变为老年代。根据这条假说,我们就不需要为了极少数的跨代引用取扫描整个老年代,只需要在新生代上建立一个“记忆集”,用于记录老年代哪块区域会存在跨代引用。现在我们的堆内存设计如下:
image.png

4.如何判断对象是否需要回收?

当堆内存占用到一定空间后,就需要进行回收,防止内存溢出。但是,程序运行时,许多对象是必须的,当有指针指向该对象时,我们便不能将其回收,不然会将出现许多异常,例如空指针异常。
目前我知道两种算法判断对象是否可以回收

4.1引用计数算法

在对象中添加一个引用计数器,当有一个地方引用它时,计数器加一,引用失效减一。当引用计数器为零时就可以标记为可删除。但是主流java虚拟机中并未采取这种方式管理内存。主要是有些特殊情况引用计数算法会比较难处理。例如,如果两个对象有引用互相指向例如

  1. public class testGCArithmetic{
  2. Obeact instance;
  3. public void test1(){
  4. testGCArithmetic A=new testGCArithmetic();
  5. testGCArithmetic B=new testGCArithmetic();
  6. A.instance=B;
  7. B.instance=A;
  8. }

在这种情况下他们的引用计数器都不可能为零,所以无法被GC回收

4.2可达性分析算法

就是通过GC Roots进行分析,凡是GC Roots直接引用,或者间接引用的对象(被GCroot引用引用的对象)就不可回收

4.2.1GC Roots

在java中一般作为GC Roots的对象一般为以下几类
1)在虚拟机栈(栈帧中的本地变量表)中引用的对象;
2)在方法区中类静态属性引用的对象
3)本地方法栈中引用的对象
4)Java虚拟机内部引用,例如基本数据类型对应的Class对象
5)所有被同步锁持有的对象
6) 其他,JVM运行所需的信息的对象

4.2.2强软弱虚引用

所谓强软弱虚引用就是提供判断这些引用在GC时行为的标志
1)强引用(Strongly Referance):就是传统的引用,例如Object A=new Object();只要GC Roots关联的强引用存在,GC就不会将其回收
2)软引用(Soft Referance):当还有足够内存时就不会被GC回收,直到要发生内存溢出时,就会先进行软引用的回收,如果还是内存溢出就会报内存溢出异常,一般用于缓存
3)弱引用(WeakReferance)一旦发生GC就将其回收感觉就是跳过GC Roots的检测
4)虚引用(PhantomReferance),形同虚设,就是没有这个引用.感觉和弱引用有点重复。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

4.2.3finalize()

当回收对象时,如果该对象重写了finalize()方法,并且没有调用过(如何判断这点?)。这些对象会放入一个名为F-Queue队列中,单开一个线程执行该队列的finalize()方法。如果这个对象的父类重写了呢?应该事和子类重写一样的效果

5.如何进行垃圾回收

由上面分析可知我们使用可达性分析算法来确定哪些对象需要回收,但是我们还是需要知道如何清除对象效率最高,下面为深入了解JVM虚拟机所提及的垃圾回收算法

5.1 标记-清除算法

这是最基础的垃圾回收算法,就跟名字一样该算法分为两部分:
1标记,将所有需要回收的对象标记出来,这里的标记就是将对象头的标记位改为11
这里的标记占用的是锁标志位,那锁在GC结束后要如何恢复?
2清除,遍历整个堆内存根据标记清除
这里有个注意点是为啥要先全标记,而非直接删除。如果一个对象中有多个引用,直接删除可能就会发生错误。
该算法有个两个主要的缺点
1效率问题:标记和清除两个过程的效率都不高。
2.内存空间碎片化,通过以下图解可以清楚的看到碎片空间为何产生
image.png
标记清除算法产生的大量空间碎片,空间碎片太多可能会导致以后程序运行过程种需要分配较大对象时无法找到足够大的连续内存来存储,当然可以维护逻辑内存,但这也是比较占用资源的操作。

5.2标记-复制算法

标记-复制算法简称复制算法,就是将内存分为容量大小相等的两块,每次使用其中的一块,当GC时将把存活的对象全部复制到另外一块上面,并且按顺序存放,这样不会存在内存碎片并且只需要移动堆顶指针,按顺序分配,然后把另外一个空间全部删除。这种算法的缺点也十分明显,可用内存缩小为原来的一半,空间浪费未免太多。
先说下堆顶指针,就是当哪个半区在使用时,那个半区就叫survivor1(幸存者),另外一个就叫survivor2.
然后是问题:按书中说法,这种复制过程效率会有提高,问题在于不是很懂这个对象复制过程,对象复制是个容易理解的操作,但所有指向该对象的引用如何随之改变,还是说引用只是保存的逻辑地址而非真实地址。如果是逻辑地址的话,这里的映射不会很复杂吗?不过目前也不是很理解逻辑地址和内存地址及映射的关系。

GC详解2 - 图4

还记得之前说过绝大多数对象都是“朝生夕死”么。既然绝大多数对象都活不过第一次GC,同时,为了节省空间我们可以把堆内存设计为以下模样(这个s1 s2应该是s0 s1,懒得的改了)
image.png
然后根据实验结果调整各个区域所占比例,最终内存设计如下(以前画的图,忽略了记忆集)。
新生代:老年代=1:2
Eden:S0:S1=8:1:1
image.png
这里我们将S0划分为这么小,就会引出另外一个问题,如果一个对象太大s0存放不下怎么办。如果溢出gc存活的对象超过了S0内存大小怎么办。也就是说在99%的情况下,不会超出这个内存,但当那个1%的情况发生时。我们需要一个处理办法。在这种情况下,这个大对象,或者这些超出内存的对象将会直接进入老年代。

上述GC机制也就是HotSpot的Serial(连续)、ParNew等新生代收集器的垃圾回收机制,进行了这些优化这种垃圾回收机制依然有着缺点,在GCRoot标记阶段,以及复制阶段需要暂停所有用户线程,防止GC过程中因为GCRoot的改变造成异常,但是由于复制算法的效率较高,这种MinorGC/YoungGC(年轻的GC)的STW(Stop The World暂停所有用户进程)时间相较于fullGC(堆GC)或MajorGC/OldGC(老年代GC)而言是可以接收的。当然后续还有更加高明的垃圾回收机制,

5.3标记-整理算法

上述垃圾回收机制针对于新生代的诸多特点设计,但并不适合老年代,因为老年代存活率很高,而标记复制算法需要将存活对象进行复制,且需要起码相当于存活对象空间两倍的空间。所以老年代一般不使用标记复制算法。
针对老年代对象难以消亡的特点有另外一种针对性的算法,标记-整理算法(Mark-Compact)其实就是在进行一次标记-清除算法之后,将移动存活对象,将其进行有序排列,以防止内存碎片过多。
image.png
但是移动存活对象并跟新所有引用这些对象的地方是一个极为负重的操作,并且这个操作需要暂停所有用户线程,却并不像之前的MinorGC那样时间可以接受,对于用户的体验是极大的破坏。于是MajorGC和fullGC成为一个需要极力避免或减少的情况。
这里的移动对象操作为什么会消耗时间远大于复制算法的复制操作 ,当然那个s1堆顶指针的移动的巧妙操作肯定会极大的提升效率,但感觉不应该有这么大的差距。同样是需要排序,和更新引用。也许是因为老年代存活对象远远多于年轻代的存活对象?
10.12:句柄感觉就是为了解决这个复制移动来进行的优化

这些垃圾收集器还有许多细节问题,但写不动了,见《深入了解jvm虚拟机》p81

6 几种较为先进的垃圾收集器机制

6.1CMS(Concurrent Mark Sweep)收集器

CMS收集器关注服务相应速度,希望stw的时间能尽量的短,带给用户更好的交互体验。
CMS收集器运作过程分为以下4个步骤:
1)初始标记(CMS inital mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
1)初始标记并不像前面GCRoots一样遍历整个相关的树进行标记,而是只是标记直接与GCRoots相关联的对象,这样由于需要标记对象很少,且GCRoots的Oop优化(详见《深入了解jvm虚拟机》p81),这个过程很快。2)而第二阶段并发标记根据第一步标记对象,标记整个关联树,耗时较长,但是由于这是个并发的过程,并不会造成STW,虽然会导致应用变慢,降低总吞吐量,但是整个过程对用户的体验影响较少。3)在前面说过STW的原因就是防止在标记过程中GCRoots的改变导致程序错误,CMS的解决方案就是记录在这个过程中GCRoots的改变,在标记结束后再重新进行一次校验标记,将并发标记阶段新关联的对象进行标记,这个过程是需要STW的,但是由于变化的GCRoots再大多数时候会较少,所以这个时间虽然会比第一阶段长,但也可以接受。4)并发清除就是将标记对象进行清除
这里还有很多的细节问题需要考虑,但写不动了详见《深入了解JVM虚拟机》p97

6.2