垃圾回收
# 1. 如果判断对象可以回收
# 1.1 引用计数法
当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
🔎这个引用计数法听起来不错,但是有一个弊端,循环引用时,两个对象的计数都为1,导致两个对象都无法释放
# 1.2 可达性分析算法
- JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
- 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
- 可以作为 GC Root 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的Native方法)引用的对象
# 1.3 四种引用
- 强引用 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用(SoftReference) 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 可以配合引用队列来释放软引用自身
- 弱引用(WeakReference) 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身
- 虚引用(PhantomReference) 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
终结器引用(FinalReference) 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。
# 2. 垃圾回收算法
- 标记清除
- 标记整理
- 复制
# 3. 分代垃圾回收
- 新创建的对象首先分配在 eden 区
- 新生代空间不足时,触发 minor gc ,eden 区 和 from 区存活的对象使用 - copy 复制到 to 中,存活的对象年龄加一,然后交换 from to minor gc 会引发 stop the world( STW ),暂停其他线程,等垃圾回收结束后,恢复用户线程运行
- 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)
- 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,停止的时间更长!
# 4. 垃圾回收器
相关概念:
- 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
- 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
# 4.1 串行
- 安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
Serial 收集器是最基本的、发展历史最悠久的新生代收集器
- Serial 收集器 单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
ParNew 收集器其实就是 Serial 收集器的多线程版本
- ParNew 收集器 多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
Serial Old 是 Serial 收集器的老年代版本
- Serial Old 收集器 同样是单线程收集器,采用标记-整理算法
# 4.2 吞吐量优先 —— 多线程
- Parallel Scavenge 收集器 属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
是 Parallel Scavenge 收集器的老年代版本
- Parallel Old 收集器 多线程,采用标记-整理算法(老年代没有幸存区)
🔎补充: GC自适应调节策略
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。 当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
Parallel Scavenge 收集器使用两个参数控制吞吐量:
- XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
- XX:GCTimeRatio=rario 直接设置吞吐量的大小
# 4.3 响应时间优先 —— 多线程
- CMS 收集器 Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
# 4.4 G1 收集器
适用场景:
- 同时注重吞吐量和低延迟(响应时间)
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
- 整体上是标记-整理算法,两个区域之间是复制算法
垃圾回收阶段
- Young Collection:对新生代垃圾收集(会STW)
- Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。(不会STW)
- Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
Full GC
G1 在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。
Young Collection 跨代引用 : 翻书吧!!!