3.1 垃圾收集机制及垃圾收集器介绍

程序计数器,虚拟机栈,本地方法栈随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈操作。而且每一个栈帧中分配多少内存基本上在类结构确定下来时就已知了。因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存就自然的跟着回收了

JAVA堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。


3.2 如何确定对象是否还存活?

     引用计数法

      给对象添加一个引用计数器,每当它被引用计数器+1,引用失效计数器-1;计数器为0时对象就不可能再被使用。

      该方法实现简单,效率高,但是JVM没有使用,其中最主要原因是很难解决对象间的相互循环引用问题。


        可达性分析算法

        通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(用图论的话来说,就是从GC Roots到这个对象不可达),则证明此对象是不可用的。如下图所示:ObjectFGHI为不可达对象。


         在JAVA中,可作为GC Roots的对象包括以下几种:

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

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

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

         本地方法中JNI引用的对象

  • Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  • Thread - 活着的线程
  • Stack Local - Java方法的local变量或参数
  • JNI Local - JNI方法的local变量或参数
  • JNI Global - 全局JNI引用
  • Monitor Used - 用于同步的监控对象
  • Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此就只有留给分析分员去确定哪些是属于"JVM持有"的了。

     3.2.3 再谈引用

     如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

     在JAVA1.2后,对引用的概念进行了扩充,将引用分为了强引用,软引用,弱引用,虚引用四种,引用关系依次减弱;

     强引用 :程序件普遍存在的引用关系,类似“Object obj = new Object()” 只要强引用存在,垃圾收集器永远不能回收掉被引用的对象。

     软引用:描述一些还有用但并非必需的对象。软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,就会抛出内存溢出异常。JDK1.2之后采用SoftReference类实现软引用。

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

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


   3.2.5 回收方法区

    方法区(永久代)垃圾回收主要分为两部分:废弃的常量和无用的类。回收废弃的常量和回收JAVA堆中的常量类似,回收无用的类,则需要类满足一下三个条件:

    该类的所有实例都已经被回收,即JAVA堆中不存在该类的任何实例

    加载该类的ClassLoader已经被回收

    该类对应的java.lang.Class对象没有在其他任何地方被引用,无法再任何地方通过反射机制访问该类的方法


    3.3 垃圾收集算法

    3.3.1 标记 - 清除算法

    整个收集过程分为:标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完后统一回收所有被标记的对象。缺点:一个是效率问题,标记和清除效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,碎片太多会导致以后需要分配较大对象时,无法找到足够的连续内存而不得不触发一次垃圾手机动作。


    3.3.2 复制算法

    将内存容量分为大小相等的两块,每次只是用其中的一块,每当这一块内存用完了,就将存活的对象复制到另一块上面,然后把使用过的内存空间一次性清掉。这样每次都是对整个半区进行内存回收,不会出现内存碎片等复杂情况。缺点:将内存缩小为原来的一半,代价太高了;在对象存活率较高时就要进行较多的复制操作,效率较低。

    现在的商业虚拟机都采用这种算法回收新生代(新生代的对象98%存活时间很短);将内存分为一块较大的Eden(伊甸区)和两块较小的Survivor(幸存者区);每次使用Eden和其中一块Survivor,当需要回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor区,然后清理Eden和用过的Survivor区(HotSpot默认Eden和Survivor比例为8:1)。

     由于没有办法保证每次回收的存活对象都不超过10%,所以当Survivor区不够用时,需要依赖其他内存(老年代)进行分配担保。

               

    3.3.3 标记-整理算法

             复制算法在对象存活率较高(老年代)时就要进行较多的复制操作,效率较低;根据老年代的特点,使用另一种“标记-整理算法”;其中标记过程与“标记-清除算法”一致,然后让存活的对象都向一端移动,然后直接清理掉端边界以外的内存;

3.3.3 分代收集算法

         当前的商业虚拟机的垃圾收集都采用“分代收集”算法:

         根据对象存活周期将内存分为几块,一般是把堆分为新生代和老生代

         新生代,存活率低,采用“复制”算法;老生代,存活率高,采用“标记-清理”,或者“标记-整理”算法进行回收;

       

3.4 HotSpot的算法实现

      3.4.1 枚举根节点

      可以作为GC Roots的节点主要是全局性的引用(例如常量或类静态属性)与执行上下文(栈帧中的本地变量表),如果要逐个检查这里面的引用,必然会消耗很多时间。

      可达性性分析对执行时间的敏感还体现在GC停顿上,分析工作必须确保一致性,就像冻结在某个时间点了。这一点导致GC运行时必须停顿所有的JAVA执行线程(SUN叫做这是Stop The World)。


     3.4.2  安全点

     程序执行时并非在所有的地方都能停顿下来开始GC,只有到达安全点时才能暂停。安全点的选取基于“是否具有让程序长时间执行的特征”,“长时间执行”最明显特征就是指令序列复用,例如方法调用,循环跳转,异常跳转等会产生安全点。

   对于安全点,另一个问题是,如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来。有两种方式:抢先式中断,主动式中断;

   抢先式中断不需要线程的执行代码主动配合,在GC发生时,中断所有的线程,如果发现有线程不在安全点上,就恢复,让他到安全点上。不采用;

   主动式中断,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真就自己中断挂起。轮询标志的地方和安全点是重合的。


   3.4.3  安全区域

安全点机制保证了程序执行时,不太长时间内就会遇到可进入的GC的安全点。但是程序不执行呢?比如线程处于Sleep或者Blocked,此时线程无法响应JVM中断请求,跑到安全的地方中断挂起,这时就要安全区域来解决。

安全区域指在一段代码中,引用关系不会发生变化,该区域内GC是安全的。

在线程执行到安全区中的代码时,首先标识自己进入了安全区,如果JVM要GC时就不用管标识为进入安全区的线程了。离开安全区时,检查系统是否已经完成了根节点枚举(或者整个GC过程),如果完成了,线程继续运行,否则他就必须等待到可以离开安全区的信号为止。


 3.5 垃圾收集器

        下图为HotSpot虚拟机的垃圾收集器,虚拟机所处区域表示其属于新生代还是老年代收集器,两个收集器之间有连线,表示他们可以搭配使用;

3.5.1 Serial收集器

    JDK1.3.1之前新生代收集器中的唯一选择。单线程收集器,在进行垃圾收集时会暂停所有其他的工作线程,对用户来讲体验不好。

        优点:简单高效,Client模式默认的新生代收集器。下图为Serial/Serial Old收集器运行示意图


3.5.2 ParNew收集器

Serial收集器的多线程版本。Server模式下虚拟机中首选的新生代收集器,因为它可以和老年代收集器CMS配合使用;单CPU下不会比Serial好,多CPU下才能超越Serial。

       下图为ParNew/Serial Old收集器运行示意图

        

PS:

并行(Parallel):指多条垃圾收集线程并行工作,但是此时用户线程仍然处于等待状态;

        并发(Concurrent):指用户线程和垃圾收集线程同时执行(不一定并行,可能交替执行),用户程序继续运行,垃圾收集程序运行另一CPU上。


3.5.3 Parallel Scavenge收集器

CMS关注点为尽可能缩小垃圾收集时用户线程的停顿时间,Parallel Scavenge收集器目标为达到一个可控制的吞吐量。吞吐量即CPU运行用户代码的时间与CPU总消耗时间的比值。

        停顿时间越短越适合需要与用户交互的程序,良好的响应速度提升用户体验;高的吞吐量则可以更高效率地利用CPU时间,尽快地完成运算任务,适合后台运算而不需要太多的交互任务。


3.5.4 Serial Old收集器

为Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。


3.5.5 Parallel Old 收集器

为Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

下图为Parallel Scavenge/Parallel Old收集器运行示意图



3.5.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器以获取最短回收停顿时间为目标。主要用在互联网站点或者BS系统服务器上,停顿时间短,响应速度快。

        Mark Sweep 标记-清理,分为四个步骤:

        初始标记->并发标记-> 重新标记->并发清除

其中初始标记和重新标记需要“Stop The World”。初始标记仅仅标记GC Roots能直接关联的对象,速度快;并发标记就是GC Roots Tracing(根搜索);重新标记修正并发标记因用户程序继续运行而导致的变动。运行示意图如下:



缺点:

并发过程对CPU资源占用较大(回收线程数=(cpu+3)/4)(解决方法,采用增量式并发收集器,即抢占式)

    无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC。并发清理时用户线程还在进行,自然就会产生新的垃圾,这些垃圾 在标记之后,CMS就无法再当次收集中处理它们,只好等待下次GC。由于垃圾收集时用户线程还在运行,那就要预留足够内存给用户线程,因此不能像其他收集器那样等待老年代几乎填满了才进行收集。

标记--清理式产生碎片


3.5.7 G1收集器

 Garbage First特点:

并行与并发:充分利用多CPU,多核环境,缩短Stop-The-World停顿时间

分代收集:采用不同的方式,处理新创建的对象和存活了一段时间的对象

空间整合:整体看基于“标记-整理”算法,局部看(Region之间)基于“复制”算法;避免在分配大对象时不会因为连续内存不够而出发一次GC

  可预测的停顿

G1之前的收集器范围是整个新生代和老年代,而G1不同,它把Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但是两者不在物理隔离了。

G1收集器避免了在整个JAVA堆中进行全区域垃圾收集,而是跟踪各个Region里面的垃圾堆积的价值大小(回收所获空间以及所需时间的经验值),并维护一个优先列表,每次优先回收最大价值的Region。因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程可大幅提高收集效率。示意图如下:



3.5.8 理解GC日志

[GC [PSYoungGen: 4981K->504K(6656K)] 4981K->2054K(20480K), 0.0395242 secs] [Times: user=0.06 sys=0.00, real=0.06 secs] 
[Full GC [PSYoungGen: 6034K->0K(6656K)] [ParOldGen: 13420K->7585K(13824K)] 19455K->7585K(20480K) [PSPermGen: 2508K->2507K(21504K)], 0.0882001 secs] [Times: user=0.14 sys=0.00, real=0.09 secs] 

[GC  和  [Full GC 说明了垃圾收集器的停顿区域,有Full说明GC发生了Stop-The-World,如果是调用System.gc(),那么会显示[Full GC(System);没有Full即代表是MinorGC,即发生在新生代的垃圾收集动作。

[PSYoungGen 为 Parallel Scavenge收集器的新生代,老年代[PSOldGen ,[PSPermGen 永久代同理

Par为Parallel收集器

DefNew为新生代默认收集器Serial

分代后面紧跟着的数字4981K->504K(6656K)含义为:GC前内存使用量->GC后内存区域使用量(该内存区域总量)

最后面4981K->2054K(20480K)含义为:GC前Java堆容量->GC后Java堆容量(Java堆总容量)

0.0395242 secs 为GC占用时间




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值