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
至于像
标记清除(会产生碎片)
标记复制(需要额外的内存空间)
标记整理(额外性能开销)
只不过是以上收集器采用的不同算法,有些文章里将这些算法与不同收集器的运行方式(如并发,增量,分代)等并列,混为一谈本人认为不妥