Java笔记--垃圾回收机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_25859403/article/details/51822623

Java的垃圾回收机制

一 、概述

Java的垃圾回收(Garbage Collection),更明确地应该是对垃圾占用的内存进行回收。

问题1.1: 为什么要了解Java的垃圾回收呢?

 主要有以下几个原因:

   1)程序需要进行内存溢出的排查
   2)程序需要内存泄漏的排查
   3)垃圾回收成为程序提高高并发上限的瓶颈。

对于Java的GC机制,我们通常需要考虑以下几个方面:

   ** 哪些内存需要回收?
   ** 何时进行回收?
   ** 怎么进行回收?

二 、哪些内存需要回收?

问题2.1:需要进行内存回收的哪些区域?

   根据<Java虚拟机规范> ,Java虚拟机所管理的内存包括:程序计数器、虚拟机栈、本地方法栈、Java堆和方法区。 其中,前三个是多有线程私有的,
它们随着所在的线程而生成、而消亡,因此我们不需考虑;后两个区域是所有线程所共享的,这两部分内存的分配和回收是动态,因此我们关注的主要区域就是这两部分。
   (1)Java堆(Java Heap):别名“GC堆”,它在虚拟机启动时而创建。这块内存区域主要的作用是存储对象实例,几乎所有的对象是实例都在这里进行内存分配,这块区域内存回收的目标主要是对象。

   (2)方法区(Method Area):这块内存区主要用于存储虚拟机加载的类信息、常量、类变量、JIT编译器编译后的代码等数据。这块区域内存回收的目标主要就是针对常量池的回收和对类型的卸载。

(一)关于Java堆的回收

通过前文我们已经知道,对于堆内存我们的回收目标是对象,确切地说是那些无效的对象,对象”无效”可被理解为该对象不能在被任何途径引用了。

问题2.2:如何判断哪些对象是”存活(无效、可回收)”的呢?

通常有两种算法用来判断对象是否是存活的。第一种是引用计数算法;另一种是可达性分析算法。
(1)"引用技术算法"原理:给对象添加一个引用计数器,每当一个引用指向该对象时,计数器+1;当引用失效时,计时器-1。因此,当计数器为零时,该对象就是不可能在被引用的了。
 这种算法的优点是实现简单,判断效率高;缺点是难以解决对象相互引用的问题。
(2)"可达性分析算法"原理:通过一系列被称为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
    目前,一些主流的语言都是通过这种算法来判断,对象是否存活的。

:Java中 可作为GC Roots的对象包括:
虚拟机栈中的引用对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。

问题2.3:对于哪些在可达性分析算法中不可达的对象一定会被回收吗?

  不一定。(可参考 五 强引用、软引用、弱引用和虚引用)
  第一种情况:该对象虽然不可达,但是,它没有覆盖finalize()方法或者它的finalize()方法已经被虚拟机调用了,这时,系统认为没有必要(或不能)调用finalize()方法了。
  第二种情况:系统认为有必要执行该对象的finalize()方法,但是,在执行finalize()方法过程中,该对象又重新抓住了救命稻草,即又与引用链上的某个对象产生了关联(比如将this赋给了某个类变量或实例变量)。

这时我们可以得到一条重要的结论:
一个对象的finalize()方法被调用了并不能说明这个对象被回收了。


(二)关于方法区的回收

前面提到关于方法区的回收,主要回收的目标是主要就是针对常量池的回收和对类型的卸载。

其中对于常量池的回收这一表述,可能“常量池中无效常量回收”更为准确。关于何为无效的常量可以理解为:
如果常量池中的某个常量,再也没有任何引用指向它,那么,可以认为这个常量是无效的、可以被回收的;这时如果发生内存回收并且有必要的话,那么,这个无效常量将会被从常量池移除。

问题2.4:当一个类变得无用时,虚拟机就可以对这个类进行回收(卸载),怎样才算是无用呢?

    1)该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    2)加载该类的ClassLoader已经被回收。
    3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  只有当一个类同时满足了以上的单个条件,该类才可以被虚拟机回收。注意,仅仅是可以。

三、 怎么进行回收?

对与怎么回收这疑问题,主要设计到了几个垃圾手机算法。主要包括:

     标记-清除算法
     复制算法
     标记-清理算法
     分代收集算法

(一)标记-清除算法

这是现代垃圾回收算法的基础,标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象,未被标记的对象就是未被引用的垃圾对象,然后再清除阶段,清除所有未被标记的对象。这个思想的意思是,在程序的起点开始就是一个根节点,随着程序的运行就会引用不同的节点,被引用的做标记,未被引用的做清除标记,做清除标记的作为垃圾回收。这种算法的缺点就是会产生空间碎片(图片来源百度)。

(二)复制算法

这是一种高效的回收算法,其核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中存活的对象复制到未使用的内存块中,之后,清除正在使用的内存块的所有对象,然后再交换两个内存的角色,这样就完成了垃圾回收。其缺点是当内存中大部分都是存活对象,那么这种算法都是在做无用功(图片来源百度)。

(三)标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可 回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存(图片来源百度)。

(四) 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样 就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。(关于分代可见下一部分)


四、 何时进行回收?

(一)分代收集算法的分代机制

年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的<下图来源>

来源
  
年轻代:
  所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
  
年老代:
  在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  
持久代:
  用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

问题2.5为什么要对对象进行分代?<摘自http://my.oschina.net/u/266531/blog/84059>

    在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,  
有的对象生命周期较长,比如Http请求中的Session对象,线程,Socket连接等;有的对象生命周期较短,比如String对象,  
由于其不变类的特性,有的在使用一次后即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,  
那么消耗的时间相对会很长,而且对于存活时间较长的对象进行的扫描工作等都是徒劳。因此就需要引入分治的思想,所谓分治的思想就是因地制宜,  
将对象进行代的划分,把不同生命周期的对象放在不同的代上使用不同的垃圾回收方式。

(二)何时进行回收?

    由于对象进行了了分代处理,所以垃圾回收区域和时间也不一样。  
回收(Gabage Colection )的类型有两种:Scavenge GC和Full GC。

    Scavenge GC:当新对象生成,但在Eden申请空间失败时就会触发Scavenge GC,对Enden去进行GC,清除掉非存活的对象,并且把存活的对象移动到Survivor区中的其中一个区中。  
前面的提到考验就是Scavenge GC,也就是说对象经过了Scavenge GC才能够进入到存活区中。这种形式的GC只会在年轻代中进行,因为大部分对象都是从Eden区开始的, 
同时Eden区不会分配得太大,所以对Eden区的GC会非常地频繁。

    Full GC:对整个对进行整理,包括了年轻代、年老代和持久代。Full GC要对整个块进行回收,所以要比Scavenge GC慢得多,  
 因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对Full GC的调节,如下原因可能触发它的执行:
              **年老代(Tenured)被写满
              **持久代(Permanent)被写满
              **System.gc()被显式调用
              **自上一次GC之后Heap的各域分配策略动态变化

五、强引用、软引用、弱引用和虚引用

在JDK 1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(WeakReference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。


    <强引用> 就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,  
 垃圾收集器永远不会回收掉被引用的对象。


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



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


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

六、对象的可用与可达性

可用性:可用即对象能够正常的参与运算。
可达性: 根据可达性分析算法,可达即对象存在一条到达GC Roots的引用链。

问题2.6:根据可用与可达性,对象可分为哪几类?

1)不可用不可达------>这种情况GC会帮我们回收掉,而C++不会
2)不可用可达  ------>这种情况会存在内存泄露
3) 可用可达    ------>正常使用

应该特别注意第二种情况:它是导致内存泄漏的原因之一。

猜你喜欢

转载自blog.csdn.net/qq_25859403/article/details/51822623
今日推荐