对于GC来讲,当程序员建立对象时,GC就开始监控这个对象的地址、大小以及使用状况。一般,GC采用有向图的方式记录和管理堆(heap)中的全部对象。经过这种方式肯定哪些对象是\"可达的\",哪些对象是\"不可达的\".当GC肯定一些对象为\"不可达\"时,GC就有责任回收这些内存空间。程序员
最基础的收集算法 —— 标记/清除算法算法
标记/清除算法的基本思想就跟它的名字同样,分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。多线程
标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历全部的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,通常是在对象的header中,将其记录为可达对象;并发
清除阶段:清除的过程是对堆内存进行遍历,若是发现某个对象没有被标记为可达对象(经过读取对象header信息),则将其回收。jvm
上图是标记/清除算法的示意图,在标记阶段,从对象GC Root 1能够访问到B对象,从B对象又能够访问到E对象,所以从GC Root 1到B、E都是可达的,同理,对象F、G、J、K都是可达对象;到了清除阶段,全部不可达对象都会被回收。高并发
在垃圾收集器进行GC时,必须中止全部Java执行线程(也称"Stop The World"),缘由是在标记阶段进行可达性分析时,不能够出现分析过程当中对象引用关系还在不断变化的状况,不然的话可达性分析结果的准确性就没法获得保证。在等待标记清除结束后,应用线程才会恢复运行。优化
前面刚提过,后续的收集算法是在标记/清除算法的基础上进行改进而来的,那也就是说标记/清除算法有它的不足。其实了解了它的原理,其缺点也就不难看出了。线程
一、效率问题。标记和清除两个阶段的效率都不高,由于这两个阶段都须要遍历内存中的对象,不少时候内存中的对象实例数量是很是庞大的,这无疑很耗费时间,并且GC时须要中止应用程序,这会致使很是差的用户体验。3d
二、空间问题。标记清除以后会产生大量不连续的内存碎片(从上图能够看出),内存空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾回收动做。对象
复制算法
为了解决效率问题,复制算法出现了。复制算法的原理是:将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,而后把这一块内存全部的对象一次性清理掉。用图说明以下:
回收前:
回收后:
复制算法每次都是对整个半区进行内存回收,这样就减小了标记对象遍历的时间,在清除使用区域对象时,不用进行遍历,直接清空整个区域内存,并且在将存活对象复制到保留区域时也是按地址顺序存储的,这样就解决了内存碎片的问题,在分配对象内存时不用考虑内存碎片等复杂问题,只须要按顺序分配内存便可。
复制算法简单高效,优化了标记/清除算法的效率低、内存碎片多的问题。可是它的缺点也很明显:
一、将内存缩小为原来的一半,浪费了一半的内存空间,代价过高;
二、若是对象的存活率很高,极端一点的状况假设对象存活率为100%,那么咱们须要将全部存活的对象复制一遍,耗费的时间代价也是不可忽视的。
基于以上复制算法的缺点,因为新生代中的对象几乎都是“朝生夕死”的(达到98%),如今的商业虚拟机都采用复制算法来回收新生代。因为新生代的对象存活率低,因此并不须要按照1:1的比例来划份内存空间,而是将内存分为一块较大的Eden空间和两块较小的From Survivor空间、To Survivor空间,三者的比例为8:1:1。每次使用Eden和From Survivor区域,To Survivor做为保留空间。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的。GC进行时,Eden区中全部存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,无论怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,须要依赖老年代进行分配担保,将这些对象存放在老年代中。
标记/整理算法
复制算法在对象存活率较高时要进行较多的复制操做,效率会变得很低,更关键的是,若是不想浪费50%的内存空间,就须要有额外的内存空间进行分配担保,以应对内存中对象100%存活的极端状况,所以,在老年代中因为对象的存活率很是高,复制算法就不合适了。根据老年代的特色,高人们提出了另外一种算法:标记/整理算法。从名字上看,这种算法与标记/清除算法很像,事实上,标记/整理算法的标记过程任然与标记/清除算法同样,但后续步骤不是直接对可回收对象进行回收,而是让全部存活的对象都向一端移动,而后直接清理掉端边线之外的内存。
回收前:
回收后:
能够看到,回收后可回收对象被清理掉了,存活的对象按规则排列存放在内存中。这样一来,当咱们给新对象分配内存时,jvm只须要持有内存的起始地址便可。标记/整理算法不只弥补了标记/清除算法存在内存碎片的问题,也消除了复制算法内存减半的高额代价,可谓一箭双雕。但任何算法都有缺点,就像人无完人,标记/整理算法的缺点就是效率也不高,不只要标记存活对象,还要整理全部存活对象的引用地址,在效率上不如复制算法。
三种算法的比较
效率:复制算法 > 标记/整理算法 > 标记/清除算法(标记/清除算法有内存碎片问题,给大对象分配内存时可能会触发新一轮垃圾回收)
内存整齐率:复制算法 = 标记/整理算法 > 标记/清除算法
内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法
终极算法 —— 分代收集算法
分代的垃圾回收策略,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的回收算法,以便提升回收效率。
年轻代(Young Generation)
1.全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(通常而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,而后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另外一个survivor1区,而后清空eden和这个survivor0区,此时survivor0区是空的,而后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
4.新生代发生的GC也叫作Minor GC,MinorGC发生频率比较高(不必定等Eden区满了才触发)
年老代(Old Generation)
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大不少(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优势是简单高效。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器(中止-复制算法)
新生代收集器,能够认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(中止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量通常为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。
Parallel Old收集器(中止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择
因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个堆进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于FullGC的调节。有以下缘由可能致使Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC以后Heap的各域分配策略动态变化