什么是垃圾?什么是分代收集?
虚拟机后面不会用到的东西就是垃圾。那虚拟机怎么判断后面会不会用到呢?这里就涉及到虚拟机的两个垃圾分代假说:
假说1:弱分代假说:大多数对象是朝生夕灭的,即新生代的对象有99%是活不过第一轮收集的
假说2:强分代假说:越熬过多次垃圾收集过程的对象,越不容易被收集。
基本上所有垃圾收集器都是遵循了这两个假说:收集器根据java堆划分出不同的区域,根据年龄(对象熬过垃圾收集器收集的次数)分配到不同的区域之中存储,即划分出新生代(eden from to)区和老年代,这样垃圾收集器就可以根据不同区域的特性进行不同的垃圾收集。
但是这也会出现问题,我们知道我们判断对象会不会被垃圾回收,是否应该存活,是通过可达性分析实现的。那么既然老年代中的对象不容易被回收,如果有新生代对象被老年代对象所引用,那么该对象理应不被收集,因此我们是否需要每次都要遍历老年代对象呢?如果每次都遍历老年代对象,那么我们分代的意义在哪里呢?这就涉及到第三条假说了。
假说3:跨代引用假说: 跨代引用相对于同代引用来说占极少数。
首先,通过假说1和2,我们推断,存在互相引用关系的两个对象,应该是同时生存或者同时消亡的,那么一个生存于新生代一个生存于老年代的情况就会很少见,即我们不会为了少量的跨代引用而去遍历整个老年代,这并不是说老年代引用就完全不管了,实际上会在新生代那边会建立一个记忆集,专门记录有哪些老年代对象存在跨代引用。只有这些老年代对象才会被遍历。
举例来说,我们知道gc root有四种,静态属性引用对象、常量引用对象、native方法引用对象、虚拟机栈引用对象,那么针对minor gc,是怎么进行垃圾分代收集的呢?
有对象A(老年代),对象B(新生代),对象C(新生代),对象D(新生代),对象D(老年代),对象E(老年代)
并且存在引用关系:
- GC Roots -> 对象A(老年代) -> 对象B(新生代)
- GC Roots -> 对象D(老年代) -> 对象E(老年代)
- GC Roots -> 对象C(新生代)
这样,当gc roots遍历到A时,就不会向下继续遍历到,而是去记忆集看A对象是否存在跨代引用,如果不存在就不继续找下去了,存在的话就继续扫描;虽然这样需要增加维护记忆集的成本,但是由于跨代引用较少,因此整体来说,比起需要扫描所有老年代对象,还是划算的。
三大垃圾收集算法
标记清除算法:
标记出所有需要回收的对象,然后对其进行收集/标记出所有存活的对象,收集没有被标记的对象。标记清除算法是最基础的收集算法,也简单易懂,但有两个显著的缺点:
- 执行效率不稳定
- 内存碎片化
标记复制算法
将内存分为大小相等的两块,标记出所有需要回收的对象,将活的对象放在另一块,然后对整块进行清除。缺点也很明显:内存减半,空间浪费大。这种方法由于是需要复制对象,因此是基本在新生代才会使用的方法(假说1,大多数对象朝生夕灭)。
标记整理算法
标记出存活的对象,然后向内存的一端移动,然后对剩余部分进行清理。是不是觉得和标记复制算法很类似?但是实际上是不同的,标记复制算法是非移动的,是复制;但是标记整理算法是需要移动的,是整理。
为什么这两点区别很大呢?因为在移动的时候,是需要stw操作的,不移动的话,复制大对象不划算,只是清除又会有内存碎片问题,即移动造成回收时候的麻烦,不移动会造成运行时的麻烦,这也是使用标记清除的cms有低延迟,而使用标记整理的parallel old有高吞吐的原因。
经典的垃圾收集器
新生代
serial垃圾收集器
标记复制算法
这是最初时用的垃圾收集器,单线程工作,这个“单”很厉害,在它工作进行gc的时候,它会直接stw,让用户莫名其妙的暂停五分钟。虽然这样说,但是实际上现在还是在经常使用,因为尽管它缺点看起来让人难以容忍,但是优点也是极其突出的:简单高效。越专心做一件事,那么做的就越好,和它的高性能相比,一点停顿实际上也是可以忍受的(在实际工作中,停顿时间可能也就几十毫秒,并不是完全不能接受)。
parnew垃圾收集器
标记复制算法
这是基于serial的多线程版本,它本身实际上和serial没有什么太大区别,只是用多线程而已,但是它有一个比较突出的优点:只有它能和cms一起使用。
parallel scavenge垃圾收集器
标记复制算法
这也是一款多线程的垃圾收集器,特别之处在于,其他垃圾收集器都是希望用户停顿时间较短,减少垃圾收集时用户线程的停顿时间,但是这款垃圾收集器的关注点在于达到一个可以控制的吞吐量,这两者不是一回事。
停顿时间缩短是有代价的,你把新生代的大小减小,那么每次gc肯定更快,这样停顿时间确实能缩短,但是带来的是gc次数会增多,实际上吞吐量也是在降低的。
老年代
serial old 垃圾收集
标记整理算法
与serial 对应,也需要stw
parallel old 垃圾收集器
标记整理算法
也是一款关注吞吐量的垃圾收集器,和parrllel scavenge配合使用能有不错的效果
cms 垃圾收集器
标记清除算法
这款垃圾收集器是一款追求低停顿的垃圾收集器,其垃圾收集过程分为如下四个阶段:
初始标记:标记gcroot能直接到达的对象
并发标记:从这些对象开始遍历整个对象图
重新标记:修正在并发标记阶段时产生的引用修改
并发清除:清除死亡对象
听起来很有规划,但是也没那么完美。在并发阶段是需要占用cpu的,会使用户程序变慢,降低吞吐量;并且也会造成内存碎片问题,尽管cms设定了当内存碎片达到一定程度时会触发内存合并,但是这样也会增加停顿时间,与初衷相违背。
特殊
g1 垃圾收集器
作为一款cms垃圾收集的替代者和接班人,g1的目标是能成为一款建立起“停顿预测模型”的垃圾收集器,为此,设计者们做出了一个思想上的转变,收集器不再面向代/堆,而是可以面向任何一个堆内存的部分来组成会收集,衡量标准不再是分代理论,而是哪块内存有垃圾,回收效果最佳。
g1垃圾收集器开启region布局,虽然也是用分代收集理论,但是代不固定,大小不固定,而是让region根据需要去扮演不同的eden\survivor\old空间。
g1的垃圾收集过程也分为如下四个阶段:
初始标记:标记gcroot能直接到达的对象
并发标记:从这些对象开始遍历整个对象图
重新标记:修正在并发标记阶段时产生的引用修改
并发清除:标记存活对象,并复制到空region中,并清除原region
与cms不同的时,g1除了并发标记阶段,都是要做stw的,因为g1并不是完全追求低停顿的垃圾收集器,还要保证吞吐量
Z垃圾收集器
zgc和g1一样 都是希望在吞吐量不错的情况下能够控制停顿时间,并且也是采用region布局,但是不同的是,region的大小和个数也是会变的。
zgc使用一种染色指针技术,即将信息标记在指针上,具体来说就是在指针上拿出4位来标记信息,通过这种方式,我们可以得知对象是否被移动过,引用状态等。
zgc的垃圾收集过程可以分为四个部分:
1 并发标记
这个阶段就类似于g1收集器 将所有需要存活的对象进行标记,这个标记是在指针上而不是对象上进行的。
2 并发预备重分配
zgc不会像g1那样根据每个region的垃圾量取舍进行扫描收集,而是会遍历整个空间所有的region,根据特定的查询条件判断这次收集过程需要清理复制那些region,并将这些region组成重分配集
3 将重分配集的region进行复制,复制到新的region上,并在原region上建立一个转发表来维护这个旧对象到新对象的转向关系
4 逐步将转向关系修改到对象指针,但是这个过程没有必要特意干一次,而是将这个过程放在下一次并发标记过程,清理结束之后就可以释放转发表