一: 如何找到垃圾对象
该对象没有其它对象对其进行引用,就可引申为垃圾。
算法:
1.引用计数法: 假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,那说明对象A没有引用了,可以被回收。
优点:
实时性较高,无需等到内存不够的时候才开始回收,运行是根据对象的计数器是否为0,就可以直接回收。
在垃圾回收过程中,应用无需挂起,如果申请内存时,内存不足,立刻报outofmemory错误
区域性,更新对象的计数器时,只是影响到该对象,不用扫描全部对象。
缺点:
每次对象被引用时,都需要去更新计数器,有一点时间开销。
浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
无法解决循环引用的问题(最大的缺点)
假设对象a与b相互引用,除此之外没有其他引用指向a或者b。 在这种情况下,a和b实际上已经死了,但由于它们的引用计数器皆不为0,在引用计数器的心中,这两个对象还活着。 因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄漏。
public class A{
public Object ref =null;
public static void main(String[] args) {
A a1 =new A();
A a2 =new A();
a1.ref = a2;
a2.ref = a1;
a1 =null;
a2 =null;
}
}
/*
*
*从上面的代码可以轻易地发现a1与a2互为引用,我们知道如果采用引用计数法,a1和a2将不能被回收,因为他们的 引用计数无法为零
*
*但是具体是为什么呢?已上图为例,当代码执行完line7时,两个对象的引用计数均为2。此时将myObject1和
*myObject2分别置为null,以前一个对象为例,它的引用计数将减1。若要满足垃圾回收的条件,需要清除
*myObject2中的ref这个引用,而要清除掉这个引用的前提条件是myObject2引用的对象被回收,可是该对象的
*引用计数也为1,因为myObject1.ref指向了它。以此类推,也就进入一种死循环的状态。
*
/
2.可达性分析:
下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象【不可达就是垃圾】
GC Root对象:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象 2.类的静态属性引用的对象
3.方法区中常量引用的对象 4.正在被Synchronize锁定的对象
5.存货的Thread对象 6.本地方法栈中的对象(native方法)
7.类加载器(单例模式),一个实例对象,不会被回收abstract
二: 垃圾回收算法
标记(其实标记的是存活的对象,特别注意)
1. Mark-Sweep(标记清除)
非移动式的
基于GC Root可达性分析扫描所有内存对象,进行标记,然后清理。
可以并发执行 (边清理、边创建对象)
如果标记的时候,不暂停应用程序,则刚新new的一个对象因为错过标记,则在清除阶段就会被回收;
清除完的内存是不连续的(内存碎片);效率低,递归全堆对象遍历,GC 的时候还需要STW。
2. Mark-Compact(标记压缩)
移动式的
整理:把存活的对象整理成连续的,不存在内存碎片。
效率低下,不可并发,正在整理的时候被标记。
算法的最终效果等同于标记一清除算法执行完成后, 再进行一次内存碎片整理【如此一来, 当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可】。
指针碰撞(Bump the pointer):如果内存空间以规整和有序的方式分布, 即己用和未用的内存都各自一边, 彼此之间维系着一个记录下一次分配起始点的标记指针, 当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上, 这种分配方式就叫做指针碰撞( Bump the pointer)
优点:
消除了标记一清除算法当中, 内存区域分散的缺点,而标记-压缩我们需要给新对象分配内存JVM 只需要持有一个内存的起始地址即可。
消除了复制算法当中, 内存减半的高额代价。
缺点:
从效率上来说, 标记一整理算法要低于复制算法。
移动对象的同时, 如果对象被其他对象引用, 则还需要调整引用的地址。
移动过程中, 需要全程暂停用户应用程序。即:STW
3. Copy
适合于Young区(朝生夕死)
1、将可用内存按容量划分为大小相等的两块 2、每次只使用其中的一块
3、当这一块的内存用完了,就将还存活着的对象复制到另外一块上 4、最后把已经使用过的内存空间一次清理掉
优点:
由于每次都是对其中一块进行内存回收,所以不需要考虑碎片问题;实现简单,运行高效。
缺点:
可使用内存缩小为原来的一半
4. 分代模型
新生代:大部分对象存活的周期很短,就会放在新生代中,98%的属于Young,俗称“短命鬼”,朝生夕死【Young= Eden(伊甸园 新生区) 区+两个 Survivor(安全) 区(8:1:1)】
老年代:长寿型对象,多次垃圾(15次)回收都没有回收掉的
永久代:方法区(methodArea),可以认为永久代就是放一些类信息的,骨灰级,不轻易发生改变,不频繁进行扫描
新生代 = Eden(伊甸园区)+2个suvivor区(采用复制清除算法)YDC
(1)新生代垃圾回收后,大多数对象都会被回收,未被回收的由eden进入suvivor0。
(2)再次进行新生代内存回收,未被回收的由 eden+suvivor0 进入到 suvivor1中
(3)再次进行新生代内存回收,未被回收的由 eden+suvivor1 进入到 suvivor0中
(4)再次进行新生代内存回收,未被回收的由 eden+suvivor0 进入到 suvivor1中
(5)当新生代中存在一直未被回收(15个周期;cms中6个周期)的对象,将由新生代进入老年代
(6)当新生代中出现了装不下的对象,那就进入老年代;
根据以上可以得出:
新生代大量死去,少量存活,采用赋值算法;老年代存活率高,回收较少,采用标记清除/标记压缩算法;
MinorGC/YGG:年轻代空间耗尽时触发
MajorGC/FullGC:在老年代无法继续分配空间时触发,新生代老年代同时进行回收。
三: 垃圾收集器
1. Serial+Serial Old
采用copy(新)+标记-整理(老),单线程串行,垃圾回收的时候,会STW(暂停用户工作),结束后,会唤醒用户工作。
2. ParNew
采用copy(新),并行收集器,Serial的多线程版本,也会STW(多核CPU的特性)
3. Parallel Scavenge+Parallel Old(JDK1.8默认)
Parallel Scavenge:并行收集器,与ParNew基本相同,更多关注吞吐量(=工作时间/工作时间+GC时间)
Parallel Old:采用标记-整理,
PS+PO 可配置时间
4. CMS(Concurrent Mark Sweep)
采用标记-清除,首次实现并发的GC,目标:最短STW停顿时间,发挥多核CPU并发运行。
工作流程:
1.初始标记:需要STW;极短,只查找GC Root第一层对象,速度快,必须STW
2.并发标记:GC标记线程与用户线程一起工作,耗时的GC追踪,因为用户不暂停,则会产生一些对象与GC Roots不可达
3.重新标记:需要STW,极短;针对2并发标记产生的变化,修正的工作,故需要STW(缺点)
4.并发清除:GC、用户一起工作
优点:
并发收集,低停顿。
缺点:
1.产生碎片问题
2.影响吞吐量
3.清除不干净,产生浮动垃圾(并发阶段,用户会产生新的垃圾,所以+Serial Old)
5. G1
Garbage First,JDK1.7.4后开始存在,不是默认,1.9开始默认。
首个打破了分代模型,采用Region代布局,适用于多核处理器、大内存容量的服务端系统。
可控的GC停顿,从整体来看基于标记-整理算法,从局部(Region之间)来看基于复制算法。
heap被划分为一个个相等的不连续的内存区域(regions),每个region都有一个分代的角色:eden、survivor、old(old还有一种细分 humongous,用来存放大小超过 region 50%以上的巨型对象),但是对每个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化(默认年轻代占整个heap的5%)。
G1从多个region中复制存活的对象,然后集中放入一个region中,同时整理、清除内存(copying收集算法)。
G1最大的特点就是高效的执行回收,优先去执行那些大量对象可回收的区域(region),根据优先等级,故可STW。
6. ZGC
JDK11开始,JDK15转正。