Java虚拟机(JVM) | 垃圾收集器与内存分配策略

一、概述

Java内存运行时区域的个部分中,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而亡,因此这几个区域就不需要考虑内存回收的问题。

而Java堆和方法区则需要考虑内存的回收。

二、对象已死吗

1、判断对象是否存活的两种算法

引用计数法

缺点:无法解决对象之间相互循环引用的问题

可达性分析算法

2、引用的分类

强引用

类似: Object obj=new Object() 这类引用,只要强引用还在,垃圾收集器永远不会收掉被引用的对象

软引用

用来描述一些还有用但并非必须的对象,JDK1.2之后,提供了SoftReferance类来实现软引用

弱引用

也是用来描述非必须对象,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。

虚引用

最弱的一种引用关系,虚引用的存在完全不会对对象的生存时间构成影响,也无法通过虚引用来取得一个对象的实例。

为一个对象设置虚引用唯一的目的是:在这个对象被垃圾收集器回收时,收到一个系统通知。

3、对象的自救

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选。

筛选的条件是此对象是否有必要执行finalize0方法。当对象没有覆盖finalize()方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

如果对象要在finalize()中成功拯救自己一一只要在finalize()方法中,让自己重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成
员变量,那在第二次标记时它将被移除出“即将回收”的集合。

但这种自救的机会只有一次,因为一个对象的finnalize() 方法最多会被系统自动调用一次

4、回收方法区

方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类。但判定一个类是否是“无用的类”的条件比较苛刻。

5、垃圾收集算法

标记-清除算法

分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成以后统一回收所有被标记的对象。

算法的不足:
1、标记和清除两个过程的效率都不高
2、会产生大量不连续的内存碎片

复制算法

将可用内存分为大小相等的两块,每次只是用其中的一块。当这一块内存用完了,九江还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

现在的商业虚拟机都采用这种收集算法来回收新生代,因为新生代中的对象98%是“朝生夕死”的,所以并不需要按照 1:1的比例来划分内存空间,而
是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块
Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

HotSpot 虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。

但我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion。

也就是如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时;这些对象将直接通过分配担保机制进入老年代。

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法只是根据对象存活周期的不同将内存划分为几块。一般是把Java
堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。

6、HotSpot的算法实现

枚举根节点

为了提高可达性分析中从GCRoots节点找引用链操作的效率

安全点

安全区域

垃圾收集器

1、Serial收集器

单线程收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

新生代采用复制算法,老生代采用标记-整理算法。

它是虚拟机运行在 Client 模式下的默认新生代收集器

2、ParNew收集器

ParNew收集器是Serial收集器的多线程版本,其他的行为控制参数和Serial收集器类似。

它是运行在Server 模式下的虚拟机中首选的新生代收集器,另外它也是除Serial收集器之外,唯一可以和CMS收集器配合工作的收集器。

3、Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个新生代收集器,它使用复制算法,也是并行的多线程收集器。

Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

4、Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记一整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurent Mode Failure时使用。

5、Parallel Old 收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。这个收集器是在JDKl.6中才开始提供的。

Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加 Parallel Old收集器。

6、CMS收集器

CMS(Concurent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器是基于“标记一清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(EMS initial mark)

  • 并发标记(CMS concurrent mark)

  • 重新标记(CMS remark)

  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快。

并发标记阶段就是进行GCRoots Tracing的过程。

而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标
记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器仍然存在三个缺点:
1、CMS收集器对CPU资源非常敏感。
CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。

2、CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

3、CMS是一款基于“标记一清除”算法实现的收集器,收集结束时会有大量空间碎片产生。

7、G1收集器

G1收集器具备如下的特点:

1、并行与并发: G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,Gl收集器仍然可以通过并发的方式让Java程序继续执行。

2、分代收集:与其他收集器一样,分代概念在G1中依然得以保留。

3、空间整合: G1从整体来看是基于“标记一整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。

但无论如何,这两种算法都意味着Gl运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。

4、可预测的停顿: 这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用Gl收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许
的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证子GF收集器在有限的时间内可以获取尽可能高的收集效率。

但如果一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达
生判定确定对象是否存活的时候,为了避免扫描所有的Java堆才能保证准确性这个问题。

在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。

G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象)。

如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,Gl收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)

  • 并发标记(Concurrent Marking)

  • 最终标记(Final Marking)

  • 筛选回收(Live Data Counting and Evacuation)

初始标记阶段仅仅只是标记一下GCRoots能直接关联到的对象,并
且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

并发标记阶段是从GCRoot开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Sct Logs里面。

最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值