您的位置:首页 > 其它

4.JVM虚拟机-GC-垃圾回收算法

2020-06-27 11:35 127 查看

目录

1. 哪个区域需要垃圾回收

2. 对象已死吗

2.1 引用计数法

2.1.1 优缺点

2.2.2 循环引用代码示例

2.2 可达性分析算法

2.2.1 可达性分析算法描述

2.2.2 GC Roots对象

2.3 Java中的四种引用类型

2.3.1 强引用

2.3.2 软引用

2.3.3 弱引用

2.3.4 虚引用

2.4 生存还是死亡

2.5 方法区如何回收

3. 常用的垃圾收集算法

3.1 标记-清除算法(Mark-Sweep)

3.3 复制算法(Copying)

3.4 标记-整理算法(Mark-compact)

3.5 分代收集算法

3.5.1 年轻代的回收算法

3.5.2 年老代的回收算法

3.5.3 持久代的回收算法

4. GC是什么时候触发的

4.1 Minor GC

4.2 Full GC

1. 哪个区域需要垃圾回收

JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区(JDK1.8 已经用MetaSpace代替Perm Gen,因此,方法区是在MetaSpace中)。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。Java堆区是虚拟机内存中管理的最大的一个区域,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分

下面时JDK1.7,JDK1.8的内存模型

JDK1.7的内存模型

 

JDK1.8的内存模型

 

2. 对象已死吗

2.1 引用计数法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

2.1.1 优缺点

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断(stop-the-world)的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

 

2.2.2 循环引用代码示例

[code]public class abc_test {

public static void main(String[] args) {
// TODO Auto-generated method stub

MyObject object1=new MyObject();
MyObject object2=new MyObject();

object1.object=object2;
object2.object=object1;

object1=null;
object2=null;

}

}

class MyObject{

MyObject object;

}

这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将

object1
object2
赋值为
null
,也就是说
object1
object2
指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们

 

2.2 可达性分析算法

2.2.1 可达性分析算法描述

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7 虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。 

GC Root在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。不会出现对象间循环引用问题。

2.2.2 GC Roots对象

在Java语言中,可作为GC Roots的对象包括下面几种:

  a) 虚拟机栈中引用的对象(栈帧中的本地变量表)

  b) 方法区中类静态属性引用的对象

  c) 方法区中常量引用的对象

  d) 本地方法栈中JNI(Native方法)引用的对象


2.3 Java中的四种引用类型

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种。

2.3.1 强引用

  在程序代码中普遍存在的,类似 

Object obj = new Object()
 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 

2.3.2 软引用

  用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

2.3.3 弱引用

  也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。比如 threadlocal

2.3.4 虚引用

  也叫幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例它的作用是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

 

2.4 生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

  第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行

finalize()
方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(此方法只能被调用一次),虚拟机将这两种情况都视为“没有必要执行。

  第二次标记

finalize()
方法中没有重新与引用链建立关联关系的,将被进行第二次标记

  第二次标记成功的对象将真的会被回收,如果对象在

finalize()
方法中重新与引用链建立了关联关系,那么将会逃离本次回收

下面是通过finalize()方法进行对象自救:

[code]public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;

public void isApve() {
System.out.println("yes, i am still apve ");
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}

public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();

//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isApve();
} else {
System.out.println("no, i am dead ");
}

//下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isApve();
} else {
System.out.println("no, i am dead ");
}
}
}

2.5 方法区如何回收

方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的
    ClassLoader
    已经被回收;
  • 该类对应的
    java.lang.Class
    对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

 

3. 常用的垃圾收集算法

 

3.1 标记-清除算法(Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

缺点:产生大量不连续的内存碎片碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。 

GC Roots演示碎片产生过程:

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

 

3.3 复制算法(Copying)

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

 

缺点:这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半,并且Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

Copying算法过程演示: 

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

 

 

 

3.4 标记-整理算法(Mark-compact)

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉边界以外的内存。

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:

 

3.5 分代收集算法

分代收集算法(Generational Collection)是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)

下图为堆内存与新生代的区域划分:

 

 

3.5.1 年轻代的回收算法

年轻代(Young Generation)一般以copying算法为主。

a) 所有新生成的对象首先都是尽可能放在年轻代,目标就是尽可能快速的收集掉那些生命周期短的对象。

b) 新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的, 如此往复。当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

 

3.5.2 年老代的回收算法

老年代(Old Generation)的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

 

3.5.3 持久代的回收算法

方法区的回收主要回收两部分内容:废弃的常量和无用的类。  

持久代(Permanent Generation)用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class(反射),例如Hibernate ,动态代理等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

 

4. GC是什么时候触发的

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GCFull GC

4.1 Minor GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来

 

4.2 Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

a) 年老代(Tenured)被写满;

b) 持久代(Perm)被写满;

c) System.gc()被显示调用;

d) 上一次GC之后Heap的各域分配策略动态变化;

 

 

参考:

0. 《深入理解java虚拟机》

1. 《https://www.cnblogs.com/aspirant/p/8662690.html》--JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器)

2. 《https://www.cnblogs.com/xiaotian15/p/7008655.html》--java虚拟机:gc内存回收

4.  《https://blog.csdn.net/Hollake/article/details/92762180》--JDK1.7和JDK1.8的内存模型比较

5. 《https://www.cnblogs.com/aflyun/p/10575740.html》--JVM内存模型以及JDK7和JDK8内存模型对比总结

6.  《https://www.geek-share.com/detail/2711361200.html》--类加载机制

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: