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;//静态变量用于快速访问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源码分析和调优读书笔记)相关推荐
- 《看透springmvc源码分析与实践》读书笔记二
域名服务器DNS 专门将域名解析为IP的服务器. TCP/IP协议 tcp在传输之前会进行三次沟通,一般称为"三次握手", 传完数据断开的时候要进行四次沟通,一般称为"四 ...
- 《看透springmvc源码分析与实践》读书笔记一
解决速度问题的核心是解决海量数据操作问题和高并发问题. 网站复杂的架构就是从这两个问题演变出来的. 海量数据的解决方案: 1. 缓存和页面静态化 将从数据库获取的数据暂时保存起来,在下次使用的时候无需 ...
- 嵌入式之uboot源码分析-启动第一阶段学习笔记
注: 以下的内容来自朱老师物联网大讲堂uboot部分课件 Uboot启动第一阶段start.S执行步骤 1.头文件包含 <config.h>(x210的各种宏定义) <version ...
- AFL源码分析之afl-clang-fast(学习笔记)
前言 通过afl-gcc来插桩这种做法已经属于不建议,更好的就是afl-clang-fast工具是通过llvm pass来插桩. #ifdef 是判断某个宏是否被定义,若已定义,执行随后的语句 #en ...
- AFL源码分析之afl-fuzz(学习笔记)(一)
文章目录 一.源码 1.信号处理函数 2.check_asan_opts(检查内存错误) 3.fix_up_sync(检查ID.sync_id是否过长,检查互斥) 4.save_cmdline(将当前 ...
- AFL源码分析之afl-fuzz(学习笔记)(二)
文章目录 前言 1.shmget(key_t key, size_t size, int shmflg)函数 2.shmat(int shm_id, const void *shm_addr, int ...
- Nginx源码安装及调优配置
由于Nginx本身的一些优点,轻量,开源,易用,越来越多的公司使用nginx作为自己公司的web应用服务器,本文详细介绍nginx源码安装的同时并对nginx进行优化配置. Nginx编译前的优化 [ ...
- Nginx源码安装及调优配置(二)
Nginx运行进程个数,一般我们设置CPU的核心或者核心数x2,如果你不了解,top命令之后按1也可以看出来(一般直接追到线程即可) [root@linuxprobe ~]# vim /usr/loc ...
- Mybatis概念以及源码分析
1.什么是 Mybatis? (1)Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关 注 SQL 语句本身,不需要花费精力去处理加载驱动.创建连接.创建 s ...
- java调用dubbo服务器_dubbo源码分析-服务端注册流程-笔记
前面,我们已经知道,基于spring这个解析入口,到发布服务的过程,接着基于DubboProtocol去发布,最终调用Netty的api创建了一个NettyServer. 那么继续沿着Registry ...
最新文章
- Appro DM8127 IPNC 挂载NFS遇到的问题及解决
- go hive skynet_MMORPG游戏服务器技术选型参考-Go语言中文社区
- java记事本应用程序_Java教程:使用记事本编写运行Java程序
- PURE DORM IS GREAT
- struts2 Action 通过Spring管理, 并通过Spring的方式读取配置文件
- 《Programming WPF》翻译 第9章 6.我们进行到哪里了?
- ad转换器工作原理_AD转换中参考电压的作用
- 华为双系统是鸿蒙系统吗,华为p50pro是鸿蒙系统吗-华为p50pro有双系统吗
- 【tensorflow】tensorflow -gpu安装及jupyter环境更改
- tcp的无延时发送_高并发架构的TCP知识介绍
- JSP指令、动作和对象
- vue数据未加载完成前显示loading遮罩
- IDM chrome插件找不到
- Mac OS使用FFmpeg进行视频H264,H265编码
- java时间段的查询_JAVA实现按时间段查询数据操作的方法
- python大数据工程师薪资待遇_2019年就业薪资,凭什么大数据工程师遥遥领先?...
- 基于SSM实现在线考试系统
- Pixel 5 root 详细过程
- pdfFactory如何设置限制打印和浏览文档权限
- 需求分析——“中”的思想