Java 垃圾回收机制

Java如何进行GC已经是一个老话题了

这里我们分成两个部分讨论  一是回收方式(引用计数器或者移动对象方式等) ,二是GC算法(Concurrent或者非Concurrent等)

通常情况下要进行GC就要知道 

1. 哪些对象需要回收 (标记空对象)

2. 如何重新分配内存 (先释放对象,然后整理压缩内存空间)

现代jvm几乎都使用了分代回收的方式 (即划分成新生代和老生代) , 通过对象的年龄(第一次回收存活的对象年龄为1,第二次仍然存活为2,依次类推)来处理对象是该处在新生代还是老生代

回收新生代的GC称为 Minor GC , 类似BigDecimal这种创建之后无法修改,而又经常需要丢弃的临时对象往往都是被Minor GC回收掉的

当对象不停的被移动到老生代, 老生代填满之后需要进行Full GC

两种GC方式都需要暂停所有线程, jvm会进入一种”假死”的状态等待本次GC运行完成

那么有没有什么方法可以缩短这种”假死”状态的时间呢?

答案是 “在线程运行期间就开始查找整理不在使用的可回收对象” , 

使用CMS和G1收集器时, 就是使用这种方式进行GC的, 它们被称作 Concurrent 收集器,也称为无停顿收集器(实际上在处理内存时还是要停顿的)或者低停顿收集器

这里你需要考虑具体的应用场景

1 如果平均GC时间长度更为重要 , 就不应该使用Concurrent 收集器, 因为Concurrent收集器会带来额外的cpu消耗

2 如果要尽量缩短程序的响应时间, 则应该选择Concurrent 收集器, 因为虽然整体GC时间变长了, 但是不会出现个别请求严重超时的情况

当然还要考虑硬件条件

1 如果cpu够强劲, Concurrent 收集器也更为适用

2 如果cpu性能较弱, Concurrent 收集器所带来的额外负担可能会导致不太适用

常用的垃圾收集器有以下4种,

1 Serial 垃圾收集器 ( 打开 -XX:+UseSerialGC 不能通过减号关闭 -XX:-UseSerialGC,需要指定另一种收集器 )

这个收集器是32位 client 模式下的默认收集器, Minor GC和Full GC都会暂停所有线程, 区别在于Full GC时 Serial 收集器会对老生代进行压缩整理

2. Throughput垃圾收集器

Throughput 收集器是Server虚拟机的默认收集器, 采用多线程的方式进行Minor GC和Full GC , 两种方式都会暂停所有线程, Full GC 对老生代进行压缩处理

-XX:+UseParallelGC 或者 -XX:+UseParallelOldGC

3. CMS收集器

CMS收集器的设计初衷在于消除Throughput收集器和Serial收集器Full GC时造成的长期停顿

进行Minor GC时 CMS收集器仍然会暂停所有线程,然后以多线程的方式进行回收

主要区别在于CMS收集器在进行Full GC时不会暂停所有线程, 它只在 Minor GC 时 和 扫描老生代内存时进行短暂的停顿, CMS不会在扫描释放期间对内存进行整理压缩

这种方式有两个弊端, 1 是额外的cpu消耗 2 是会使内存碎片化 , 当内存过度碎片化, jvm无法获得连续内存时(比如创建了新的数组结构), CMS会变为Serial收集器, 暂停所有线程, 单线程回收, 整理压缩内存

-XX:+UseParNewGC 或者 -XX:+UseConcMarkSweepGC

4. G1垃圾收集器

G1 收集器也被称为 垃圾优先收集器 , 它的设计初衷是尽量缩短处理超大堆 ( 大于4GB) 时产生的停顿.

G1 收集器会将堆划分为若干个区域(Region),每个区域再分为新生代和老生代,新生代的回收需要暂停所有线程,然后使用多线程进行回收

老生代复杂一些,  大多数时候不需要暂停所有线程, 仍然进行扫描然后释放, G1收集器会通过在不同Region移动老生代对象来完成内存的整理和压缩

-XX:+UseG1GC

至于像

标记清除(会产生碎片)

标记复制(需要额外的内存空间)

标记整理(额外性能开销)

只不过是以上收集器采用的不同算法,有些文章里将这些算法与不同收集器的运行方式(如并发,增量,分代)等并列,混为一谈本人认为不妥