learn-jvm-3-垃圾回收

何时进行回收

一般来说,当某个区域内存不够的时候就会进行垃圾收集

  • young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
  • full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,
    则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);
    或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;
    或者System.gc()、heap dump带GC,默认也是触发full GC。

如何判断一块内存是垃圾?

引用计数

当有对象引用自身时,就会计数器加1,删除一个引用就减一,当计数为0时即可判断为垃圾.
存在的问题:引用计数存在循环引用问题

可达性分析(根搜索算法)

通过一些根节点开始,分析引用链,没有被引用的对象都可以被标记为垃圾对象。
JVM普遍采用该算法

GC Roots

Java虚拟机将以下对象定义为 GC Roots:

  • Java虚拟机栈中引用的对象:比如方法里面定义这种局部变量 User user= new User();
  • 静态属性引用的对象:比如 private static User user = new User();
  • 常量引用的对象:比如 private static final User user = new User();
  • 本地方法栈中引用的对象

引用链不可达标记

即便引用链不可达,也并不意味着该对象一定会被回收,因为回收要经历两次标记过程!
第一次标记:对象进行根搜索之后,如果发现没有与GC Roots 相连接的引用链,就会被第一次标记并进行筛选。所谓筛选,就是检查此对象是否有必要执行finalize方法,如果对象定义了该方法并且没有执行过。那么该对象就会被放入到一个队列F-Queue,随后会有一个低优先级的线程去执行这个队列里面对象的finalize方法
第二次标记:JVM 将对F-Queue队列里面的对象进行第二次标记。如果对象不想被回收,那么就得在finalize方法里面拯救自己,否则,这些对象就真的会被回收


垃圾回收算法

标记清除

对非垃圾对象进行标记,清除其他的对象。
存在的问题:这种方式对对内存空间造成空隙,即内存碎片,最终导致有空余空间,但没有连续的足够大小的空间分配内存。

标记整理

标记非垃圾对象后,将这些对象整理好,依次排列内存。
存在的问题:这样内存就是整齐的了。但是因为会造成对象移动,所以效率会有降低。~

标记清除整理

即组合标记清除标记整理两种方式,在若干次清除后进行一次整理。

复制

划分成两个相同大小的区域,收集时,将第一个区域的活对象复制到另一个区域.
这样不会有内存碎片问题。但是最多只能存放一半内存,而且所有的活对象都需要拷贝。
将内存分为一块较大的eden空间和2块较少的survivor空间,
每次使用eden和其中一块survivor,
当回收时将eden和 survivor还存活的对象一次过拷贝到另外一块survivor空间上,
然后清理掉eden和用过的survivor。
Sun Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。

分代回收

  • 对于新生代的对象回收,使用复制算法
  • 对于老年代的对象回收,使用标记-清除算法

收集器

垃圾收集器和收集算法不是冲突的,是结合完成的,不同的垃圾收集器会在不同的内存区域选择相应的垃圾收集算法完成清理。

Serial收集器

串行收集器主要有两个特点:第一,它仅仅使用单线程进行垃圾回收;第二,它独占式的垃圾回收。使用复制算法,优点是实现相对简单,逻辑处理特别高效,且没有线程切换的开销。适合单CPU处理器或者较小的应用内存,当JVM 在 Client 模式下运行时,它是默认的垃圾收集器

Parallel收集器

并行收集器,并行收集器是工作在新生代的垃圾收集器,它只简单地将串行回收器多线程化。它的回收策略、算法以及参数和串行回收器一样。
并行回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此,在并发能力比较强的 CPU 上,它产生的停顿时间要短于串行回收器,而在单 CPU 或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,由于多线程的压力,它的实际表现很可能比串行回收器差。

CMS收集器

CMS 是Concurrent Mark Sweep 的缩写,意为并发标记清除,和Parallel收集器不同的在于只在标记的时候独占也就是stop the world,老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。缺点是会产生内存碎片,可能需要再次GC来整理,也可以通过设置来避免。
对于 CMS,整个过程会有几步:

  • 初始标记。
  • 并发标记。
  • 重新标记。
  • 并发清除。

初始标记,stop the world,只标记能在GC roots直接找到的对象,时间很短,接着并发标记,根据初始标记来循环遍历所有活着的对象也就是tracing过程,这部分是用户线程并发进行的,重新标记是为了标记那些在并发标记过程中改变,漏掉的等对象,最后进行并发清除,当然其中还有更多的细节来保证GC的正确性,我就不是很了解了。

为什么初始标记不能也做成并发的?

而答案是:可以做成并发的,就是实现起来麻烦一些而已。Android Runtime(ART)编译器里的CMS实现在初始标记的时候采用了checkpointing做法,就不是完全stop-the-world而是一个个线程分别错开一点时间来暂停。这样系统在扫描一个线程的栈的时候其它线程还可以跑,比stop-the-world的影响小。