文章目录
1 什么是垃圾回收机制
垃圾回收(Garbage Collection)是JVM垃圾回收器提供的一种用于死去的(不可能再被任何途径使用的)对象占据的内存空间的一种机制。
垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
2 Java中的引用类型
Java里有不同的引用类型,分别是强引用、软引用、弱引用和虚引用。
强引用
: 无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象软引用
: SoftReference内存充足时不回收,内存不足时则回收弱引用
: WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象虚引用
: PhantomReference是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
//强引用
Object object = new Object();
//软引用
SoftReference softReference=new SoftReference<>("1");
//弱引用
WeakReference weakReference=new WeakReference<>("2");
//虚引用
PhantomReference phantomReference = new PhantomReference("3",new ReferenceQueue());
3 如何判断对象是否可以被回收
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。
判断对象是否存活一般有两种方式:
引用计数
:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一,计数为0时可以回收。此方法虽然简单,无法解决对象相互循环引用的问题。比如两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
可达性分析
:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。
在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
4 方法区的垃圾收集
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。
5 垃圾收集算法
5.1 标记-清除(Mark-Sweep)算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
- 执行效率不稳定
如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。
- 内存空间的碎片化问题
标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
5.2 标记整理(Mark-Compact)算法
首先标记出所有需要回收的对象,在标记完成后,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
缺点:
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿被描述为"Stop The World"。
5.3 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:
可用内存缩小为了原来的一半,空间浪费太多
5.4 分代收集算法
根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。
6 内存分配与回收策略
6.1 内存分配策略
6.1.1 内存分配
- 对象优先在 Eden 分配。大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
- 大对象直接进入老年代。大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
- 长期存活的对象进入老年代。对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄(默认是15,可以使用-XX:MaxTenuringThreshold 参数来进行设置)则移动到老年代中。
- 动态对象年龄判定。Survivor区的对象年龄从小到大进行累加,当累加到X年龄时的总和大于50%(可以使用-XX:TargetSurvivorRatio 参数来进行设置),那么比年龄X大的对象都会晋升到老年代,不必达到MaxTenuringThreshold 中要求的年龄。
- 空间分配担保。在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,那么就进行 Minor GC 。 如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
6.1.2 堆的年轻代为什么要有两个Survivor区
- 如果没有Survivor区,每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major GC/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;
- 如果只有一个survivor区,刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor
GC,Eden中的存活对象就会被移动到Survivor区。下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。 - 如果有两个survivor区,新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1,S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复,这就解决了内存碎片化的问题。
6.2 内存回收策略
6.2.1 Minor GC 、Major GC、Mixed GC和 Full GC的区别
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
6.2.2 Minor GC触发条件
当Eden区满时,触发Minor GC,而Survivor区满不会触发Minor GC。
6.2.3 Full GC触发条件
- System.gc()方法的调用(此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存)
- 老年代空间不足
- 空间分配担保失败
- 方法区空间不足
- 由 Eden 区、S0 区向 S1 复制的对象太大,导致直接进入老年代,但是老年代内存不够
- CMS GC时出现concurrent mode failure(执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足,便会报 Concurrent Mode Failure 错误,并触发 Full GC)