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