Java垃圾收集器与内存分配策略

一、GC的历史

GC(Garbage Collection)垃圾收集,该技术并非Java语言的伴生技术,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。而Lisp在胚胎期时人们就在思考GC需要完成的事情。

二、GC需要完成的事情

  • 那些内存需要回收?
  • 什么时候回收?
  • 如何回收?

三、为何要了解GC和内存分配

排查各种内存溢出、内存泄漏问题,当垃圾收集成为系统高并发的瓶颈时如何优化。

四、Java虚拟机GC的主要区域

Java堆和方法区。

五、如何判断对象是否可回收

如何判断对象是否可被回收

Java堆中存放的几乎所有的对象实例,如何判断那些对象还”存活“,那些已经“死去”,这是进行垃圾回收的关键。
判断对象是否可被回收的算法有两种:
1.引用计数算法
引用计数算法的原理:是通过在对象中添加一个引用计数器,通过对计数器的加减来记录引用和引用失效,当计数器归零时就是指对象不能再被引用,引用计数算法实现简单,判定效率也很高,但是很难解决对象之间相互循环引用的问题,所以Java虚拟机中并没有选用引用计数算法来管理内存。
2.可达性分析算法
可达性分析算法的原理:是通过一系列称为”GC Roots“的对象作为起始点,从这些点出发开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(即从根节点达到该对象是不可达的),则证明此对象是不可用的。
GC Roots对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常用的引用对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

Java引用的概念:

  1. Java1.2之前Java对于引用的定义很传统:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。该定义下对象只有两种状态:被引用或者没有被引用两种,对于一些当内存足够时能保留,不够时则抛弃的对象无法描述,这类对象在很多有缓存功能的系统中都是存在的。
  2. Java1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用的强度依次减弱。
  • 强引用就是指在程序之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收调被引用的对象。
  • 软引用是用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果第二次回收还没有足够的内存,这时会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供WeakReference 类来实现弱引用。
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference 类来实现虚引用。

五、如何回收对象(垃圾收集算法)

标记-清除算法

“标记-清除(Mark-Sweep)算法“是最基础的收集算法,算法分为两个阶段“标记”和“清除”,标记过程即为判断对象是否存活的过程,清除为统一回收所被标记的对象。之所以说该算法是最基础的算法,是因为后续的收集算法都是在此基础上针对其不足进行改进的。
不足:一是效率问题,标记和清除两个过程的效率都不高。二是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后运行过程中分配较大的对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作。

复制算法

将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:实现简单,运行效率高。每次都对整个半区进行内存回收,内存分配时不用考虑内存碎片等复杂情况。
缺点:可用内存变成一半,内存浪费严重,代价太大。
优化:通过IBM公司专门的研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间。将内存分为三块一块较大的Eden空间和两块较小的Survivor空间,每次使用两块即一块Eden和一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor大小比例是8:1,也就是每次新生代可用内存空间为整个新生代的90%(80%+10%),只有10%的内存会被浪费。(当一次垃圾收集存活的对象占用的空间大于10%时,则需要由其他内存(指老年代)进行分配担保(Handle Promotion))

标记-整理算法

同“标记-清理”算法一样“标记-整理”(Mark-Compact)算法也分为两个阶段“标记”和“整理”,标记过程即为判断对象是否存活的过程,整理过程不是在直接对可回收对象进行清理,而是让所有存活的对象都向一端移动。然后直接清理掉端边界以外的内存。
适用范围:老年代

分代收集算法

”分代收集“(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。
优点:可以根据各个年代的特点采用最合适的收集算法。新生代中,每次垃圾回收时都会有大量的对象死去,只有少量存活,那就选用复制算法,只需要少量存活对象的复制成本就可以完成收集。老年代中因为对象存活率高、没有额外的空间对它进行分配担保,必须使用“标记-清理”或者“标记-整理”算法来进行回收。

HotSpot垃圾收集器

垃圾收集器.jpg
Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。Serial收集器是一个单线程的收集器,它只会使用一个cpu或一条收集线程去完成收集工作,而且在进行垃圾收集时,必须暂停其他所有的工作线程(即虚拟机中的Stop The World),直到垃圾收集结束。

Serial/Serial Old 收集器运行示意图.jpg
ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
ParNew收集器在单CPU的环境中绝对不会比Serial收集器有更好的效果,甚至存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分百地保证可以超越Serial收集器,但是随着CPU的数量增加,GC时系统资源的有效利用率提高

ParNew/Serial Old 收集器运行示意图.jpg
Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,使用的是复制算法,并且是多线程的收集器。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

Serial Old收集器

Serial Old 是Serial收集器的老年代版本,它是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还用两大用途:一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,二是作为CMS收集器的后备预案,在并发收集发生Concurrent ModeFailure时使用。

Serial/Serial Old 收集器运行示意图.jpg
Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用对线程和“标记-整理”算法。这个收集器是在JDK1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态,原因是,如果选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择,由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parllel Scavenge收集器也未必在整体性能上获得吞吐量最大化的效果,由于单线程的老年代集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

Parallel Scavenge/Parallel Old收集器运算示意图.jpg
CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记-整理”算法实现的,运作过程相对于前面几种收集器来说更为复杂一些,整个过程分为四步:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)
    其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
    由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以于用户线程一起工作,所以,总体来说,CMS收集器的内存回收是与用户线程一起并发执行的
Concurrent Mark Sweep收集器运行示意图.jpg

CMS三个明显缺点

  • CMS收集器对CPU资源非常敏感。
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
  • CMS是基于“标记-清除”算法实现的收集器,收集结束后会产生大量空间碎片。
G1

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,早在JDK1.7刚刚确立项目目标,Sun公司给出的JDK1.7 RoadMap里面,它就被视为JDK 1.7 中HotSpot虚拟机的一个重要进化特征,在JDK7u4时Sun公司认为它达到足够成熟的商用程度,移除了“Experimental”的标识。
G1是一款面向服务器应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK1.5发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运作,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的挺堵时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
    在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不在是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不在是物理隔离的,它们都是一部分Region(不需要连续)的集合。
    G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

“化整为零”思路存在的问题及解决办法:

将Java堆化整为零后可以Region为单位进行垃圾收集,但是由于Region不是孤立的,一个对象分配在某个Region中,它并非只能被本Region中的其他对象以你用,二是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活时就要对整个堆进行扫描才能确保准确性。为了解决这个问题,G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏
(该问题并不是G1中才有的,在以前的分代收集中也是存在的,只是在G1中更加突出了,以前分代收集新生代时也是不得不对老年代进行扫描,解决办法也是引入Remembered Set)
如果不计算维护Remembered Set 的操作,G1收集器的运作大致可以划分以下几个步骤

  • 初始标记(Initial Marking):仅仅是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking):是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这一阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking):是为了修正在并发阶段因用户程序继续运行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,该阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting andEvacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控的,而且停顿用户线程将大幅度提高收集效率。
G1收集器运行示意图.jpg

六、对象的分配策略

对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分比固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还用虚拟机中与内存相关的参数的设置。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC
虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
⚠️Minor GC和Full GC的区别:

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最经典的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更坏的是遇到一群“朝生夕死”的“大对象”,写程序时应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数(该参数只对Serial 和ParNew两款收集器有效),令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在EDen区及两个Survivor区之间发生大量的内存复制。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋级老年代,如果在Survivor空间中相同年龄兽所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保

虚拟机在发生MinorGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时要改为进行一次Full GC。

《深入理解Java虚拟机 JVM高级特性与最佳实践 第二版》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容