のガベージコレクションJavaの深い理解

前提

生産からの最近の大規模な交通量のシステムは、システムのチューニングGCの一部である必要があり、ビューのGC(ピンポイントの結合)のポイントを記録します。しかし、過去のビューでは、このいずれかを実行するように設計されていませんが、比較的包括的な概要を行うためにここに蓄積するのを散乱されています。だけのためにこの記事HotSpot VMつまりOracle Hotspot VMたりOpenJDK Hotspot VM、バージョンJava8は、他のVMは必ずしも適用されません。

GC(ガベージコレクション)とは何ですか

Garbage Collection「ガベージコレクション」として翻訳することができます - 一般的に主観的なアプローチであると考えられている:ゴミを見つけるために、そしてゴミを投げました。ジャンクがクリアされると逆にVM、GCの実装プロセスでは、GCの目的は、使用されているすべてのオブジェクト、およびスパムとしてマークされた残りのオブジェクトを追跡することで、そのオブジェクトがマークされ、これらのオブジェクトのリサイクルゴミがメモリを占有し、自動メモリ管理を実現するためです。

世代仮説(仮説世代)

名前 具体的な内容
世代弱仮説(弱世代仮説) 若い年齢でほとんどのオブジェクト死
強い世代仮説(ストロング世代仮説) 古いオブジェクトは死ぬしにくく、あります

弱い世代仮説が確認されたプログラミングパラダイムやプログラミング言語の異なる様々な種類のにあった、との強い世代仮説の証拠は、現在利用可能なまだ議論の余地が十分で、意見ではありません。

世代別ガベージコレクタは、スペースのスループットを向上させながら、回復プロセスの滞留時間を短縮するために主に設計されています。若い世代のためのオブジェクトの複製アルゴリズムが回復する場合は、希望の休止時間は、主に二次復旧(に依存しMinor Collectionた後、ライブオブジェクトの合計量)、これが今度は若い世代空間の全体的な値に依存します。

、回復プロセスは比較的高速であるが、二つの回復の間隔が短すぎる起因して、若い世代のオブジェクトは「死者を達するための」十分な時間を持っていない可能性があり一度ものの、空間全体の若い世代は、小さすぎるので、小さなメモリの回復につながります、以下の条件につながることがあります。

  • 若い世代には、あまりにも頻繁に、オブジェクトは、ガベージコレクタスレッドポーズを増加させるオブジェクトの数が増加し、コピーし、そのオーバーヘッドデータにスタックをスキャンする必要性を回収し、生き残りました。
  • 古いのはすぐに満たされている古い年にオブジェクトを原因となります若い世代の大部分を強化する、それがスタック全体のためのガベージコレクション率に影響を与えます。
  • 多くの証拠は、オブジェクトのターゲットが更新操作の若い世代の古いので、多数(に昇格した場合は、古い、途中で修正するよりも頻繁にオブジェクトの新しい世代への変化を示しているmutation)書き込みますバリア評価者が比較的大きいを持参します圧力。
  • プロモーションは、コレクションの作業プログラムが疎になり作るの対象となります。

バランスの技術の上記の態様の世代別ガベージコレクタの設計:

  1. 二次の速度の回復をスピードアップしようとします。
  2. 二次復旧のコストを最小限に抑えます。
  3. 一次回収の高い回復のコストを削減するために(Major Collection)。
  4. 適切に評価者のメモリ管理のオーバーヘッドを減らすために。

基づいて、弱い世代仮説メモリに、JVMのヒープの若い世代(に若い世代)と古い年(旧世代の古い時々も呼ばれている間、)終身

jvmgcs-2.png

JVMは、異なる世代のためのさまざまなガベージコレクションのアルゴリズムを提供します。実際には、相互に参照する可能性が異なる世代間の目的は、これらのオブジェクトを考える世代別ガベージコレクション時に参照されるGC Roots(次のセクションの分析を参照します)。弱い世代仮説は適用されませんいくつかのアプリケーションのための特定のシナリオに可能であり、GCアルゴリズムは、老齢またはオブジェクトの若い世代のために最適化された、「中等度」の平均余命、JVMのガベージコレクションのパフォーマンスを持ってすることを目的とします相対的な欠点。

オブジェクトは、アルゴリズムを生きることを宣告します

JVMは、到達可能性アルゴリズム(によってあるReachability Analysis物体が生きているかどうかを決定するために)。このアルゴリズムの基本的な考え方は、次のとおりの一連によって呼び出さGC Roots(GC根集合)横断経路を探索する参照の鎖(と呼ばれ、ノードの集合から下に検索を開始するために、出発点としてアクティブ参照Reference Chain)、オブジェクトをするときにGC Roots任意の参照チェーンを持っていません接続時に、オブジェクトの説明は到達できません。

jvmgcs-1.png

GC Roots具体是指什么?这一点可以从HotSpot VMParallel Scavenge源码实现总结出来,参考jdk9分支的psTasks.hpppsTasks.cpp

// psTasks.hpp
class ScavengeRootsTask : public GCTask {
 public:
  enum RootType {
    universe              = 1,
    jni_handles           = 2,
    threads               = 3,
    object_synchronizer   = 4,
    flat_profiler         = 5,
    system_dictionary     = 6,
    class_loader_data     = 7,
    management            = 8,
    jvmti                 = 9,
    code_cache            = 10
  };
 private:
  RootType _root_type;
 public:
  ScavengeRootsTask(RootType value) : _root_type(value) {}

  char* name() { return (char *)"scavenge-roots-task"; }

  virtual void do_it(GCTaskManager* manager, uint which);
};

// psTasks.cpp
void ScavengeRootsTask::do_it(GCTaskManager* manager, uint which) {
  assert(ParallelScavengeHeap::heap()->is_gc_active(), "called outside gc");

  PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(which);
  PSScavengeRootsClosure roots_closure(pm);
  PSPromoteRootsClosure  roots_to_old_closure(pm);

  switch (_root_type) {
    case universe:
      Universe::oops_do(&roots_closure);
      break;

    case jni_handles:
      JNIHandles::oops_do(&roots_closure);
      break;

    case threads:
    {
      ResourceMark rm;
      Threads::oops_do(&roots_closure, NULL);
    }
    break;

    case object_synchronizer:
      ObjectSynchronizer::oops_do(&roots_closure);
      break;

    case flat_profiler:
      FlatProfiler::oops_do(&roots_closure);
      break;

    case system_dictionary:
      SystemDictionary::oops_do(&roots_closure);
      break;

    case class_loader_data:
    {
      PSScavengeKlassClosure klass_closure(pm);
      ClassLoaderDataGraph::oops_do(&roots_closure, &klass_closure, false);
    }
    break;

    case management:
      Management::oops_do(&roots_closure);
      break;

    case jvmti:
      JvmtiExport::oops_do(&roots_closure);
      break;


    case code_cache:
      {
        MarkingCodeBlobClosure each_scavengable_code_blob(&roots_to_old_closure, CodeBlobToOopClosure::FixRelocations);
        CodeCache::scavenge_root_nmethods_do(&each_scavengable_code_blob);
        AOTLoader::oops_do(&roots_closure);
      }
      break;

    default:
      fatal("Unknown root type");
  }

  // Do the real work
  pm->drain_stacks(false);
}

由于HotSpot VM的源码里面注释比较少,所以只能参考一些资料和源码方法的具体实现猜测GC Roots的具体组成:

  • Universe::oops_do:VM的一些静态数据结构里指向GC堆里的对象的活跃引用等等。
  • JNIHandles::oops_do:所有的JNI handle,包括所有的global handle和local handle。
  • Threads::oops_do:所有线程的虚拟机栈,具体应该是所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用,或者换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
  • ObjectSynchronizer::oops_do:所有被对象同步器关联的对象,看源码应该是ObjectMonitor中处于Block状态的对象,从Java代码层面应该是通过synchronized关键字加锁或者等待加锁的对象。
  • FlatProfiler::oops_do:所有线程的中的ThreadProfiler
  • SystemDictionary::oops_doSystem Dictionary,也就是系统字典,是记录了指向Klass,KEY是一个Entry,由KalssNameClassloader组成,实际上,YGC不会处理System Dictionary,但是会扫描System Dictionary,某些GC可能触发类卸载功能,可以这样理解:System Dictionary包含了所有的类加载器。
  • ClassLoaderDataGraph::oops_do:所有已加载的类或者已加载的系统类。
  • Management::oops_doMBean所持有的对象。
  • JvmtiExport::oops_doJVMTI导出的对象,断点或者对象分配事件收集器相关的对象。
  • CodeCache::scavenge_root_nmethods_do:代码缓存(Code Cache)。
  • AOTLoader::oops_do:AOT加载器相关,包括了AOT相关代码缓存。

还有其他有可能的引用:

StringTable::oops_do:所有驻留的字符串(StringTable中的)。

JVM中的内存池

JVM把内存池划分为多个区域,下面分别介绍每个区域的组成和基本功能,方便下面介绍GC算法的时候去理解垃圾收集如何在不同的内存池空间中发挥其职责。

jvmgcs-3.png

  • 年轻代(Young Generation):包括EdenSurvivor Spaces,而Survivor Spaces又等分为Survivor 0Survivor 1,有时候也称为fromto两个区。
  • 老年代(Old Generation):一般称为Tenured
  • 元空间:称为Metaspace,在Java8中VM已经移除了永久代Permanent Generation

Eden

伊甸园是地上的乐园,根据《圣经·旧约·创世纪》记载,神·耶和华照自己的形像造了人类的祖先男人亚当,再用亚当的一个肋骨创造了女人夏娃,并安置第一对男女住在伊甸园中。

Eden,也就是伊甸园,是一块普通的在创建对象的时候进行对象分配的内存区域。而Eden进一步划分为驻留在Eden空间中的一个或者多个Thread Local Allocation Buffer(线程本地分配缓冲区,简称TLAB)TLAB是线程独占的。JVM允许线程在创建大多数对象的时候直接在相应的TLAB中进行分配,这样可以避免多线程之间进行同步带来的性能开销。

当无法在TLAB中进行对象分配的时候(一般是缓冲区没有足够的空间),那么对象分配操作将会在Eden中共享的空间(Common Area)中进行。如果整个Eden都没有足够的空间,则会触发YGC(Young Generation Garbage Collection),以释放更多的Eden中的空间。触发YGC后依然没有足够的内存,那么对象就会在老年代中分配(一般这种情况称为分配担保(Handle Promotion),是有前置条件的)。

当垃圾回收器收集Eden的时候,会遍历所有相对于GC Roots可达的对象,并且标记它们是对象,这一阶段称为标记阶段。

这里还有一点需要注意的是:堆中的对象有可能跨代链接,也就是有可能年轻代中的对象被老年代中的对象持有(注:老年代中的对象被年轻代中的对象持有这种情况在YGC中不需要考虑),这个时候如果不遍历老年代的对象,那么就无法通过可达性算法分析这种被被老年代中的对象持有的年轻代对象是否可达。JVM中采用了Card Marking卡片标记)的方式解决了这个问题,这里不对卡片标记的细节实现进行展开。

jvmgcs-4.png

标记阶段完成后,Eden中所有存活的对象会被复制到幸存者空间(Survivor Spaces) 的其中一块空间。复制阶段完成后,整个Eden被认为是空的,可以重新用于分配更多其他的对象。这里采用的GC算法称为标记-复制(Mark and Copy) 算法:标记存活的对象,然后复制它们到幸存者空间(Survivor Spaces) 的其中一块空间,注意这里是复制,不是移动

关于Eden就介绍这么多,其中TLABCard Marking是JVM中的相对底层实现,大概知道即可。

Survivor Spaces

Survivor Spaces也就是幸存者空间,幸存者空间最常用的名称是fromto。最重要的一点是:幸存者空间中的两个区域总有一个区域是空的。

下一次YGC触发之后,空闲的那一块幸存者空间才会入驻对象。年轻代的所有存活的对象(包括Eden和非空的from幸存者区域中的存活对象),都会被复制到to幸存者区域,这个过程完成之后,to幸存者区域会存放着活跃的对象,而from幸存者区域会被清空。接下来,from幸存者区域和to幸存者区域的角色会交换,也就是下一轮YGC触发之后存活的对象会复制到from幸存者区域,而to幸存者区域会被清空,如此循环往复。

jvmgcs-5.png

上面提到的存活对象的复制过程在两个幸存者空间之间多次往复之后,某些存活的对象“年龄足够大”(经过多次复制还存活下来),则这些“年纪大的”对象就会晋升到老年代中,这些对象会从幸存者空间移动到老年代空间中,然后它们就驻留在老年代中,直到自身变为不可达。

如果对象在Eden中出生并且经过了第一次YGC之后依然存活,并且能够被Survivor Spaces容纳的话,对象将会被复制到Survivor Spaces并且对象年龄被设定为1。对象在Survivor Spaces中每经历一次YGC之后还能存活下来,则对象年龄就会增加1,当它的年龄增加到晋升老年代的年龄阈值,那么它就会晋升到老年代也就是被移动到老年代中。晋升老年代的年龄阈值的JVM参数是-XX:MaxTenuringThreshold=n

VM参数 功能 可选值 默认值
-XX:MaxTenuringThreshold=n Survivor Spaces存活对象晋升老年代的年龄阈值 1<= n <= 15 15

值得注意的是:JVM中设置-XX:MaxTenuringThreshold的默认值为最大可选值,也就是15。

JVM还具备动态对象年龄判断的功能,JVM并不是永远地要求存活对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor Spaces中相同年龄的所有对象的大小总和大于Survivor Spaces的一半,那么年龄大于或者等于该年龄的对象可以直接晋升到老年代,不需要等待对象年龄到达MaxTenuringThreshold,例如:

类型 占比 年龄 动作(MaxTenuringThreshold=15)
ObjectType-1 60% 5 下一次YGC如果存活直接晋升到老年代
ObjectType-2 1% 6 下一次YGC如果存活直接晋升到老年代
ObjectType-3 10% 4 下一次YGC如果存活对象年龄增加1

可以简单总结一下对象进入老年代的几种情况:

  • 多次YGC对象存活下来并且年龄到达设定的-XX:MaxTenuringThreshold=n导致对象晋升。
  • 因为动态对象年龄判断导致对象晋升。
  • 大对象直接进入老年代,这里大对象通常指需要大量连续内存的Java对象,最常见的就是大型的数组对象或者长度很大的字符串,因为年轻代完全有可能装不下这类大对象。
  • 年轻代空间不足的时候,老年代会进行空间分配担保,这种情况下对象也是直接在老年代分配。

Tenured

老年代(Old Generation)更多时候被称为Tenured,它的内存空间的实现一般会更加复杂。老年代空间一般要比年轻大大得多,它里面承载的对象一般不会是“内存垃圾”,侧面也说明老年代中的对象的回收率一般比较低。

老年代发生GC的频率一般情况下会比年轻代低,并且老年代中的大多数对象都被期望为存活的对象(也就是对象经历GC之后存活率比较高),因此标记和复制算法并不适用于老年代。老年代的GC算法一般是移动对象以最小化内存碎片。老年代的GC算法一般规则如下:

  • 通过GC Roots遍历和标记所有可达的对象。
  • 删除所有相对于GC Roots不可达的对象。
  • 通过把存活的对象连续地复制到老年代内存空间的开头(也就是起始地址的一端)以压缩老年代内存空间的内容,这个过程主要包括显式的内存压缩从而避免过多的内存碎片。

jvmgcs-6.png

Metaspace

在Java8之前JVM内存池中还定义了一块空间叫永久代(Permanent Generation),这块内存空间主要用于存放元数据例如Class信息等等,它还存放其他数据内容,例如驻留的字符串(字符串常量池)。实际上永久代曾经给Java开发者带来了很多麻烦,因为大多数情况下很难预测永久代需要设定多大的空间,因为开发者也很难预测元数据或者字符串常量池的具体大小,一旦分配的元数据等内容出现了失败就会遇到java.lang.OutOfMemoryError: Permgen space异常。排除内存溢出导致的java.lang.OutOfMemoryError异常,如果是正常情况下导致的异常,唯一的解决手段就是通过VM参数-XX:MaxPermSize=XXXXm增大永久代的内存,不过这样也是治标不治本。

因为元数据等内容是难以预测的,Java8中已经移除了永久代,新增了一块内存区域Metaspace(元空间),很多其他杂项(例如字符串常量池)都移动了Java堆中。Class定义信息等元数据目前是直接加载到元空间中。元空间是一片分配在机器本地内存(native memory)的内存区,它和承载Java对象的堆内存是隔离的。默认情况下,元空间的大小仅仅受限于机器本地内存可以分配给Java程序的极限值,这样基本可以避免因为添加新的类导致java.lang.OutOfMemoryError: Permgen space异常发生的场景。

VM参数 功能 可选值 默认值
XX:MetaspaceSize=Xm Metaspace扩容时触发FullGC的初始化阈值 - -
XX:MaxMetaspaceSize=Ym Metaspace的内存上限 - 接近于无穷大

常用内存池相关的VM参数

  • -Xmx-Xms
VM参数 功能 可选值 默认值
-Xmx 设置最大堆内存大小 有下限控制,视VM版本 -
-Xms 设置最小堆内存大小 有下限控制,视VM版本 -

jvmgcs-7.png


  • -Xmn-XX:NewRatio-XX:SurvivorRatio
VM参数 功能 可选值 默认值
-Xmn 设置年轻代内存大小 - -
-XX:NewRatio= 设置老年代和年轻代的内存大小比值,设置为4表示年轻代占堆内存的1/5 - 4
-XX:SurvivorRatio= 设置Eden和幸存者区域的内存大小比值,设置为8表示from:to:Eden=1:1:8 - 8

jvmgcs-8.png

GC类型

参考R大(RednaxelaFX)的知乎回答,其实在HotSpot VM的GC分类只有两大种:

  • Partial GC:也就是部分GC,不收集整个GC堆。
    • Young GC:只收集young gen的GC。
    • Old GC:只收集old gen的GC,目前只有CMS的concurrent collection是这个模式。
    • Mixed GC:收集整个young gen以及部分old gen的GC,目前只有G1有这个模式。
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

因为HotSpot VM发展多年,外界对GC的名词解读已经混乱,所以才出现了Minor GCMajor GCFull GC

Minor GC

Minor GC,也就是Minor Garbage Collection,直译为次级垃圾回收,它的定义相对清晰:发生在年轻代的垃圾回收就叫做Minor GC。Minor Garbage Collection处理过程中会发生:

  1. 当JVM无法为新的对象分配内存空间的时候,始终会触发Minor GC,常见的情况如Eden的内存已经满了,并且对象分配的发生率越高,Minor GC发生的频率越高。
  2. Minor GC期间,老年代中的对象会被忽略。老年代中的对象引用的年轻代的对象会被认为是GC Roots的一部分,在标记阶段会简单忽略年轻代对象中引用的老年代对象。
  3. Minor GC会导致Stop The World,表现为暂停应用线程。大多数情况下,Eden中的大多数对象都可以视为垃圾并且这些垃圾不会被复制到幸存者空间,这个时候Minor GC的停顿时间会十分短暂,甚至可以忽略不计。相反,如果Eden中有大量存活对象需要复制到幸存者空间,那么Minor GC的停顿时间会显著增加。

Major GC和Full GC

Major GC(Major Garbage Collection,可以直译为主垃圾收集)和Full GC目前是两个没有正式定义的术语,具体来说就是:JVM规范中或者垃圾收集研究论文中都没有明确定义Major GC或者Full GC。不过按照民间或者约定俗成,两者区别如下:

  • Major GC:对老年代进行垃圾收集。
  • Full GC:对整个堆进行垃圾收集 -- 包括年轻代和老年代。

实际上,GC过程是十分复杂的,而且很多Major GC都是由Minor GC触发的,所以要严格分割Major GC或者Minor GC几乎是不可能的。另一方面,现在垃圾收集算法像G1收集算法提供部分垃圾回收功能,侧面说明并不能单纯按照收集什么区域来划分GC的类型

上面的一些理论或者资料指明:与其讨论或者担心GC到底是Major GC或者是Minor GC,不如花更多精力去关注GC过程是否会导致应用的线程停顿或者GC过程是否能够和应用线程并发执行

常用的GC算法

下面分析一下目前Hotspot VM中比较常见的GC算法,因为G1算法相对复杂,这里暂时没有能力分析。

GC算法的目的

GC算法的目的主要有两个:

  1. 找出所有存活的对象,对它们进行标记。
  2. 移除所有无用的对象。

寻找存活的对象主要是基于GC Roots的可达性算法,关于标记阶段有几点注意事项:

  1. 标记阶段所有应用线程将会停顿(也就是Stop The World),应用线程暂时停顿保存其信息在还原点中(Safepoint)。
  2. 标记阶段的持续时间并不取决于堆中的对象总数或者是堆的大小,而是取决于存活对象的总数,因此增加堆的大小并不会显著影响标记阶段的持续时间。

标记阶段完成后的下一个阶段就是移除所有无用的对象,按照处理方式分为三种常见的算法:

  • Sweep -- 清理,也就是Mark and Sweep,标记-清理。
  • Compact -- 压缩,也就是Mark-Sweep-Compact,标记-清理-压缩。
  • Copy -- 复制,也就是Mark and Copy,标记-复制。

Mark-Sweep算法

Mark-Sweep算法,也就是标记-清理算法,是一种间接回收算法(Indirect Collection),它并非直接检测垃圾对象本身,而是先确定所有存活的对象,然后反过来判断其他对象是垃圾对象。主要包括标记和清理两个阶段,它是最简单和最基础的收集算法,主要包括两个阶段:

  • 第一阶段为追踪(trace)阶段:收集器从GC Roots开始遍历所有可达对象,并且对这些存活的对象进行标记(mark)。
  • 第二阶段为清理(sweep)阶段:收集器把所有未标记的对象进行清理和回收。

jvmgcs-9.png

Mark-Sweep-Compact算法

内存碎片化是非移动式收集算法无法解决的一个问题之一:尽管堆中有可用空间,但是内存管理器却无法找到一块连续内存块来满足较大对象的分配需求,或者花费较长时间才能找到合适的空闲内存空间。

Mark-Sweep-Compact算法,也就是标记-清理-压缩算法,也是一种间接回收算法(Indirect Collection),它主要包括三个阶段:

  • 标记阶段:收集器从GC Roots开始遍历所有可达对象,并且对这些存活的对象进行标记。
  • 清理阶段:收集器把所有未标记的对象进行清理和回收。
  • 压缩阶段:收集器把所有存活的对象移动到堆内存的起始端,然后清理掉端边界之外的内存空间。

jvmgcs-10.png

对堆内存进行压缩整理可以有效地降低内存外部碎片化(External Fragmentation)问题,这个是标记-清理-压缩算法的一个优势。

Mark-Copy算法

Mark-Copy算法,也就是标记-复制算法,和标记-清理-压缩算法十分相似,重要的区别在于:标记-复制算法在标记和清理完成之后,所有存活的对象会被复制到一个不同的内存区域 -- 幸存者空间。主要包括三个阶段:

  • 标记阶段:收集器从GC Roots开始遍历所有可达对象,并且对这些存活的对象进行标记。
  • 清理阶段:收集器把所有未标记的对象进行清理和回收 --- 实际上这一步可能是不存在的,因为存活对象指针被复制之后,原来指针所在的位置已经可以重新分配新的对象,可以不进行清理
  • 复制阶段:把所有存活的对象复制到Survivor Spaces中的某一块空间中。

jvmgcs-11.png

标记-复制算法可以避免内存碎片化的问题,但是它的代价比较大,因为用的是半区复制回收,区域可用内存为原来的一半。

小结

JVM和GC是Java开发者必须掌握的内容,包含的知识其实还是挺多的,本文也只是简单介绍了一些基本概念:

  • 分代假说。
  • Minor GC、Major GC和Full GC。
  • 内存池组成。
  • 常用的GC算法。

后面会分析一下GC收集器搭配和GC日志查看、JVM提供的工具等等。

参考资料:

説明リンク

おすすめ

転載: www.cnblogs.com/throwable/p/10993090.html