学习java虚拟机 - 垃圾回收

学习java虚拟机 - 垃圾回收

一、什么是垃圾回收

  Java会对内存自动分配于回收管理,使得上层业务更加安全,方便的使用内存实现程序逻辑。在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的。垃圾回收的主要目的是清除不再使用的对象,自动释放内存。

 

 

二、如何判断哪些对象需要回收

  可达性分析:

  GC Roots.如果一个对象于GC Roots之间没有直接或间接的引用关系,比如某个市区任何引用的对象,或者两个互相用到装循环引用的对象的。

  什么对象可以作为GC Roots呢?比如:类静态属性中引用的对象,常量引用的对象,虚拟机栈中引用的对象、本地方法栈中引用的对象。

 

  4种引用:

  a) 强引用. 

     new 生成的对象,JVM不会强制回收这类对象。

  b)软引用.

    有用但非必需的对象。在发生内存溢出之前,尝试对这些对象进行第二次回收。 SoftReference类。

  一般用来实现内存敏感的高速缓存. 软引用和一个引用队列(ReferenceQueue)联合使用, 如果软引用所引用的对象被垃圾回收, JAVA虚拟机就会吧这个软引用加入到与之关联的引用队列中. (Mybatis中的缓存模块中的 SoftCache使用了)

  c)弱引用.

    非必需对象。垃圾收集器工作是,无论当前内存是否足够,都会回收。WeakReference类实现。

  弱引用比软引用的生命周期更加短. 它与软引用类似,可以和一个引用队列(RefrenceQueue)使用, 如果弱引用所引用的对象被垃圾回收, Java虚拟机就会把这个弱引用加入到与之关联的引用队列中.

  d)虚引用.

    为一个对象设置需引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 PhantomReference类来实现。

  虚引用主要用来跟踪对象被垃圾回收的活动. 虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(RefrenceQueeu)联合使用. 当垃圾回收器准备回收一个对象时, 如果发现它还有徐引用, 就会在回收对象内存之前, 把这个虚引用加入到与之关联的引用队列中,.程序可以通过吧这个徐引用加入到与之关联的引用队列中, 来了解被引用的对象是否将要被垃圾回收了.

 

 

三、什么时候回收

  1) 枚举根节点

  在枚举根节点进行可达性分析时, 必须保证在分析过程中不会出现对象引用关系还在变化的现象, 否则就无法保证分析结果的准确性.  因此,GC进行时必须停止执行所有线程(Stop the World, STW). 

  HotSpot虚拟机在停顿时, 使用一组成为OopMap的数据结构来直接得知哪些地方存放对象引用, 如此, 就不必检查所有执行上下文和全局引用位置, 减少耗时.

 

  2) 安全点

   HotSpot没有为每条指令生成OopMap,为了减少空间的损耗, 只在"特定的位置"记录这些信息,这些位置称为安全点(Safepoint).

  安全点的选定基本上是以程序"是否具有让程序长时间执行的特征" 为标准进行选定的, "长时间执行"的最明显特征就是指令序列复用, 例如方法调用丶循环跳转丶异常跳转等, 具备这些功能的指令才会产生Safepoint.

  如何在GC发生时让所有线程(不包括JNI调用的线程)都"跑"到最近的安全点上再停顿下来. 有抢占式中断(Preemptive Suspension) 和 主动式中断(Voluntarty Suspension). 抢占式中断, 在GC发生时, 首先把所有线程全部中断, 如果发现有线程不在安全点上, 就恢复线程, 让它"跑"到安全点上. 但是, 现在虚拟机一般都是用主动式中断. 当GC需要中断线程的时候,不直接对线程操作, 仅仅简单的设置一个标识, 各个线程执行时, 主动去轮训这个标识, 发现中断标识为真时, 就自己中断挂起.

 

  3) 安全域

   程序不执行时无法相应JVM的中断请求, "走"到安全的地方中断挂起. (程序不执行, 就是没有分配CPU时间, 典型的例子就是线程处于sleep状态或者Blocked状态)

   这时候需要安全区域(SafeRegion)解决. 安全区域就是在一段代码片段之中,引用关系不会发生变化, 在这个区域中的任意地方开始GC都是安全的.

  在线程执行到安全区域中的代码是, 首先标识自己已经进入了安全区域. 如果这段时间内发生GC, 就不用管这些进入安全区域的线程了. 当线程离开安全区域时, 它要检查系统是否已经完成了根节点枚举, 如果完成了, 线程就继续执行, 否则它就等待直到可以安全离开安全区域的信号.

 

 

四、如何回收

  4.1)  垃圾回收算法

  a)  标记清除算法--- 会产生较多空间碎片,一般用于老年代

  最基础的收集算法,后续的算法都是基于这种思路的改进.  它分为"标记"和"清除"两个阶段: 首先标记出所有需要回收的对象, 在标记完成后统一回收所有被标记的对象.

  有两个不足, 一是效率问题, 标记和清除两个过程效率都不高,另一个是空间问题,标记清除之后会产生大量不连续的内存碎片.空间碎片太多可能倒置以后在程序运行过程中需要分配较大对象时,找不到足够的连续内存而不得不提前触发另一个垃圾回收.

 

 

  b)  标记复制算法-- 一般用于新生代。

   将可用内存按容量划分为大小相等的凉快, 每次只使用其中的一块.当这一块内存用完之后,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉. 这样使得每次都是对整个半区进行内存回收, 内存分配时, 也就不用考虑内存碎片等复杂情况.

  现在的虚拟机都是采用这种收集算法来回收新生代. 将内存分为一块较大的Eden空间和两块较小的Survivor空间, 每次使用Eden和其中一块Survivor. 每当回收时, 就讲还存活的对象一次性复制到另一块Survivor空间上, 最后清理掉Eden和刚才用过的Survivor空间.

    HotSpot虚拟机默认Eden和Survivor的大小是8:1. 由于新生代存活率较低, 复制代价较低, 并且只浪费10%的内存空间, 还是可以接受的.

 

 

 

  d) 标记整理算法 -- 这个用于老年代。

   老年代存活率较高,如果使用复制算法, 则复制代价比较高, 老年代一般使用"标记整理"算法, 这种算法和"标记清除"算法类似, 但标记之后, 并不是清除, 而是将存活的对象复制移到到一端, 然后将端边界外的对象清除掉.

 

 

  e) 分代收集算法 

    根据对象存活周期的不同将内存划分为几块, 一般是把java堆划分为新生代和老年代, 然后根据各年代的特点采用最适当的收集算法.

    在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,那就选用复制算法

    在老年代中,一般存活率较高,一般选用清除算法。

 

  

  4.2) 垃圾收集器

   4.2.1) 新生代

     1) Serial收集器 (串行)

  该收集器是只有一条线程的收集器, 垃圾回收时,必须停掉其他的线程, 即是发生STW(Stop the World)。适用于单个CPU的环境, 适用于桌面应用场景(client模式)

    

     2)ParNew收集器(并发)

   Serial的并发实现, 该收集器的控制参数丶对象分配丶回收策略等等都和Serial收集器一样.在JDK1.5中, 老年代使用CMS收集器时, 新生代只能搭配Serial或ParNew收集器中的一个. ParNew收集器是使用CMS收集器时(-XX: UseConcMarkSweepGC)默认的新生代收集器.

 

    3)Parallel Scavenge收集器(并发)

   该收集器的关注点和其他收集器的不同, CMS等收集器的关注点在于尽量缩短垃圾回收的停顿时间, 而这个收集器的目的是为了提供更大的吞吐量,如虚拟机运行100分钟,垃圾收集用了1分钟,它的吞吐量为99%。

  停顿时间越短, 越适合与用户交互的程序, 良好的响应速度能够提升用户体验. 而高吞吐量能够高效率的利用CPU时间,尽快的完成计算任务, 主要适合在后台计算而无须用户过多交互的任务.

 

  4.2.2) 老年代

    4)Serial Old收集器(串行)

      Serial Old收集器是Serial收集器的老年代版. 它是一个单线程的收集器, 使用"标记-整理"算法. 

      主要作用: 1. Client模式使用  2. Server模式, 搭配新生代Parallel Scavenge收集器使用. 3. 作为CMS的备选方案

 

 

    

    

     5) Parallel Old收集器(并发)

   Parallel Old收集器是Parallel Scavenge收集器的老年代版本, 使用多线程和"标记-整理"算法. 该收集器是JDK1.6版本才提供的,  JDK 1.5 提供的Parallel New收集器, 由于Serial Old的"拖累", 一直无法发挥"最大吞吐量"的优势, 直到1.6提供的Parallel Old收集器才是名副其实.

 

    6) CMS收集器(并发)

      CMS(Concurrent Mark Sweep)收集器是一种以最短回收停顿时间为目标的垃圾收集器. 

    CMS是基于"标记-清除"算法的, 分为一下四个步骤:

      a. 初始标记(STW)

      b. 并发标记

      c. 重新标记(STW)

      d. 并发清除     

 

     三个缺点:

    a. 对CPU资源比较敏感.在并发阶段, 它不会导致用户线程停顿, 但它会因占用一部分线程而导致应用程序变慢. CMS默认启动的回收线程数为(CPU数量+3)/4

    b. CMS处理器无法处理浮动垃圾. CMS并发标记清除阶段, 用户线程还在运行, 可能产生垃圾. 这些垃圾被成为"浮动垃圾".所以, 在进行垃圾回收时,也需要预留一些空间供给用户线程使用. JDK 1.5 默认设置, 当老年代使用了68%时, 就会触发GC. 如果应用程序老年代增长不会很快,可以使用-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比, 以降低内存回收次数从而获取更好的性能. JDK1.6, 启动阀值设置为92%. 如果CMS运行期间预留的内存无法满足程序需要, 就会出现一次"Concurrent Mode Failure" 失败, 这时虚拟机讲启动后背源: 临时启用Serial Old收集器来重新进行老年代的垃圾收集, 使得停顿时间更长了.  启动阀值设置过高, 很容器出现失败, 导致性能降低.

    c. 标记清除算法,会产生大量空间碎片。所以需要空间整理,而空间整理会产生STW

   可以使用-XX:+UseCMSCompactAtFullCollection参数来强制JVM在FGC完成后对老年代进行压缩,执行一次空间碎片整理。为减少STW的次数, 可以使用--XX:+CMSFullGCsBeforeCompaction=n参数,在执行了n次FGC后,JVM在在老年代执行空间碎片整理。

 

 

    7) G1收集器  

     G1是一款面向服务端应用的垃圾收集器.

 

    与其他GC收集器相比, G1具备如下特点:

  a) 并行与并发: G1 能充分利用多CPU丶多核环境下的硬件优势, 使用多个CPU来缩短STW挺短时间.

  b) 分代收集: 可以独立管理整个GC堆. 

  c) 空间整合: 从整体上看, 是基于"标记-整理"算法实现的收集器, 从局部(两个Region之间)上来看是基于"复制"算法实现的.G1运作期间不会产生内存空间碎片, 收集后能提供规整的可用内存, 利于程序长时间运行.

  d) 可预测的停顿: 这是G1相对于CMS的另一大优势, 降低停顿时间是G1和CMS共同的关注点, 但G1除了追求低停顿之外, 还能建立可预测的停顿时间模型, 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒, 这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了.

 

     G1收集器大概分为以下几个步骤:

      a) 初始标记

      b) 并发标记

      c) 最终标记

      d) 筛选回收

 

 

 

五丶参数总结

 

 

 

学习资料:

  <深入理解java虚拟机>

  <精尽 Java 虚拟机 | 芋道源码>

 

posted @ 2019-08-26 01:00  timfruit  阅读(199)  评论(0编辑  收藏  举报