G1的基本概念(G1源码分析和调优读书笔记)

G1的基本概念

分区

分区(Heap Region, HR)或称堆分区,是G1堆和操作系统交互的最小管理单位。
G1的分区类型大致可以分为四类:

1.自由分区

2.新生代分区

3.大对象分区

4.老生代分区

其中新生代分区又可以分为Eden和Survivor;大对象分区又可以分为:大对象头分区和大对象连续分区。

堆分区默认大小计算方式 ↓

// 判断是否是设置过堆分区大小,如果有则使用;
//没有,则根据初始内存和最大分配内存,获得平均值,并根据HR的个数得到分区的大小,和分区的下限比较,取两者的最大值。
void HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size) {
  uintx region_size = G1HeapRegionSize;
  if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
    size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
    region_size = MAX2(average_heap_size / TARGET_REGION_NUMBER,
                       (uintx) MIN_REGION_SIZE);
  }

  //对region_size按2的幂次对齐,并且保证其落在上下限范围内。
  int region_size_log = log2_long((jlong) region_size);
  // Recalculate the region size to make sure it's a power of
  // 2. This means that region_size is the largest power of 2 that's
  // <= what we've calculated so far.
  region_size = ((uintx)1 << region_size_log);

//确保region_size落在[1MB,32MB]之间
  // Now make sure that we don't go over or under our limits.
  if (region_size < MIN_REGION_SIZE) {
    region_size = MIN_REGION_SIZE;
  } else if (region_size > MAX_REGION_SIZE) {
    region_size = MAX_REGION_SIZE;
  }
  
  
  // 根据region_size 计算一些变量,比如卡表大小
  // And recalculate the log.
  region_size_log = log2_long((jlong) region_size);


  // Now, set up the globals.
  guarantee(LogOfHRGrainBytes == 0, "we should only set it once");
  LogOfHRGrainBytes = region_size_log;

  guarantee(LogOfHRGrainWords == 0, "we should only set it once");
  LogOfHRGrainWords = LogOfHRGrainBytes - LogHeapWordSize;

  guarantee(GrainBytes == 0, "we should only set it once");
  // The cast to int is safe, given that we've bounded region_size by
  // MIN_REGION_SIZE and MAX_REGION_SIZE.
  GrainBytes = (size_t)region_size;

  guarantee(GrainWords == 0, "we should only set it once");
  GrainWords = GrainBytes >> LogHeapWordSize;
  guarantee((size_t) 1 << LogOfHRGrainWords == GrainWords, "sanity");

  guarantee(CardsPerRegion == 0, "we should only set it once");
  CardsPerRegion = GrainBytes >> CardTableModRefBS::card_shift;

按照默认值计算,G1可以管理的最大内存为
2048 X 32MB =64GB。假设设置xms=32G,xmx=128G,则每个堆分区的大小为32M,分区个数动态变化范围从1024到4096个。

region_size的一半以上的大对象直接进入老生代。

新生代大小

新生代大小指的是新生代内存空间的大小,前面提到的G1新生代大小按分区组织,首先需要计算整个新生代的大小。

如果G1推断出最大值和最小值相等,那么说明新生代不会动态变化,即代表G1在后续对新生代垃圾回收的时候可能不满足期望停顿的时间。

//初始化新生代大小参数,根据不同的jvm参数判断计算新生代大小,供后续使用。
G1YoungGenSizer::G1YoungGenSizer() : _sizer_kind(SizerDefaults), _adaptive_size(true),
        _min_desired_young_length(0), _max_desired_young_length(0)    {
//如果设置了NewRatio且同时设置NewSize或MaxNewSize的情况下,则NewRatio被忽略        
  if (FLAG_IS_CMDLINE(NewRatio)) {
    if (FLAG_IS_CMDLINE(NewSize) || FLAG_IS_CMDLINE(MaxNewSize)) {
      warning("-XX:NewSize and -XX:MaxNewSize override -XX:NewRatio");
    } else {
      _sizer_kind = SizerNewRatio;
      _adaptive_size = false;
      return;
    }
  }

//参数传递有问题,最小值大于最大值
  if (NewSize > MaxNewSize) {
    if (FLAG_IS_CMDLINE(MaxNewSize)) {
      warning("NewSize (" SIZE_FORMAT "k) is greater than the MaxNewSize (" SIZE_FORMAT "k). "
              "A new max generation size of " SIZE_FORMAT "k will be used.",
              NewSize/K, MaxNewSize/K, NewSize/K);
    }
    MaxNewSize = NewSize;
  }
  
  //根据参数计算分区个数
  if (FLAG_IS_CMDLINE(NewSize)) {
    _min_desired_young_length = MAX2((uint) (NewSize / HeapRegion::GrainBytes),
                                     1U);
    if (FLAG_IS_CMDLINE(MaxNewSize)) {
      _max_desired_young_length =
                             MAX2((uint) (MaxNewSize / HeapRegion::GrainBytes),
                                  1U);
      _sizer_kind = SizerMaxAndNewSize;
      _adaptive_size = _min_desired_young_length == _max_desired_young_length;
    } else {
      _sizer_kind = SizerNewSizeOnly;
    }
  } else if (FLAG_IS_CMDLINE(MaxNewSize)) {
    _max_desired_young_length =
                             MAX2((uint) (MaxNewSize / HeapRegion::GrainBytes),
                                  1U);
    _sizer_kind = SizerMaxNewSizeOnly;
  }
}

//使用G1NewSizePercent来计算新生代的最小值
uint G1YoungGenSizer::calculate_default_min_length(uint new_number_of_heap_regions) {
  uint default_value = (new_number_of_heap_regions * G1NewSizePercent) / 100;
  return MAX2(1U, default_value);
}

//使用G1MaxNewSizePercent来计算新生代的最大值
uint G1YoungGenSizer::calculate_default_max_length(uint new_number_of_heap_regions) {
  uint default_value = (new_number_of_heap_regions * G1MaxNewSizePercent) / 100;
  return MAX2(1U, default_value);
}

//这里根据不同的参数输入来计算大小
//recalculate_min_max_young_length在初始化时被调用,在堆空间改变时也会被调用
void G1YoungGenSizer::recalculate_min_max_young_length(uint number_of_heap_regions, uint* min_young_length, uint* max_young_length) {
  assert(number_of_heap_regions > 0, "Heap must be initialized");

  switch (_sizer_kind) {
    case SizerDefaults:
      *min_young_length = calculate_default_min_length(number_of_heap_regions);
      *max_young_length = calculate_default_max_length(number_of_heap_regions);
      break;
    case SizerNewSizeOnly:
      *max_young_length = calculate_default_max_length(number_of_heap_regions);
      *max_young_length = MAX2(*min_young_length, *max_young_length);
      break;
    case SizerMaxNewSizeOnly:
      *min_young_length = calculate_default_min_length(number_of_heap_regions);
      *min_young_length = MIN2(*min_young_length, *max_young_length);
      break;
    case SizerMaxAndNewSize:
      // Do nothing. Values set on the command line, don't update them at runtime.
      break;
    case SizerNewRatio:
      *min_young_length = number_of_heap_regions / (NewRatio + 1);
      *max_young_length = *min_young_length;
      break;
    default:
      ShouldNotReachHere();
  }

另一个问题,分配新的分区时何时拓展,一次拓展多少内存?
G1是自适应拓展空间的。

参数-XX:GCTimeRatio表示GC与应用耗费时间比,G1中默认为9,计算方式为_gc_overhead_perc = 100.0x(1.0/(1.0+GCTimeRatio)),即G1 GC时间与应用时间占比不超过10%时不需要动态拓展。

size_t G1CollectorPolicy::expansion_amount() {
//根据历史信息获取平均GC时间
  double recent_gc_overhead = recent_avg_pause_time_ratio() * 100.0;
  double threshold = _gc_overhead_perc;
  //G1 GC时间与应用时间占比超过阈值才需要动态扩展
  //这个阈值的值为10% 上文提过计算方式
  if (recent_gc_overhead > threshold) {
    // We will double the existing space, or take
    // G1ExpandByPercentOfAvailable % of the available expansion
    // space, whichever is smaller, bounded below by a minimum
    // expansion (unless that's all that's left.)
    const size_t min_expand_bytes = 1*M;
    size_t reserved_bytes = _g1->max_capacity();
    size_t committed_bytes = _g1->capacity();
    size_t uncommitted_bytes = reserved_bytes - committed_bytes;
    size_t expand_bytes;
    size_t expand_bytes_via_pct =
      uncommitted_bytes * G1ExpandByPercentOfAvailable / 100;
    expand_bytes = MIN2(expand_bytes_via_pct, committed_bytes);
    expand_bytes = MAX2(expand_bytes, min_expand_bytes);
    expand_bytes = MIN2(expand_bytes, uncommitted_bytes);

  ......
  } else {
    return 0;
  }
}
//G1内存拓展时间书后面部分会介绍

G1停顿预测模型

G1是一个响应优先的GC算法,用户可以设定期望停顿时间由参数MaxGCPauseMills控制,默认值为200ms。
G1会在这个目标停顿时间内完成垃圾回收的工作。

G1使用停顿预测模型来满足期望,预测逻辑基于衰减平均值和衰减标准差。

卡表和位图

GC最早引入卡表是为了对内存的引用关系做标记,从而根据引用关系快速遍历活跃对象。

可以借助位图的方式,记录内存块之间的引用关系。用一个位来描述一个字,我们只需要判定位图里面的位是否有1,有的话则认为发生了引用。



以位为粒度的位图能准确描述每一个字的引用关系,但是包含信息太少,只能描述两个状态:引用和未被引用。但是如果增加一个字节来描述状态,则位图需要256kb的空间,这个数字太大,开销占了25%。所以一个可能的做法是位图不再描述一个字,而是一个区域,JVM使用512字节作为单位,用一个字节描述512字节的引用关系。

G1中还使用了bitmap,用bitmap可以描述一个分区对另外一个分区的引用情况,也可以描述内存分配的情况。

并发标记时也使用了bitmap来描述对象的分配情况。

对象头

java代码首先被翻译成字节码(bytecode),在JVM执行时才能确定要执行函数的地址,如何实现java的多态调用,最直观的想法是把java对象映射成C++对象或者封装成C++对象,比如增加一个额外的对象头,里面指向一个对象,而这个对象存储了java代码的地址。


所以JVM设计了对象的数据结构来描述java对象,这个结构分为三块区域:对象头 、实例数据和对齐填充


而我们刚才提到的类似虚指针的东西就可以放在对象头中,而JVM设计者还利用对象头来描述更多信息,对象的锁信息、GC标记信息等。

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  
  //静态变量用于快速访问BarrierSet
  static BarrierSet* _bs;

1.标记信息


第一部分标记信息位于MarkOop。

以下三种情况时要保存对象头:
1.使用了偏向锁,并且偏向锁被设置了
2.对象被加锁了
3.对象被设置了hash_code

2.元数据信息


第二部分元数据信息字段指向的是Klass对象(Klass对象是元数据对象,如Instance Klass 描述java对象的类结构),这个字段也和垃圾回收有关系。

内存分配和管理

JVM通过操作系统的系统调用进行内存的申请,典型的就是mmap。

mmap使用PAGE_SIZE为单位来进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行映射。


操作系统对内存的分配管理典型的分为两个阶段:
保留和提交。

保留阶段告知系统从某一地址开始到后面的dwSize大小的连续虚拟内存需要供程序使用,进程其他分配内存的操作不得使用这段内存;

提交阶段将虚拟地址映射到对应的真实物理地址中,这样这块内存就可以正常使用。


JVM常见对象类型


ResourceObj:线程有一个资源空间,一般ResourceObj都位于这里。定义资源空间的目的是对JVM其他功能的支持,如CFG、在C1/C2优化时可能需要访问运行时信息(这些信息可以保存在线程的资源区)。


StackObj:栈对象,声明的对象使用栈管理。其实例对象并不提供任何功能,且禁止New/Delete操作。对象分配在线程栈中,或者使用自定义的栈容器进行管理。


ValueObj:值对象,该对象在堆对象需要进行嵌套时使用,简单地说就是对象分配的位置和宿主对象(即拥有)是一样的。


AllStatic: 静态对象,全局对象,只有一个。值得一提的是C++初始化没有通过规范保证,可能会有两个静态对象相互依赖的问题,初始化时可能会出错。JVM中很多静态对象初始化都是显示调用静态初始化函数。


MetaspaceObj: 元对象,比如InstanceKlass这样的元数据就是元对象。


CHeapObj:
这是堆空间的对象,由new/delete/free/malloc管理。其中包含的内容很多,比如java对象、InstanceOop(后面提到的G1对象分配出来的对象)。除了Java对象,还有其他的对象也在堆中。

  mtNone              = 0x0000,  // undefined
  mtClass             = 0x0100,  // JVM中java类
  mtThread            = 0x0200,  // JVM中线程对象
  mtThreadStack       = 0x0300,
  mtCode              = 0x0400,  // JVM中生成的编译代码
  mtGC                = 0x0500,  // GC的内存
  mtCompiler          = 0x0600,  // 编译器使用的内存
  mtInternal          = 0x0700,  // JVM中内部使用的类型,不属于上述类型。
                                
  mtOther             = 0x0800,  // 不是由JVM使用的内存
  mtSymbol            = 0x0900,  //符号表使用内存
  mtNMT               = 0x0A00,  // mNMT使用内存
  mtChunk             = 0x0B00,  // chunk用于缓存
  mtJavaHeap          = 0x0C00,  // Java 堆
  mtClassShared       = 0x0D00,  // 共享类数据
  mtTest              = 0x0E00,  // Test type for verifying NMT
  mtTracing           = 0x0F00,  // memory used for Tracing
  mt_number_of_types  = 0x000F,  // number of memory types (mtDontTrack
                                 // is not included as validate type)
  mtDontTrack         = 0x0F00,  // memory we do not or cannot track
  mt_masks            = 0x7F00,

线程

图片.png

JVM线程图 如上

JavaThread:就是要执行Java代码的线程,比如Java代码的启动会创建一个JavaThread运行;对于Java代码的启动,可以通过JNI_CreateJavaVM来创建一个JavaThread,而对于一般的Java线程,都是调用java.lang.thread中的start方法,这个方法通过JNI调用创建JavaThread对象,完成真正的线程创建。


CompilerThread:执行JIT的线程。


WatcherThread:执行周期性任务,JVM里面有很多周期性任务,例如内存管理中对小对象使用了ChunkPool,而这种管理需要周期性的清理动作Cleaner;JVM中内存抽样任务MemProf?ilerTask等都是周期性任务。


NameThread:是JVM内部使用的线程,分类如图2-1所示。


VMThread:JVM执行GC的同步线程,这个是JVM最关键的线程之一,主要是用于处理垃圾回收。简单地说,所有的垃圾回收操作都是从VMThread触发的,如果是多线程回收,则启动多个线程,如果是单线程回收,则使用VMThread进行。

VMThread提供了一个队列,任何要执行GC的操作都实现了VM_GC_Operation,在JavaThread中执行VMThread::execute(VM_GC_Operation)把GC操作放入到队列中,然后再用VMThread的run方法轮询这个队列就可以了。


当这个队列有内容的时候它就开始尝试进入安全点,然后执行相应的GC任务,完成GC任务后会退出安全点

ConcurrentGCThread:并发执行GC任务的线程,比如G1中的ConcurrentMark
Thread和ConcurrentG1RefineThread,分别处理并发标记和并发Refine,这两个线程将在混合垃圾收集和新生代垃圾回收中介绍。


WorkerThread

工作线程,在G1中使用了FlexibleWorkGang,这个线程是并行执行的(个数一般和CPU个数相关),所以可以认为这是一个线程池。

线程池里面的线程是为了执行任务(在G1中是G1ParTask),也就是做GC工作的地方。VMThread会触发这些任务的调度执行(其实是把G1ParTask放入到这些工作线程中,然后由工作线程进行调度)。

JVM线程状态

//新创建线程
case NEW        
: return "NEW";
//可运行或者正在运行
case RUNNABLE                 : return "RUNNABLE";
//调用Thread.sleep()进入睡眠
case SLEEPING                 : return "TIMED_WAITING (sleeping)";
//调用Object.wait()进入等待
case IN_OBJECT_WAIT           : return "WAITING (on object monitor)";
//调用Object.wait(long)进入等待且有过期时间
case IN_OBJECT_WAIT_TIMED     : return "TIMED_WAITING (on object monitor)";
//JVM内部调用LockSupport.park()进入等待
case PARKED                   : return "WAITING (parking)";
//JVM内部调用LockSupport.park()进入等待,且有过期时间
case PARKED_TIMED             : return "TIMED_WAITING (parking)";
//进入一个同步块
case BLOCKED_ON_MONITOR_ENTER : return "BLOCKED (on object monitor)";
//终止
case TERMINATED               : return "TERMINATED";
default                       : return "UNKNOWN";

操作系统的线程状态:

  ALLOCATED,                    // 分配了但未初始化
  INITIALIZED,                  // 初始化完未启动
  RUNNABLE,                     //  已经启动并可被执行或者正在运行
  MONITOR_WAIT,                 // 等待一个Monitor
  CONDVAR_WAIT,                 // 等待一个条件变量
  OBJECT_WAIT,                  // 通过调用Object.wait()等待对象
  BREAKPOINTED,                 //调式状态
  SLEEPING,                     // 通过Thread.sleep()进入睡眠
  ZOMBIE                        // 僵尸状态,等待回收
  

栈帧

栈帧(frame)在线程执行时和运行过程中用于保存线程的上下文数据,JVM设计了栈帧,这是垃圾回收中国最重要的根,栈帧的结构在不同的CPU中并不相同,在x86中代码如下所示:

  _pc = NULL;//程序计数器,指向下一个要执行的代码地址
  _sp = NULL;//栈顶指针
  _unextended_sp = NULL;//异常栈顶指针
  _fp = NULL;//栈底指针
  _cb = NULL;//代码块的地址
  _deopt_state = unknown;//这个字段描述从编译代码到解释代码反优化的状态

栈帧也和GC密切相关,在GC过程中,通常第一步就是遍历根,Java线程栈帧就是根元素之一,遍历整个栈帧的方式是通过StackFrameStream,其中封装了一个next指针,其原理和上述的代码一样通过sender来获得调用者的栈帧。

我们将Java的栈帧来作为根遍历堆,对对象进行标记并收集垃圾。

句柄

线程不但可以执行java代码,也可以执行本地代码(JVM里的代码)。JVM没有区分Java栈和本地方法栈,如果通过栈进行处理则必须要区分这两种情况。


JVM设计了handleArea,这是一块线程的资源区,在这个区域分配句柄并管理所有的句柄,如果函数还在调用中,那么句柄有效,句柄关联的对象也就是活跃对象。

为了管理句柄的生命周期,引入了HandleMark,通常HandleMark分配在栈上,在创建HandleMark的时候标记handleArea对象有效,在HandleMark对象析构的时候从HandleArea中删除对象的引用。

在HandleMark中标记Chunk的地址,这个就是找到当前本地方法代码中活跃的句柄,因此也就可以找到对应的活跃的OOP对象。下面是HandleMark的构造函数和析构函数,它们的主要工作就是构建句柄链表,代码如下所示:
图片.png

©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页