Garbage Collection Java-depth understanding of

premise

The recent large traffic volume system, from production logs GC (binding of Pinpoint) point of view, need to be part of the system tuning GC. However, in view of the past it is not designed to do this one, but have been scattered accumulation here to do a relatively comprehensive summary. This article only for HotSpot VMthat is Oracle Hotspot VMor OpenJDK Hotspot VM, version Java8, other VM does not necessarily apply.

What is the GC (Garbage Collection)

Garbage CollectionCan be translated as "garbage collection" - generally considered to be subjective approach is: to find garbage, then threw out the garbage. In the VM, GC implementation process on the contrary, GC purpose is to track all objects being used, and the remaining objects marked as spam, then the object is marked as junk is cleared, garbage recycling these objects occupy memory, in order to achieve automatic memory management.

Generational hypothesis (Generational Hypothesis)

name specific contents
Generational weak hypothesis (Weak Generational Hypothesis) Most objects death at a young age
Strong generational hypothesis (Strong Generational Hypothesis) Older an object is, the less likely to die

Weak generational hypothesis has been in a variety of different types of programming paradigms or programming language confirmed, and strong generational hypothesis evidence currently available is not sufficient, opinions still controversial.

Generational garbage collector is designed primarily to reduce the dwell time of the recovery process, while improving the throughput of the space. If the object replication algorithm for the young generation to recover, then the desired pause time largely depends on secondary recovery ( Minor Collectiontotal amount of live objects after), and this in turn depends on the overall value of the young generation space.

If the young generation of the whole space is too small, although once the recovery process is relatively fast, but due to the interval between two recovery is too short, the young generation objects may not have enough time "to reach the dead", thus leading to the recovery of small memory , may lead to the following conditions:

  • Young generation too often the object recovered and survived the need to copy the number of objects increases, increasing the garbage collector thread pause and scan stacks on its overhead data.
  • Will enhance a greater proportion of the young generation will cause the object to the old year old's are quickly filled, it will affect the garbage collection rate for the entire stack.
  • Much evidence shows that changes to the new generation of objects more frequently than modify an object's old, prematurely if the target was promoted to the young generation's old, so a large number of update operations ( mutation) will write barrier evaluators bring relatively large pressure.
  • Promotion will be the object of making the work program of the collection becomes sparse.

Generational garbage collector designers of the above aspects of an art of balance:

  1. To try to speed up the recovery of the secondary speed.
  2. To minimize the cost of secondary recovery.
  3. To reduce the cost of a higher recovery of primary recovery ( Major Collection).
  4. To properly reduce memory management overhead of evaluators.

Based on the weak generational hypothesis , JVM heap in the memory into the young generation ( Young Generation ) and the old year ( Old Generation ), while old's sometimes also called Tenured .

j-v-m-g-c-s-2.png

JVM provides different garbage collection algorithm for different generational. In fact, an object between different generational likely to refer to each other, these objects are referenced in generational garbage collection time will be considered GC Roots(see next section analysis). Weak generational hypothesis is possible in certain scenarios for some applications are not applicable; and GC algorithm optimized for the younger generation of old age or an object, the object to have "moderate" life expectancy, JVM's garbage collection performance is the relative disadvantage.

Object sentenced to live algorithm

JVM is by reachability algorithm ( Reachability Analysis) to determine whether the object alive. The basic idea of this algorithm is: called by a series of GC Roots(GC根集合)references active as a starting point, to start the search from the set of nodes down, searching traversed path is called the chain of references ( Reference Chain), when an object to GC Rootsnot have any reference chain when connected, the object description is not reachable.

j-v-m-g-c-s-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算法的时候去理解垃圾收集如何在不同的内存池空间中发挥其职责。

j-v-m-g-c-s-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卡片标记)的方式解决了这个问题,这里不对卡片标记的细节实现进行展开。

j-v-m-g-c-s-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幸存者区域会被清空,如此循环往复。

j-v-m-g-c-s-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不可达的对象。
  • 通过把存活的对象连续地复制到老年代内存空间的开头(也就是起始地址的一端)以压缩老年代内存空间的内容,这个过程主要包括显式的内存压缩从而避免过多的内存碎片。

j-v-m-g-c-s-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版本 -

j-v-m-g-c-s-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

j-v-m-g-c-s-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)阶段:收集器把所有未标记的对象进行清理和回收。

j-v-m-g-c-s-9.png

Mark-Sweep-Compact算法

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

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

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

j-v-m-g-c-s-10.png

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

Mark-Copy算法

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

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

j-v-m-g-c-s-11.png

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

小结

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

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

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

参考资料:

  • 《深入理解Java虚拟机-2nd》
  • 《The Garbage Collection Handbook》
  • 知乎-RednaxelaFX部分回答
  • Java Garbage Collection handbook
  • OpenJDK HotSpot VMPart of the source code

Description link

Guess you like

Origin www.cnblogs.com/throwable/p/10993090.html