Java虚拟机的垃圾收集器与内存分配策略

版权声明:转载或者引用本文内容请注明来源及原作者 https://blog.csdn.net/sky_format/article/details/88908669

垃圾收集器工作地点

在java虚拟机中的程序计数器、虚拟机栈、本地方法3个区域随线程而生,亦随线程二灭,当方法或线程结束时,内存就自动回收了,不需要考虑这几部分区域的回收机制。而在Java堆和方法区不一样,我们只有在运行时才知道创建了那些对象,这部分内存的分配与回收是动态的,而垃圾收集器关注的就是这部分区域。

判断对象是否‘死亡’算法

引用计数算法

其基本思想就是:向对像中添加一个引用计数器,每当有地方引用时,计数器值加一,引用失效时,计数器值减一,当计数器值为0时,代表其已经‘死亡’,可以被回收。
这种算法实现简单,效率也相对较高,但是其不能解决对像相互引用的问题,比如对像objA和objB有字段temp,objA.temp = objB; objB.temp = objA,除此之外,两者无其他引用,由于两者互相引用,导致计数器值永不为0,所以GC收集器也就无法回收这一部分内存。

可达性分析算法

可达性算法图
算法原理如图上,其实就像是简单的判断图的可达性一样,这种算法通过一系列的‘GC Roots’为起点,由这些节点向下搜索,搜索路径就是我们常说的引用链,当一个对象没有任何引用链到GC Roots时,我们就说对象已经‘死亡’。如图上白色方框的内容。

可作为‘GC Roots’的对象

  • 虚拟机栈引用的对象
  • 方法区静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

引用类别

  • 强引用
    类似object A = new Object(),强引用存在,就不会被回收

  • 软引用
    用来描述还有用但并非必须的对象。这类引用关联的对象,在发生溢出之前,会将这些对象列进回收范围之中进行第二次回收。

  • 弱引用
    被引用关联的对象只能生存到下一次垃圾回收之前,垃圾回收机制工作之时,就是被回收之日。

  • 虚引用
    这种引用完全不会对对象的生存时间造成威胁,也无法通过一个虚引用获取对象的实例,唯一的作用也就是在对象被回收的时候告知系统

垃圾收集算法

标记-清除算法

算法原理很简单,首先标记所有需要被回收的对象,然后在标记完成之后统一回收所有标记对象。
但其不足之处也很明显:效率低、内存碎片多。标记清除后产生大量的内存碎片,当需要分配较大对象时,由于无法分配足够的空间所以不得不触发一次垃圾回收动作。

复制算法

目的在于解决效率问题
基本思想就是将内存分成大小相等的两块,每次只使用其中一块,当这一块内存用完,将还存活的对象移植到另一块去,然后将使用过的内存一次性全部清理,使得每次只需要对半个区进行内存回收,内存分配时也不需考虑内存碎片的情况。
但将内存空间缩小了一半,代价不是一般的大

标记-整理算法

针对老年代情况
过程与‘标记-清除’算法一样,但后续步骤不是直接回收,而是让所有存活对象向一端移动,类似于数组的删除动作,需要将后续数据全部迁移形成一个紧密的整体一样,最后再清除端边界以外的内存区域。

分代收集算法

根据对象的存活周期不同将内存划分为几块,一般为新生代、老生代,根据年龄的特点采取最适当的收集算法。

垃圾收集器

垃圾收集器
图上半部分代表新生代的垃圾收集器,下半部分代表老生代的垃圾收集器,连线表示两者能够配合使用。

Serial收集器

元老级收集器,这个收集器是单线程收集器,关键是在进行垃圾收集时,必须暂停所有其他工作线程(‘Stop the Word’),直到收集工作结束,这就有些让人难以接受了。

ParNew收集器

Serial收集器的多线程版本,其他基本与Serial收集器一样。
但它是许多运行在Server模式下虚拟机的首要选择,因为可以与CMS配合工作(不得不说好的大腿至关重要啊0.0)

Parallel Scavenge收集器

新生代收集器,使用复制算法,还是多线程收集器。
对比ParNew,它的关注点在于可控的吞吐量(CPU用于运行用户代码时间与CPU总消耗时间比),可以高效利用CPU时间,尽快完成运算任务,适合后台运算不需要太多交互的任务。还有一个区别在于自适应的调节策略,虚拟机可以根据系统情况动态调节相关参数以提供最合适的停顿时间或最大的吞吐量。

Serial Old收集器

Serial收集器老年代版本,单线程,主要意义在于给Client模式下的虚拟机使用

Parallel Old收集器

Parallel Scavenge收集器老年代版本,使用多线程和‘标记-整理’算法

CMS收集器(Concurrent Mark Sweep)

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,基于‘标记-清除’算法实现,运作过程:

  • 初始标记
    仅仅只是标记GC Roots能关联的对象,速度快
  • 并发标记
    进行GC Roots Tracing的过程
  • 重新标记
    修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  • 并发清除
    CMS收集器运行图

以上图片来自于百度图片

CMS工作时间最长的并发标记和并发清楚过程收集器线程都可以与用户线程一起工作,整体上CMS收集器内存回收工作是与用户线程一起并发执行。
CMS收集器缺点:

  • 对CPU资源非常敏感
    在并发阶段,虽然不会因为用户线程停顿,但是会因为占用一部分线程导致应用程序变慢

  • 无法处理浮动垃圾
    由于CMS并发清理阶段用户线程还在运行,伴随线程运行自然有新的垃圾产生,这一部分出现在标记线程之后,CMS无法再当次手机中过处理它们,只好留到下一次GC处理,这一部分垃圾就称为‘浮动垃圾’

  • 空间碎片
    由于基于‘标记-清除’算法,会产生大量的空间碎片,将会给大对象分配带来大麻烦

G1收集器

对比CMS,有如下特点

  • 并发与并行
    使用多个CPU降低Stop-The-Word时间,可以通过并发的方式让Java程序继续执行

  • 分代收集
    分代概念在G1收集器中得到保留

  • 空间整合
    G1从整体上来看是基于‘标记-整理’算法实现的收集器,局部来看是基于‘复制’算法实现的收集器,运行期间不会产生空间碎片

  • 可预测停顿
    G1除追求低停顿外,还能建立可预测停顿时间模型,rag使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒
    G1收集器运行示意图
    G1收集器将java堆划分为大小相等的独立区域,虽然保留有新生代与老年代的概念,但两者已经不再是物理隔离,都是一部分Region(不需要连续)的集合。G1收集器跟踪各个Region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这种方式保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器的运作:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
    前两个步骤基本与CMS一样,在最终标记的时候,修正在并发标记期间因用户程序继续运行导致标记产生变动的那一部分记录,虚拟机将这段时间对象变化记录在Remembered Set Logs中,最终标记阶段需要将其中的数据合并到Remembered Set中,需要停顿线程,但是可以并行执行。

内存分配与回收策略

对象的内存分配,主要是在堆上进行分配,对象主要分配在新生代的Eden区上,如果线程启动了本地线程分配缓冲,将按线程优先分配在TLAB上。少数会直接在老年代中分配,分配细节主要取决于垃圾收集器组合、虚拟机与内存相关参数设置。

  • 对象优先在Eden分配
    多数情况下,对象在新生代Eden区中分配,当没有足够空间分配时,将发起一次Minor GC(新生代垃圾收集动作,频繁且速度快)【full GC/Major GC 老年代GC,经常伴随至少一次Minor GC,速度慢】

  • 大对象直接进入到老年代
    所谓的打对象就是需要大量连续内存的对象,典型的就是长字符串以及数组

  • 长期存活的对象将进入老年代
    为了区分新生代与老年代,虚拟机为每个对象定义了一个对象年龄计数器,如果对象出生在Eden并经过一个Minor GC仍然存活,并且能够被Survivor容纳,将被移植到Survivor空间,并且对象年龄设为1,Survivor空间对象每熬过一次Minor GC,年龄加一,年龄增加至一定程度(默认为15),就会被晋升到老年代中。

  • 动态对象年龄判定
    为更好适应不同程序的内存状况,虚拟机并不是永远达到阈值才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间一半,年龄大于或等于该年龄的对象就可以进入老年代。

  • 空间分配担保
    在发生Minor GC之前,先检查老年代最大可用连续空间十分大于新生代所有对象总空间,如果是,则minor GC确保安全,如果不成立,查看参数十分允许担保失败,如果允许,那么继续检查老年代可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,进行一次Minor GC,尽管有风险,如果不允许冒险,就改为一次Full GC。

猜你喜欢

转载自blog.csdn.net/sky_format/article/details/88908669