当前位置:主页 > 查看内容

JVM调优之垃圾定位、垃圾回收算法、垃圾处理器对比

发布时间:2021-06-11 00:00| 位朋友查看

简介:谈垃圾回收器之前,要先讲讲垃圾回收算法,以及JVM对垃圾的认定策略,JVM垃圾回收器是垃圾回收算法的具体实现,了解了前面的前置知识,有利于对垃圾回收器的理解。 什么是垃圾? 垃圾,主要是指堆上的对象,那么如何确定这些对象是可以被回收的呢? 大概思路就是,如果……

谈垃圾回收器之前,要先讲讲垃圾回收算法,以及JVM对垃圾的认定策略,JVM垃圾回收器是垃圾回收算法的具体实现,了解了前面的前置知识,有利于对垃圾回收器的理解。

什么是垃圾?

垃圾,主要是指堆上的对象,那么如何确定这些对象是可以被回收的呢?

大概思路就是,如果一个对象永远不可能被访问到,那么就是垃圾,可以被回收了如何确定对象永远不会被使用呢?

引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

如图,每一个对象的引用都是1,构成了循环引用,但是并不能被其他对象访问,这两个对象再无任何引用,引用计数算法也就无法回收它们。

代码验证:

  1. package com.courage; 
  2. public class ReferenceCountingGC { 
  3.     public Object instance = null
  4.     private static final int _1MB = 1024 * 1024
  5.     /** 
  6.      * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 
  7.      */ 
  8.     private byte[] bigSize = new byte[5* _1MB]; 
  9.     public static void testGC() { 
  10.         //5 M 
  11.         ReferenceCountingGC objA = new ReferenceCountingGC(); 
  12.         //5 M 
  13.         ReferenceCountingGC objB = new ReferenceCountingGC(); 
  14.         objA.instance = objB; 
  15.         objB.instance = objA; 
  16.         objA = null
  17.         objB = null
  18. // 假设在这行发生GC,objA和objB是否能被回收? 
  19.         System.gc(); 
  20.     } 
  21.     public static void main(String[] args) { 
  22.         testGC(); 
  23.     } 

执行结果:

  1. [0.004s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead. 
  2. [0.012s][info   ][gc,heap] Heap region size: 1M 
  3. [0.015s][info   ][gc     ] Using G1 
  4. [0.015s][info   ][gc,heap,coops] Heap address: 0x0000000701000000, size: 4080 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 
  5. ...... 
  6. [0.119s][info   ][gc,metaspace   ] GC(0) Metaspace: 805K->805K(1056768K) 
  7. [0.119s][info   ][gc             ] GC(0) Pause Full (System.gc()) 14M->0M(8M) 2.886ms 
  8. [0.119s][info   ][gc,cpu         ] GC(0) User=0.03s Sys=0.00s Real=0.00s 
  9. [0.120s][info   ][gc,heap,exit   ] Heap 
  10. ...... 

为了篇幅,我将部分打印内容省略了,可见System.gc()后内存占用由14M->0M,将对象这10M释放了。也就是JVM里面并没使用引用计数法来标记垃圾。

根可达算法

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的
    参数、局部变量、临时变量等在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  2. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  3. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  4. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
    NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  5. 所有被同步锁(synchronized关键字)持有的对象。
  6. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

垃圾回收算法

本文介绍了常见的三种垃圾回收算法(mark-sweep,mark-compact,mark-copy),是java虚拟机各种垃圾收集器的算法基础。

垃圾回收算法思想

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。

2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;

如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

标记-清除算法 Mark-Sweep

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

它的主要缺点有两个:

第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-复制 Mark-Copy

标记-复制算法常被简称为复制算法它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了。

标记-压缩 Mark-Compact

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记-压缩算法其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存:

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序(STW问题)才能进行 。

垃圾处理器

基于上面的三种垃圾回收算法,衍生出7种垃圾回收器:

Serial收集器

这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint) [1] 最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之

外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:

PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规

则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它

却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集

器,其中有一个与功能、性能无关但其实很重要的原因是:

除了Serial收集器外,目前只有它能与CMS

收集器配合工作,另一方面CMS的出现巩固了ParNew的地位


本文转载自网络,原文链接:http://www.cnblogs.com/Courage129/p/14365167.html
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐