深入浅出JVM(四)之垃圾回收算法
1.垃圾回收算法的分类
简单的说,在jvm中垃圾回收算法分为寻找垃圾算法(确认垃圾算法)和GC算法(垃圾收集算法)。
1.1寻找垃圾算法
1.就是要进行垃圾回收,如何判断一个对象是否可以被回收的算法。分为引用计数法和可达性分析算法
2.简单的说就是内存中已经不再被使用到的空间就是垃圾。
3.垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
1.1.1引用计数法
1. 给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象
2.但是主流的Java虚拟机里面都没有选用这种算法,其中最主要的原因是它很难解决对象之间相互循环引用的问题
1.1.2可达性分析算法
1.将“GC Roots” 对象作为起点,开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
2.简单的说,将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
1.1.3什么是GCRoots ?
1. 所谓 GC Roots 或者说 Tracing Roots的“根集合” 就是一组必须活跃的引用
2.GCRoots 包括:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(Native方法)的引用对象
1.2四大引用
1.如果一个变量的类型是 类类型,而非基本类型,那么该变量又叫做引用
2.从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期
3.这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用;引用强度依次逐渐减弱
1.2.1强引用(StrongReference)不回收
1.是指在程序代码之中普遍存在的引用赋值,可以直接访问目标对象,最常见的引用类型是强引用,也是默认的引用类型
2.无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象,虚拟机宁愿抛出OOM异常,也不会回收
3.强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象,强引用是造成Java内存泄漏的主要原因之一
1.2.2软引用(SoftReference)内存不足则回收
1.软引用是用来描述一些还有用,但非必需的对象,将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放
2.当内存足够: 不会回收软引|用的可达对象,当内存不够时: 会回收软引用的可达对象
3.软引用可用来实现内存敏感的高速缓存,高速缓存就有用到软引用,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
4.其他引用场景:软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从
1.2.3弱引用(WeakReference)发现即回收
1.弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
2.在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象
3.软引用、弱引用都非常适合来保存那些可有可无的缓存数据。
4.弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收
1.2.4虚引用(PhantomReference) 对象回收跟踪
1.虚引用(Phantom Reference),也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
2.一个对象是否有虚引用的存在,完全不会决定对象的生命周期,它不能单独使用,也无法通过虚引用来获取被引用的对象
3.如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收
4.为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程:能在这个对象被收集器回收时收到一个系统通知
5.由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虛引用中执行和记录。
1.2.5GCroots与四大引用的关系
1.3GC算法(垃圾收集算法 )
1.GC任务就是执行垃圾回收,释放垃圾对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存
2.目前在JVM中比较常见的三种垃圾收集算法分别是:复制算法(Copying),标记清除算法(Mark-Sweep)和标记压缩(Mark-Compact)算法
1.3.1复制算法(Copying)
1.为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块就是年轻代两个Survivor区,每次使用其中的一块。当这一块的
2.因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法
3.它浪费了一半的内存,这太要命了。
4.如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。
5.所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
1.3.1.1复制算法(Copying)的原理
简单一句说MinorGC的过程就是:复制->清空->互换,在新生代(年轻代)中使用的是MinorGC,这种GC采用的复制算法
1.eden,From区复制到To区,年龄+1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到From区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年代的标准,则移动到老年代区域),同时把这些对象的年龄+1
2.清空,eden区,From区
然后,清空Eden和From区中的对象,也即复制之后有交换,谁空谁是to
3.To区和From区互换
最后,To区和From区互换,原To区成为下一次GC时的From区。部分对象会在From区和To区域中复制来复制去,如此交换15次(次数可以调节)最终如果还是存活,就存入到老年代
1.3.2标记清除算法(Mark-Sweep)
1.顾名思义,主要进行两项工作,第一项则是标记,第二项则是清除。
2.标记:从引用根节点开始标记遍历所有的GC Roots, 先标记出非垃圾对象。
3.清除:遍历整个堆,把未标记的对象清除。
4.适用于存活对象比较多的情况下,效率高。老年代就是由标记清除或者是标记清除与标记整理的混合实现,
1.3.2.1标记清除算法(Mark-Sweep)原理
用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要非垃圾的对象标记一遍,最终统一回收这些没有被标记对象,完成标记清理工作接下来便让应用程序恢复运行。
1.此算法需要暂停整个应用,
2.效率问题,如果需要标记的对象太多,效率不高;需要进行两次扫描,严重耗时
3.空间问题,标记清除后会产生大量不连续的碎片。
1.3.3标记压缩(Mark-Compact)算法
1.它也分为两个阶段,一个是标记(mark),一个是压缩(compact). 其中标记阶段跟标记-清除算法中的标记阶段是一样的
2.压缩阶段:它的工作就是移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,
通过这样的方式来达到减少内存碎片的目的
1.3.1.1标记压缩(Mark-Compact)算法优点与缺点
优点:
1.标记压缩算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,同时也消除了复制算法当中,内存减半的高额代价
缺点:
1.从效率上来说,标记-整理算法要低于复制算法。
2.移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。 移动过程中,需要全程暂停用户应用程序。即:STW
2.分代收集算法
1.JVM使用分代收集算法,将堆内存划分为年轻代和老年代,两块内存分别采用不同的垃圾回收算法
2.这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法
2.2算法选择讨论
1.从内存效率上看:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
2.从内存整齐度上看:复制算法=标记整理算法>标记清除算法。
3.从内存利用率上看:标记整理算法=标记清除算法>复制算法。
4.可以看出,效率上来说,复制算法最优,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程,所以分代收集算法就出现了,也就是根据各个年代的特点选择合适的垃圾收集算法
2.3各代的收集原则
1.次数上频繁收集新生代(MiorGC) 2.次数上较少收集年老代(FullGC) 3.基本不动元空间(永久代)
2.4各代算法的选择
2.4.1年轻代(Young Gen)
1.年轻代特点是区域相对老年代较小,对像存活率低,所以可以选择复制算法
2.这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解
2.4.2老年代(Tenure Gen)
1.老年代的特点是区域较大,对像存活率高。
2.这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
3.Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。
4.Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。
5.Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。
6.基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
2.5空间分配担保机制
1.谁进行空间担保?
JVM使用分代收集算法,将堆内存划分为年轻代和老年代,两块内存分别采用不同的垃圾回收算法,空间担保指的是老年代进行空间分配担保
2.什么是空间分配担保?
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
3.为什么要进行空间担保?
是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor区无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。