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;//静态变量用于快速访问BarrierSetstatic 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,  // undefinedmtClass             = 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 NMTmtTracing           = 0x0F00,  // memory used for Tracingmt_number_of_types  = 0x000F,  // number of memory types (mtDontTrack// is not included as validate type)mtDontTrack         = 0x0F00,  // memory we do not or cannot trackmt_masks            = 0x7F00,

线程

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,                 // 等待一个MonitorCONDVAR_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的构造函数和析构函数,它们的主要工作就是构建句柄链表,代码如下所示:

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

  1. 《看透springmvc源码分析与实践》读书笔记二

    域名服务器DNS 专门将域名解析为IP的服务器. TCP/IP协议 tcp在传输之前会进行三次沟通,一般称为"三次握手", 传完数据断开的时候要进行四次沟通,一般称为"四 ...

  2. 《看透springmvc源码分析与实践》读书笔记一

    解决速度问题的核心是解决海量数据操作问题和高并发问题. 网站复杂的架构就是从这两个问题演变出来的. 海量数据的解决方案: 1. 缓存和页面静态化 将从数据库获取的数据暂时保存起来,在下次使用的时候无需 ...

  3. 嵌入式之uboot源码分析-启动第一阶段学习笔记

    注: 以下的内容来自朱老师物联网大讲堂uboot部分课件 Uboot启动第一阶段start.S执行步骤 1.头文件包含 <config.h>(x210的各种宏定义) <version ...

  4. AFL源码分析之afl-clang-fast(学习笔记)

    前言 通过afl-gcc来插桩这种做法已经属于不建议,更好的就是afl-clang-fast工具是通过llvm pass来插桩. #ifdef 是判断某个宏是否被定义,若已定义,执行随后的语句 #en ...

  5. AFL源码分析之afl-fuzz(学习笔记)(一)

    文章目录 一.源码 1.信号处理函数 2.check_asan_opts(检查内存错误) 3.fix_up_sync(检查ID.sync_id是否过长,检查互斥) 4.save_cmdline(将当前 ...

  6. AFL源码分析之afl-fuzz(学习笔记)(二)

    文章目录 前言 1.shmget(key_t key, size_t size, int shmflg)函数 2.shmat(int shm_id, const void *shm_addr, int ...

  7. Nginx源码安装及调优配置

    由于Nginx本身的一些优点,轻量,开源,易用,越来越多的公司使用nginx作为自己公司的web应用服务器,本文详细介绍nginx源码安装的同时并对nginx进行优化配置. Nginx编译前的优化 [ ...

  8. Nginx源码安装及调优配置(二)

    Nginx运行进程个数,一般我们设置CPU的核心或者核心数x2,如果你不了解,top命令之后按1也可以看出来(一般直接追到线程即可) [root@linuxprobe ~]# vim /usr/loc ...

  9. Mybatis概念以及源码分析

    1.什么是 Mybatis? (1)Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关 注 SQL 语句本身,不需要花费精力去处理加载驱动.创建连接.创建 s ...

  10. java调用dubbo服务器_dubbo源码分析-服务端注册流程-笔记

    前面,我们已经知道,基于spring这个解析入口,到发布服务的过程,接着基于DubboProtocol去发布,最终调用Netty的api创建了一个NettyServer. 那么继续沿着Registry ...

最新文章

  1. Appro DM8127 IPNC 挂载NFS遇到的问题及解决
  2. go hive skynet_MMORPG游戏服务器技术选型参考-Go语言中文社区
  3. java记事本应用程序_Java教程:使用记事本编写运行Java程序
  4. PURE DORM IS GREAT
  5. struts2 Action 通过Spring管理, 并通过Spring的方式读取配置文件
  6. 《Programming WPF》翻译 第9章 6.我们进行到哪里了?
  7. ad转换器工作原理_AD转换中参考电压的作用
  8. 华为双系统是鸿蒙系统吗,华为p50pro是鸿蒙系统吗-华为p50pro有双系统吗
  9. 【tensorflow】tensorflow -gpu安装及jupyter环境更改
  10. tcp的无延时发送_高并发架构的TCP知识介绍
  11. JSP指令、动作和对象
  12. vue数据未加载完成前显示loading遮罩
  13. IDM chrome插件找不到
  14. Mac OS使用FFmpeg进行视频H264,H265编码
  15. java时间段的查询_JAVA实现按时间段查询数据操作的方法
  16. python大数据工程师薪资待遇_2019年就业薪资,凭什么大数据工程师遥遥领先?...
  17. 基于SSM实现在线考试系统
  18. Pixel 5 root 详细过程
  19. pdfFactory如何设置限制打印和浏览文档权限
  20. 需求分析——“中”的思想

热门文章

  1. Mybatis多表新增
  2. 将.bat文件设置为Window系统开机自启动项
  3. cookie用法--抽屉网的自动登录(cookie是通过代码自动获取的)
  4. VMware虚拟机net模式无法共享主机ip
  5. 分享一个好的清理系统垃圾软件
  6. 空间滤波 - 钝化掩蔽和高提升滤波
  7. 【JZOJ3424】粉刷匠
  8. Mac 无法打开淘宝,天猫,京东等
  9. 用计算机中的知识秀恩爱,教你用专业知识取情侣网名
  10. ckeditor+ckfinder