Java教程:9种常见的CMS GC问题详解(七)

开课吧开课吧锤锤2021-03-23 10:40

    Java编程语言是一种简单、面向对象、分布式、解释型、健壮安全、与系统无关、可移植、高性能、多线程和动态的语言。如今Java已经广泛应用于各个领域的编程开发。

    4.3场景三:MetaSpace区OOM

    4.3.1现象

    JVM在启动后或者某个时间点开始,MetaSpace的已使用大小在持续增长,同时每次GC也无法释放,调大MetaSpace空间也无法彻底解决。

    4.3.2原因

    在讨论为什么会OOM之前,我们先来看一下这个区里面会存什么数据,Java7之前字符串常量池被放到了Perm区,所有被intern的String都会被存在这里,由于String.intern是不受控的,所以-XX:MaxPermSize的值也不太好设置,经常会出现java.lang.OutOfMemoryError:PermGenspace异常,所以在Java7之后常量池等字面量(Literal)、类静态变量(ClassStatic)、符号引用(SymbolsReference)等几项被移到Heap中。而Java8之后PermGen也被移除,取而代之的是MetaSpace。

    在最底层,JVM通过mmap接口向操作系统申请内存映射,每次申请2MB空间,这里是虚拟内存映射,不是真的就消耗了主存的2MB,只有之后在使用的时候才会真的消耗内存。申请的这些内存放到一个链表中VirtualSpaceList,作为其中的一个Node。

    在上层,MetaSpace主要由KlassMetaspace和NoKlassMetaspace两大部分组成。

    KlassMetaSpace:就是用来存Klass的,就是Class文件在JVM里的运行时数据结构,这部分默认放在CompressedClassPointerSpace中,是一块连续的内存区域,紧接着Heap。CompressedClassPointerSpace不是必须有的,如果设置了-XX:-UseCompressedClassPointers,或者-Xmx设置大于32G,就不会有这块内存,这种情况下Klass都会存在NoKlassMetaspace里。

    NoKlassMetaSpace:专门来存Klass相关的其他的内容,比如Method,ConstantPool等,可以由多块不连续的内存组成。虽然叫做NoKlassMetaspace,但是也其实可以存Klass的内容,上面已经提到了对应场景。

    具体的定义都可以在源码shared/vm/memory/metaspace.hpp中找到:

class Metaspace : public AllStatic {

  friend class MetaspaceShared;

 public:
  enum MetadataType {
    ClassType,
    NonClassType,
    MetadataTypeCount
  };
  enum MetaspaceType {
    ZeroMetaspaceType = 0,
    StandardMetaspaceType = ZeroMetaspaceType,
    BootMetaspaceType = StandardMetaspaceType + 1,
    AnonymousMetaspaceType = BootMetaspaceType + 1,
    ReflectionMetaspaceType = AnonymousMetaspaceType + 1,
    MetaspaceTypeCount
  };

 private:

  // Align up the word size to the allocation word size
  static size_t align_word_size_up(size_t);

  // Aligned size of the metaspace.
  static size_t _compressed_class_space_size;

  static size_t compressed_class_space_size() {
    return _compressed_class_space_size;
  }

  static void set_compressed_class_space_size(size_t size) {
    _compressed_class_space_size = size;
  }

  static size_t _first_chunk_word_size;
  static size_t _first_class_chunk_word_size;

  static size_t _commit_alignment;
  static size_t _reserve_alignment;
  DEBUG_ONLY(static bool   _frozen;)

  // Virtual Space lists for both classes and other metadata
  static metaspace::VirtualSpaceList* _space_list;
  static metaspace::VirtualSpaceList* _class_space_list;

  static metaspace::ChunkManager* _chunk_manager_metadata;
  static metaspace::ChunkManager* _chunk_manager_class;

  static const MetaspaceTracer* _tracer;
}

    MetaSpace的对象为什么无法释放,我们看下面两点:

    MetaSpace内存管理:类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。每个加载器有单独的存储空间,通过ClassLoaderMetaspace来进行管理SpaceManager*的指针,相互隔离的。

    MetaSpace弹性伸缩:由于MetaSpace空间和Heap并不在一起,所以这块的空间可以不用设置或者单独设置,一般情况下避免MetaSpace耗尽VM内存都会设置一个MaxMetaSpaceSize,在运行过程中,如果实际大小小于这个值,JVM就会通过-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio两个参数动态控制整个MetaSpace的大小,具体使用可以看MetaSpaceGC::compute_new_size()方法(下方代码),这个方法会在CMSCollector和G1CollectorHeap等几个收集器执行GC时调用。这个里面会根据used_after_gc,MinMetaspaceFreeRatio和MaxMetaspaceFreeRatio这三个值计算出来一个新的_capacity_until_GC值(水位线)。然后根据实际的_capacity_until_GC值使用MetaspaceGC::inc_capacity_until_GC()和MetaspaceGC::dec_capacity_until_GC()进行expand或shrink,这个过程也可以参照场景一中的伸缩模型进行理解。

void MetaspaceGC::compute_new_size() {
  assert(_shrink_factor <= 100, "invalid shrink factor");
  uint current_shrink_factor = _shrink_factor;
  _shrink_factor = 0;
  const size_t used_after_gc = MetaspaceUtils::committed_bytes();
  const size_t capacity_until_GC = MetaspaceGC::capacity_until_GC();

  const double minimum_free_percentage = MinMetaspaceFreeRatio / 100.0;
  const double maximum_used_percentage = 1.0 - minimum_free_percentage;

  const double min_tmp = used_after_gc / maximum_used_percentage;
  size_t minimum_desired_capacity =
    (size_t)MIN2(min_tmp, double(max_uintx));
  // Don't shrink less than the initial generation size
  minimum_desired_capacity = MAX2(minimum_desired_capacity,
                                  MetaspaceSize);

  log_trace(gc, metaspace)("MetaspaceGC::compute_new_size: ");
  log_trace(gc, metaspace)("    minimum_free_percentage: %6.2f  maximum_used_percentage: %6.2f",
                           minimum_free_percentage, maximum_used_percentage);
  log_trace(gc, metaspace)("     used_after_gc       : %6.1fKB", used_after_gc / (double) K);


  size_t shrink_bytes = 0;
  if (capacity_until_GC < minimum_desired_capacity) {
    // If we have less capacity below the metaspace HWM, then
    // increment the HWM.
    size_t expand_bytes = minimum_desired_capacity - capacity_until_GC;
    expand_bytes = align_up(expand_bytes, Metaspace::commit_alignment());
    // Don't expand unless it's significant
    if (expand_bytes >= MinMetaspaceExpansion) {
      size_t new_capacity_until_GC = 0;
      bool succeeded = MetaspaceGC::inc_capacity_until_GC(expand_bytes, &new_capacity_until_GC);
      assert(succeeded, "Should always succesfully increment HWM when at safepoint");

      Metaspace::tracer()->report_gc_threshold(capacity_until_GC,
                                               new_capacity_until_GC,
                                               MetaspaceGCThresholdUpdater::ComputeNewSize);
      log_trace(gc, metaspace)("    expanding:  minimum_desired_capacity: %6.1fKB  expand_bytes: %6.1fKB  MinMetaspaceExpansion: %6.1fKB  new metaspace HWM:  %6.1fKB",
                               minimum_desired_capacity / (double) K,
                               expand_bytes / (double) K,
                               MinMetaspaceExpansion / (double) K,
                               new_capacity_until_GC / (double) K);
    }
    return;
  }

  // No expansion, now see if we want to shrink
  // We would never want to shrink more than this
  assert(capacity_until_GC >= minimum_desired_capacity,
         SIZE_FORMAT " >= " SIZE_FORMAT,
         capacity_until_GC, minimum_desired_capacity);
  size_t max_shrink_bytes = capacity_until_GC - minimum_desired_capacity;

  // Should shrinking be considered?
  if (MaxMetaspaceFreeRatio < 100) {
    const double maximum_free_percentage = MaxMetaspaceFreeRatio / 100.0;
    const double minimum_used_percentage = 1.0 - maximum_free_percentage;
    const double max_tmp = used_after_gc / minimum_used_percentage;
    size_t maximum_desired_capacity = (size_t)MIN2(max_tmp, double(max_uintx));
    maximum_desired_capacity = MAX2(maximum_desired_capacity,
                                    MetaspaceSize);
    log_trace(gc, metaspace)("    maximum_free_percentage: %6.2f  minimum_used_percentage: %6.2f",
                             maximum_free_percentage, minimum_used_percentage);
    log_trace(gc, metaspace)("    minimum_desired_capacity: %6.1fKB  maximum_desired_capacity: %6.1fKB",
                             minimum_desired_capacity / (double) K, maximum_desired_capacity / (double) K);

    assert(minimum_desired_capacity <= maximum_desired_capacity,
           "sanity check");

    if (capacity_until_GC > maximum_desired_capacity) {
      // Capacity too large, compute shrinking size
      shrink_bytes = capacity_until_GC - maximum_desired_capacity;
      shrink_bytes = shrink_bytes / 100 * current_shrink_factor;

      shrink_bytes = align_down(shrink_bytes, Metaspace::commit_alignment());

      assert(shrink_bytes <= max_shrink_bytes,
             "invalid shrink size " SIZE_FORMAT " not <= " SIZE_FORMAT,
             shrink_bytes, max_shrink_bytes);
      if (current_shrink_factor == 0) {
        _shrink_factor = 10;
      } else {
        _shrink_factor = MIN2(current_shrink_factor * 4, (uint) 100);
      }
      log_trace(gc, metaspace)("    shrinking:  initThreshold: %.1fK  maximum_desired_capacity: %.1fK",
                               MetaspaceSize / (double) K, maximum_desired_capacity / (double) K);
      log_trace(gc, metaspace)("    shrink_bytes: %.1fK  current_shrink_factor: %d  new shrink factor: %d  MinMetaspaceExpansion: %.1fK",
                               shrink_bytes / (double) K, current_shrink_factor, _shrink_factor, MinMetaspaceExpansion / (double) K);
    }
  }

  // Don't shrink unless it's significant
  if (shrink_bytes >= MinMetaspaceExpansion &&
      ((capacity_until_GC - shrink_bytes) >= MetaspaceSize)) {
    size_t new_capacity_until_GC = MetaspaceGC::dec_capacity_until_GC(shrink_bytes);
    Metaspace::tracer()->report_gc_threshold(capacity_until_GC,
                                             new_capacity_until_GC,
                                             MetaspaceGCThresholdUpdater::ComputeNewSize);
  }
}

    由场景一可知,为了避免弹性伸缩带来的额外GC消耗,我们会将-XX:MetaSpaceSize和-XX:MaxMetaSpaceSize两个值设置为固定的,但是这样也会导致在空间不够的时候无法扩容,然后频繁地触发GC,最终OOM。所以关键原因就是ClassLoader不停地在内存中load了新的Class,一般这种问题都发生在动态类加载等情况上。

    4.3.3策略

    了解大概什么原因后,如何定位和解决就很简单了,可以dump快照之后通过JProfiler或MAT观察Classes的Histogram(直方图)即可,或者直接通过命令即可定位,jcmd打几次Histogram的图,看一下具体是哪个包下的Class增加较多就可以定位了。不过有时候也要结合InstBytes、KlassBytes、Bytecodes、MethodAll等几项指标综合来看下。如下图便是笔者使用jcmd排查到一个Orika的问题。

jcmd <PID> GC.class_stats|awk '{print$13}'|sed  's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1

Java

    如果无法从整体的角度定位,可以添加-XX:+TraceClassLoading和-XX:+TraceClassUnLoading参数观察详细的类加载和卸载信息。

    4.3.4小结

    原理理解比较复杂,但定位和解决问题会比较简单,经常会出问题的几个点有Orika的classMap、JSON的ASMSerializer、Groovy动态加载类等,基本都集中在反射、Javasisit字节码增强、CGLIB动态代理、OSGi自定义类加载器等的技术点上。另外就是及时给MetaSpace区的使用率加一个监控,如果指标有波动提前发现并解决问题。

    以上内容由开课吧老师、新宇、湘铭、祥璞提供,更多Java教程尽在开课吧广场Java教程频道。

有用
分享