JAVA 垃圾收集算法,垃圾收集器与内存分配策略(内容全面,解析简单易懂)
垃圾收集器需要解决的三个问题:
1)哪些内存需要回收
2)什么时候回收
3)如何回收
背景:程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了,所以我们着重需要探究的是堆和方法区,因为他们是线程共享的,并且一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才会知道创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的内存就是这块内存
问题1:怎么判断哪些内存是需要回收的?(不能再被任何途径使用的对象就是需要回收的)
方案1:引用计数法
给对象添加一个引用计数器,每当有一个地方引用他时,计数值就加1,当引用失效时,计数值就减一,任何时候计数器为0就是不可能再被使用,该对象的内存需要回收
缺点:不能解决循环引用的问题!所以没有被当前虚拟机采用
方案2:可达性分析算法
通过一系列称为GC Roots的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径叫做引用链,当GC Roots到这个对象不可达,则证明此对象不可用,该对象的内存需要回收
可作为GC Roots的对象:
1)虚拟机栈中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中的JNI引用的对象(即Native方法)
引用分类
分类的目的:描述这样一类对象:当内存空间足够时,则可以保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象
具体分类:强引用,软引用,弱引用,虚引用
强引用:类似Object obj=new Object()这类的引用,只要强引用还存在,则GC永远不会回收掉被引用的对象
软引用:描述一些还有用但是非必需的对象,在系统将要发生OOM异常之前,GC将会把这些对象进行第二次回收,如果这次回收之后还是没有足够的内存,才会抛出OOM异常,JDK提供了SofeRefrence类来实现软引用
弱引用:描述非必需对象对象,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象,JDK提供了WeakRefrence类来实现弱引用
虚引用:为对象设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知
问题2:对象和GC Roots不可达时,对象有没有自救的办法?
要宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行可达性分析之后发现和GC Roots不可达,那么它将会被第一次标记,并且进行第一次筛选,筛选的条件是对象是否有必要执行finalizefan方法
当对象没有覆盖finalize方法或者finalize方法已经被调用过,虚拟机都不会再执行finalize方法,如果虚拟机执行了对象的finalize方法,那么对象将被放置在一个F-Queue的队列中,并在稍后由一个虚拟机自动建立,低优先级的的Finalize线程去执行它(这里的执行指的是虚拟机会触发这个方法,但并不会承诺等待它运行结束,因为如果一个对象的finalize在队列中执行缓慢,或者发生了死循环,将很有可能导致队列中其他对象处于永久等待,甚至导致整个内存回收系统崩溃)
finalize方法是对象逃脱死亡命运最后一次机会,稍后GC将对队列中的对象进行第二次标记,如果对象要在finalize成功拯救自己,只需要重新与引用链上的一个对象建立关联即可,那么他在第二次标记时将会被移除出回收集合,否则的话将会被回收
(任何一个对象的finalize方法都只会被系统执行一次!)
方法区的垃圾回收
方法区存储的是类的信息和常量等,GC在方法区中回收的就是废弃的常量和无用的类
判断废弃常量的:该常量没有任何一个对象引用它
判断无用的类:
1)该类的所有实例都被回收了
2)该类的类加载器已经被回收了
3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类1
垃圾收集算法
1.标记清除算法
首先标记出所有需要清除(回收)的对象,然后清除他们
缺点:1)效率问题,标记和清除两个过程的效率都不高
2)空间问题:标记清除之后会产生大量不连续的内存碎片
2.复制算法
将内存划为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收
缺点:1)代价太大,内存缩小为原来的一半了
2)效率问题;当对象存活率很高的时候,要进行很多的复制操作,效率低
该算法的应用:
商业虚拟机都是采用复制算法来回收新生代,不过划分的两块内存比例不是1:1,而是8:1(Eden:Survivor),因为新生代中的对象百分之98都是“朝生夕死”,当新生代内存空间不够时,我们采用内存分配担保机制:如果新生代上的空间不足存放上一次新生代收集存活下来的对象,这些对象将直接通过分配担保机制进入老年代
3.标记整理算法
首先标记常需要清除的对象,然后将不需要清除的存活的对象向一边移动,然后直接清理掉端边界以为的内存
4.分代收集算法
根据对象存活周期的不同将内存划分为不同的内存区,一般是划分为新生代和老年代,这样就可以根据不同代的特定采用不同代的垃圾收集算法,在新生代中,每次垃圾收集都有大量的对象死去,只有少量存活,那么建议复制算法,只需要付出少量对象的复制成本就可以完成,而老年代中因为对象的存活率高,没有额外的空间进行分配担保,就必须使用标记清理或者标记整理
垃圾收集器
1.Serial收集器(新生代收集器)
单线程收集器,在它进行垃圾收集时会暂停其他所有正在工作的线程,直到收集结束,这样会导致Stop The World时间太长,带来不好的体验,但是它简单高效,因为线程交互的开销,可以获得最高的单线程收集效率,在用户桌面应用场景中,分配給虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代停顿时间可以控制在几十毫秒或者一百毫秒以内,只要不频繁发生,还是可以接受的,所以Serial对运行在Client模式下的虚拟机来说是个很好的选择
Serial新生代采用复制算法
2.ParNew收集器(新生代收集器)
Serial收集器的多线程版本,除Serial收集器外,ParNew是唯一能与CMS收集器配合工作的收集器
PerNew新生代采用复制算法
3.Parallel Scavenge收集器(新生代收集器,吞吐量优先的收集器)
新生代收集器,采用复制算法,并行的多线程收集器
和ParNew的差别在于它的关注点不同,CMS等收集器关注的是尽可能的缩短用户线程停顿时间,而Parallel Scavenge收集器关注的是一个可控制的吞吐量
需要知道的是,GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的
GC自适应调节策略:虚拟机根据当前系统的运行情况收集性能监控信息,动态调整参数以获得最小停顿时间或者最大吞吐量
4.Serial Old收集器(老年代收集器)
Serial收集器的老年代版本,单线程收集器,采用标记整理算法
给Client模式下的虚拟机使用,可以在JDK1.5以及之前的版本中和Parallel Scavevge搭配使用,或者作为CMS收集器的后备预案
5.Parallel Old收集器(老年代收集器)
Parallel Scavenge收集器的老年代版本,多线程,标记整理算法
和Parallel Scavenge搭配使用,这样可以构成完整的吞吐量优先或CPU资源敏感的收集器
6.CMS收集器(以最短停顿时间为目的)(老年代收集器)
经典收集器,以获取最小的停顿时间为目标,常用于B/S系统的服务端
标记清除算法,不过过程比普通的标记清除更为复杂,可以分为四个步骤
1)初始标记(需要Stop The World):标记GC Roots能直接关联的对象
2)并发标记:进行GC Roots引用链搜索的过程标记
3)重新标记(需要 Stop The World):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分的标记记录
4)并发清除:并发的清除标记
CMS收集器的优点:
1)并发收集!!!
2)低停顿
CMS收集器的缺点:
1)对CPU资源敏感
2)无法处理浮动垃圾:CMS收集器和用户程序并行执行,在CMS收集器工作期间还会产生新的垃圾,叫做浮动垃圾,所以CMS收集器不会像其他收集器一样等到老年代几乎满了再进行收集,需要预留一部分空间提供并发的用户程序使用,默认老年代使用了百分之68就开始进行垃圾收集
3)内存碎片问题:可以通过参数设置进行内存整理,不过内存整理过程无法并发,碎片虽然没有了,但是停顿时间变长了
7.G1收集器(Garbage-First)(面向服务端,收集器前沿成果之一)(整个堆的收集器)
特点:
1)并行与并发:G1能充分利用多核环境下的硬件优势,使用多核来缩短用户线程停顿时间,部分其他收集器原本需要停下java线程去执行GC动作,G1收集器仍然可以通过并发的方式让java程序和GC动作一起继续执行
2)分代收集
3)空间整合:G1从整体来看是属于标记整理算法实现的收集器,但是从局部来看又是属于复制算法实现的收集器,无论怎么样,这两种算法都不会产生内存碎片,这样有利于程序长时间运行
4)可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时垃圾收集器的特征了
使用G1收集器时,java堆的内存布局和其他收集器有很大的差别,它将整个java堆多个大小相等的独立区域,虽然还有新生代和老年代的概念,但是新生代和老年代已经不是物理隔离的了,他们都是一部分Region(不需要连续)的集合
问题:G1收集器为什么能够建立可以预测的停顿时间模型?
因为G1收集器的堆的内存划分是一个个大小相等的块,它可以避免在java堆中进行全局的垃圾收集,G1收集各个Region里面垃圾的价值大小(回收所获得的空间大小和所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值大的Region
G1收集器的运作过程:
1)初始标记(需要Stop The World):标记GC Roots能够直接关联的对象,修改一些TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行时,能在正确可用的Region中创建对象
2)并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象
3)最终标记:(需要Stop The World):修改在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记
4)筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划
Full GC:在老年代中的GC动作
标记清除算法:CMS收集器,G1收集器
复制算法:Serial收集器,ParNew收集器,Parallel Scavenge收集器
标记整理算法:Serial Old收集器,Parallen Old收集器
新生代收集器:Serial收集器,ParNew收集器,Parallen Scavenen收集器
老年代收集器:Serial Old收集器,Parallen Old收集器,CMS收集器
整个堆的收集器:G1
垃圾收集器部分到此结束,我们探讨了这么久的内存回收,我们现在来看看内存分配的几条规则:
1)对象优先的Eden(新生代)中分配,如果当前Eden新生代没有足够的空间进行分配,那么将发起一次新生代的GC动作
2)大对象直接进入老年代(很长的字符串以及数组)
3)长期存活的对象直接进入老年代:每个对象有年龄计数器,每活过一次新生代GC,年龄计数器+1,当年龄到达一定阀值,对象直接进入老年代
4)动态对象年龄判定:虚拟机并不是永远的要求对象的年龄必须达到一定的阀值才能晋升到老年代,如果在Survivor老年代中相同年龄的所有对象的大小的和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入到老年代
5)空间分配担保机制:如果新生代上的空间不足存放上一次新生代收集存活下来的对象,这些对象将直接通过分配担保机制进入老年代
参考书籍:深入理解JAVA虚拟机