GC与内存分配策略

一、GC

  第一步:判断对象是否已死?有两种方法:第一种是引用计数法,即给对象添加一个引用计数器,当被引用时,计数器就+1;当引用失效时,就-1;当计数器为0时,代表对象没有被引用。但是计数器的缺点就是:对象之间相互引用时导致计数器不为零,无法被回收。第二种方法是可达性分析法,即通过定义一系列的GC Roots对象作为起始点,从这些起点向下搜索,当一个对象到GC Roots没有任何引用链时,则此对象是不可用的。

    可以作为GC Roots的对象包括:虚拟机栈中引用的对象、方法去种类静态属性或常量引用的对象,本地方法栈中native方法引用的对象。

    这里再说明一下引用!!!

    引用分为 -->  强引用:把一个对象赋给一个引用变量,这个引用变量就是强引用。(强引用的对象不会被回收),当内存空间不足,Java虚拟机宁愿抛弃OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。强引用其实就是我们平时 A a = new A()这个意思。

          软引用:用来描述一些还有用但非必需的对象。(在系统将要发生溢出异常之前,将这些对象列入回收范围进行二次回收,如果回收之后还没有足够内存,才会抛出异常。利用SoftReference类实现软引用)。软引用可用来实现内存敏感的高速缓存。

          弱引用:用来描述非必需的对象,它的强度较软引用弱。(当发生GC时,无论当前内存是否足够,都会回收被弱引用关联的对象。利用WeakReference类来实现弱引用)

          虚引用:主要作用是跟踪对象被垃圾回收的状态,是最弱的引用关系。(PhantomReference类来实现虚引用)

  一个对象的死亡,至少要经历两次标记的过程:如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果对象没有覆盖finalize()方法或者finalize方法已经被虚拟机调用过,则没有必要执行。如果这个对象被判定为有必要执行finalize方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立一个低优先级的Finalizer线程去执行它。

  finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对象重新与引用链的任何一个对象建立关联,那在第二次标记时会将它移除“即将回收”的集合;如果对象没有逃脱,那么它就真的被回收了。

  回收方法区:永久带的垃圾收集主要回收废弃常量和无用的类。判断一个类是不是“无用的类”条件:该类所有的实例都已经被回收;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类方法。

  第二步:垃圾收集算法

    第一种是标记-清除算法(mark-sweep),算法分为标记和清除两部分:首先标记出所有要回收的对象,标记完成后统一回收所有被标记的对象。缺点:一个是标记和清除两个过程效率不高,另一个是标记和清除之后产生大量不连续的空间碎片,导致以后分配较大对象时找不到足够的内存而提前触发GC。

    第二种是复制算法(copying),它是将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完后,就将还存活的对象复制到另一块上面,然后再把已使用过的内存一次清理掉。优点:解决了内存碎片化,实现简单,运行高效;缺点:将内存缩小为原来的一半,这样代价较大。

    第三种是标记-整理算法(mark-compact),分为标记和整理两部分,首先标记出所有要回收的对象,将所有存活的对象都向一端移动,然后直接清理掉端边以外的内存。

    第四种是分代收集算法,就是根据对象存货周期的不同将内存划分为新生代和老年代,然后根据每个年代特性采用合适的算法。在新生代中每次都有大量对象死去,只有少量存活,那么就采用复制算法;而老年代对象存活率高没有额外空间对它进行担保,就必须使用标记-清除或标记-整理算法来回收。

  第三步:垃圾收集器

    Serial收集器,是单线程收集器,它进行GC时必须暂停其他所有的工作线程,直到它收集结束,这项工作实际是虚拟机在后台自动发起和自动完成的。

    ParNew收集器,就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为与Serial收集器一样。

    Parallel Scavenge收集器,是使用复制算法的新生代收集器,又是并行的多线程收集器。它的目的是达到一个可控制的吞吐量,高效率地利用CPU时间, PS收集器提供了控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数和直接设置吞吐量大小的 -XX:GCTimeRatio参数用于精确控制吞吐量。(PS收集器还有一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,这个开关打开后,就不用手工指定新生代的大小、Eden和Survivor区的比例、晋升老年代对象的大小等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息动态调整这些参数,这种调节方式称为GC自适应的调节机制)。

    Serial Old收集器,是单线程收集器,使用标记-整理算法,是Serial收集器的老年代版本,是给Client模式下的虚拟机使用。

    Paralled Old收集器,是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

    CMS收集器,是一种以获取最短回收停顿时间为目标的收集器,使用多线程和标记-清楚算法。

      运作过程分为:初始标记(仅仅只是标记一下GC Roots能直接关联到的对象)

             并发标记(进行GC Roots Tracing 的过程)

             重新标记(为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录)

             并发清除(清除GC Roots不可达的对象,不需要暂停工作线程)。

      优点:并发收集、低停顿;

      缺点:CMS收集器对CPU资源非常敏感、CMS收集器无法处理浮动垃圾、基于标记-清除算法会产生大量的空间碎片。

    G1收集器,是一款面向服务端的垃圾收集器,G1具备并行与并发、分代收集、空间整合(基于标记-整理和复制算法实现,不会产生空间碎片)、可预测停顿的特点;G1收集器中它将Java堆划分为多个大小相等的独立区域,独立使用和回收,这样可以控制一次回收多个区域,减少一次GC所产生的停顿(区域划分机制)。G1之所以能建立可预测的停顿模型,是因为G1跟踪各个区域里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域(优先区域回收机制)。为了避免全堆扫描,G1使用Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

      运作过程分为:初始标记(仅仅只是标记一下GC Roots能直接关联的对象,并修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的region中创建对象,此阶段需要停顿线程,但时间很短)

             并发标记(从GC Root开始对堆中的对象进行可达性分析,找出存活的对象,耗时长,但可与用户线程并发执行)

             最终标记(为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录并把Remembered Set Logs的数据合并到Remembered Set 中,此阶段需要停顿线程,但可并发执行)

             筛选回收(首先根据各个region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划)

    并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

    并发(Concurrent):指用户线程与垃圾收集线程同时执行。

二、内存分配与回收策略

  大多数情况下,对象分配在新生代的Eden区上,分配规则是不固定的,取决于当前使用的垃圾收集器以及虚拟机中与内存相关的参数设置。

  新生代:用来存放新产生的对象,占用1/3空间;新生代分为一个Eden区和两个Survivor区(FromSurvivor和ToSurvivor);当Eden区没有足够空间进行分配时,虚拟机将会进行一次Minor GC。进行一次Minor GC后的对象进入FromSurvivor区,当From和Eden区没有可用空间时,会进行第二次Minor GC,将存活的对象移到ToSurvivor区。

  大对象(需要大量连续内存空间的对象)直接进入老年代;虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做避免了在Eden区及两个Survivor区之间发生大量的内存复制。长期存活的对象将进入老年代,如果对象在Eden区出生并且经过第一次Minor GC后仍然存活,将被移动到Survivor区,并且对象年龄设为1。对象在Survivor区中每经过一次Minor GC,年龄就+1,当它的年龄增加到一定程度时(默认为15),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

  长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC,那么对象会进入Survivor区,之后每经过一次Minor GC,那么对象的年龄加1,直到达到阈值,对象进入老年代。

  动态对象年龄判定:如果在Survivor区中相同年龄的所有对象大小总和大于Survivor区的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

  空间分配担保:当出现大量对象在Minor GC后仍然存活就需要老年代进行分配担保,把Survivor区无法容纳的对象直接进入老年代中。前提是老年代本身有容纳这些对象的剩余空间。

  在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总大小或者历次晋升到老年代对象的平均大小,如果大于就进行MinorGC,否则将进行FullGC。

面试题

说一下Java的垃圾回收机制?

  它使得Java程序员在编写程序的时候不再需要考虑内存管理。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会运行。

  垃圾回收机制可以用3个词来概括:where,when和how?

  where:运行时的内存分布情况;

  when:对象何时需要被回收?也就是何时回收无效对象,已死对象的?

  (答案见上面内容)

 

posted @ 2019-12-05 22:39  MrHH  阅读(...)  评论(...编辑  收藏