垃圾收集主要是针对堆和⽅法区进⾏。程序计数器、虚拟机栈和本地⽅法栈这三个区域属于线程私有 的,只存在于线程的⽣命周期内,线程结束之后就会消失,因此不需要对这三个区域进⾏垃圾回收。
判断⼀个对象是否可被回收
1 引用计数法
在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。 当计数器为 0 时,就认为该对象无效了。 引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是主 流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因为它很难解决对象之间循环引 用的问题。
循环引用:对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数都不为 0,于是 引用计数算法无法通知 GC 收集器回收它们。
2可达性分析法
以 GC Roots 为起始点进⾏搜索,可达的对象都是存活的,不可达的对象可被回收。
GC Roots 是指:
Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
引⽤类型
不同的引用类型, 主要体现的是对象不同的可达性状态 reachable 和垃圾收集的影响。
强引用(Strong Reference) 类似 "Object obj = new Object()" 这类的引用,就是强引用,只要强引用存在,垃圾收集器永远 不会回收被引用的对象。
但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量, 那么对象在很长一段时间内不会被回收,会产生内存泄漏。
软引用(So Reference) 软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内 存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清 理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂 时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用(Weak Reference) 弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收 只被弱引用关联的对象。
虚引用(Phantom Reference) 虚引用也称幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存 在,完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后,做某些事 情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。为⼀个对象设置虚引⽤的唯⼀⽬的是能在这个对象被回收时收到⼀个系统通知。
垃圾收集算法
1标记-清除算法
标记的过程是:遍历所有的 GC Roots ,然后将所有 GC Roots 可达的对象标记为存活的 对象。
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标 记过的对象的标记,以便下次的垃圾回收。
另外,还会判断回收后的分块与前⼀个空闲分块是否连 续,若连续,会合并这两个分块
这种方法有两个不足:
效率问题:标记和清除两个过程的效率都不高。
空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配 较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法(新生代) 为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每 次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,
就将存活者的对象复制到另 一块上面,然后将第一块内存全部清除。
这种算法有优有劣:
优点:不会有内存碎片的问题。
缺点:内存缩小为原来的一半,浪费空间。
为了解决空间利用率问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。
回收时,将 Eden 和 Survivor 中还存活的对象一次 性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。
但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其 他内存(指老年代)进行分配担保。
分配担保
为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进 行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,
这样存活的对象直接通过分 配担保机制进入老年代,然后再将新对象存入 Eden 区。
3标记-整理算法(老年代)
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots ,然后将存活的 对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后直接清理掉端边界以外的内存
因此,第二阶段才称为整理阶段。
原因:这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量 对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。
优点: 不会产⽣内存碎⽚ 不⾜: 需要移动⼤量对象,处理效率⽐较低。
4分代收集算法 根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各 个年代的特点采用最适当的收集算法。
新生代:复制算法 老年代:标记-清除算法、标记-整理算法
Java新生代、老生代和永久代
https://www.jianshu.com/p/d3a0b4e36c28
1 新生代
主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代分为Eden区、ServivorFrom、ServivorTo三个区。
老年代的对象比较稳定,所以MajorGC不会频繁执行。
HotSpot 垃圾收集器
HotSpot 虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点,虽然我们要对各个收集 器进行比较,但并非为了挑选出一个最好的收集器。我们选择的只是对具体应用最合适的收集 器。
新生代垃圾收集器
Serial 垃圾收集器(单线程)
ParNew 垃圾收集器(多线程) ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。
Parallel Scavenge 垃圾收集器(多线程)
老年代垃圾收集器
Serial Old 垃圾收集器(单线程)
Parallel Old 垃圾收集器(多线程) Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
CMS 垃圾收集器
G1 通用垃圾收集器
G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块 块独立的 Region。
当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾 回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基 于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
这里抛个问题
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描 整个堆内存才能完整地进行一次可达性分析?
并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的 区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存 进行遍历。
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
初始标记:标记与GC roots直接关联的对象。
并发标记:可达性分析。
最终标记,对并发标记过程中,用户线程修改的对象再次标记一下。
筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间制定回收计划并回收。