[Inside HotSpot] Serial垃圾回收器 (一) Full GC
Serial垃圾回收器Full GC
Serial垃圾回收器的Full GC使用标记-压缩(Mark-Compact)进行垃圾回收,该算法基于Donald E. Knuth提出的Lisp2算法,它会把所有存活对象滑动到空间的一端,所以也叫sliding compact。Full GC始于gc/serial/tenuredGeneration
的TenuredGeneration::collect,它会在GC前后记录一些日志,真正的标记压缩算法发生在GenMarkSweep::invoke_at_safepoint,我们可以使用-Xlog:gc*
得到该算法的流程:
GC(0) Pause Young (Allocation Failure)
GC(1) Pause Full (Allocation Failure)
GC(1) Phase 1: Mark live objects
GC(1) Phase 1: Mark live objects 1.136ms
GC(1) Phase 2: Compute new object addresses
GC(1) Phase 2: Compute new object addresses 0.170ms
GC(1) Phase 3: Adjust pointers
GC(1) Phase 3: Adjust pointers 0.435ms
GC(1) Phase 4: Move objects
GC(1) Phase 4: Move objects 0.208ms
标记-压缩分为四个阶段(如果是fastdebug版jvm,可以使用-Xlog:gc*=trace
得到更为详细的日志,不过可能详细过头了...),这篇文章将围绕四个阶段展开。
1. 阶段1:标记存活对象
第一阶段对应GC日志的GC(1) Phase 1: Mark live objects
JVM在process_string_table_roots()
和process_roots()
中会遍历所有类型的GC Root,然后使用XX::oops_do(root_closure)
从该GC Root出发标记所有存活对象。XX表示GC Root类型,root_closure表示标记存活对象的方法(闭包)。GC模块有很多闭包(closure),它们代表的是一段代码、一种行为。root_closure就是一个MarkSweep::FollowRootClosure
闭包。这个闭包很强大,给它一个对象,就能标记这个对象,迭代标记对象的成员,以及对象所在的栈的所有对象及其成员:
// hotspot\share\gc\serial\markSweep.cpp
void MarkSweep::FollowRootClosure::do_oop(oop* p) { follow_root(p); }
template <class T> inline void MarkSweep::follow_root(T* p) {
// 如果引用指向的对象不为空且未标记
T heap_oop = RawAccess<>::oop_load(p);
if (!CompressedOops::is_null(heap_oop)) {
oop obj = CompressedOops::decode_not_null(heap_oop);
if (!obj->mark_raw()->is_marked()) {
mark_object(obj); // 标记对象
follow_object(obj); // 标记对象的成员
}
}
follow_stack(); // 标记引用所在栈
}
// 如果对象是数组对象则标记数组,否则标记对象的成员
inline void MarkSweep::follow_object(oop obj) {
if (obj->is_objArray()) {
MarkSweep::follow_array((objArrayOop)obj);
} else {
obj->oop_iterate(&mark_and_push_closure);
}
}
// 标记引用所在的整个栈
void MarkSweep::follow_stack() {
do {
// 如果待标记栈不为空则逐个标记
while (!_marking_stack.is_empty()) {
oop obj = _marking_stack.pop();
follow_object(obj);
}
// 如果对象数组栈不为空则逐个标记
if (!_objarray_stack.is_empty()) {
ObjArrayTask task = _objarray_stack.pop();
follow_array_chunk(objArrayOop(task.obj()), task.index());
}
} while (!_marking_stack.is_empty() || !_objarray_stack.is_empty());
}
// 标记数组的类型的Class和数组成员,比如String[] p = new String[2]
// 对p标记会同时标记java.lang.Class,p[1],p[2]
inline void MarkSweep::follow_array(objArrayOop array) {
MarkSweep::follow_klass(array->klass());
if (array->length() > 0) {
MarkSweep::push_objarray(array, 0);
}
}
既然走到这里了不如看看JVM是如何标记对象的:
inline void MarkSweep::mark_object(oop obj) {
// 获取对象的mark word
markOop mark = obj->mark_raw();
// 设置gc标记
obj->set_mark_raw(markOopDesc::prototype()->set_marked());
// 垃圾回收器视情况保留对象的gc标志
if (mark->must_be_preserved(obj)) {
preserve_mark(obj, mark);
}
}
对象的mark work有32bits或者64bits,取决于CPU架构和UseCompressedOops:
// hotspot\share\oops\markOop.hpp
32 位mark lword:
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
最后的lock2位有不同含义:
[ptr | 00] locked ptr指向栈上真正的对象头
[header | 0 | 01] unlocked 普通对象头
[ptr | 10] monitor 膨胀锁
[ptr | 11] marked GC标记
原来垃圾回收标记就是对每个对象mark word最后两位置11,可是如果最后两位用于其他用途怎么办?比如这个对象的最后两位表示膨胀锁,那GC就不能对它进行标记了,所以垃圾回收器还需要视情况在额外区域保留对象的mark word(PreservedMark)。回到之前的话题,GC Root有很多,有的是我们耳熟能详的,有的则是略微少见:
- 所有已加载的类(
ClassLoaderDataGraph::roots_cld_do
) - 所有Java线程当前栈帧的引用和虚拟机内部线程(
Threads::possibly_parallel_oops_do
) - JVM内部使用的引用(
Universe::oopds_do
和SystemDictionary::oops_do
) - JNI handles(
JNIHandles::oops_do
) - 所有synchronized锁住的对象引用(
ObjectSynchronizer::oops_do
) - java.lang.management对象(
Management::oops_do
) - JVMTI导出(
JvmtiExport::oops_do
) - AOT代码的堆(
AOTLoader::oops_do
) - code cache(
CodeCache::blobs_do
) - String常量池(
StringTable::oops_do
)
它们都包含可进行标记的引用,会视情况进行单线程标记或者并发标记,JVM会使用CAS(Atomic::cmpxchg)自旋锁等待标记任务。如果任务全部完成,即标记线程和完成计数相等,就结束阻塞。当对象标记完成后jvm还会使用ref_processor()->process_discovered_references()
对弱引用,软引用,虚引用,final引用(重写了finialize()方法的引用)根据它们的Java语义做特殊处理,不过与算法本身没有太大关系,有兴趣的请自行了解。
2. 阶段2:计算对象新地址
计算对象新地址的思想是:从地址空间开始扫描,如果cur_obj指针指向已经GC标记过的对象,则将该对象的新地址设置为compact_top,然后compact_top推进,cur_obj推进,直至cur_obj到达地址空间结束。
计算新地址伪代码如下:
// 扫描堆空间
while(cur_obj<space_end){
if(cur_obj->is_gc_marked()){
// 如果cur_Obj当前指向已标记过的对象,就计算新的地址
int object_size += cur_obj->size();
cur_obj->new_address = compact_top;
compact_top = cur_obj;
cur_obj += object_size;
}else{
// 否则快速跳过未标记的连续空间
while(cur_obj<space_end &&!cur_obj->is_gc_marked()){
cur_obj += cur_obj->size();
}
}
}
有了上面的认识,对应到HotSpot实现也比较简单了。计算对象新地址的代码位于CompactibleSpace::scan_and_forward:
// hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_forward(SpaceType* space, CompactPoint* cp) {
...
// compact_top为对象新地址的起始
HeapWord* compact_top = cp->space->compaction_top();
DeadSpacer dead_spacer(space);
//最后一个标记对象
HeapWord* end_of_live = space->bottom();
// 第一个未标记对象
HeapWord* first_dead = NULL;
const intx interval = PrefetchScanIntervalInBytes;
// 扫描指针
HeapWord* cur_obj = space->bottom();
// 扫描终点
HeapWord* scan_limit = space->scan_limit();
// 扫描老年代
while (cur_obj < scan_limit) {
// 如果cur_obj指向已标记对象
if (space->scanned_block_is_obj(cur_obj) && oop(cur_obj)->is_gc_marked()) {
Prefetch::write(cur_obj, interval);
size_t size = space->scanned_block_size(cur_obj);
// 给cur_obj指向的对象设置新地址,并前移compact_top
compact_top = cp->space->forward(oop(cur_obj), size, cp, compact_top);
// cur_obj指针前移
cur_obj += size;
// 修改最后存活对象指针地址
end_of_live = cur_obj;
} else {
// 如果cur_obj指向未标记对象,则获取这片(可能连续包含未标记对象的)空间的大小
HeapWord* end = cur_obj;
do {
Prefetch::write(end, interval);
end += space->scanned_block_size(end);
} while (end < scan_limit && (!space->scanned_block_is_obj(end) || !oop(end)->is_gc_marked()));
// 如果需要减少对象移动频率
if (cur_obj == compact_top && dead_spacer.insert_deadspace(cur_obj, end)) {
oop obj = oop(cur_obj);
compact_top = cp->space->forward(obj, obj->size(), cp, compact_top);
end_of_live = end;
} else {
// 否则跳过未存活对象
*(HeapWord**)cur_obj = end;
// 如果first_dead为空则将这片空间设置为第一个未存活对象
if (first_dead == NULL) {
first_dead = cur_obj;
}
}
// cur_obj指针快速前移
cur_obj = end;
}
}
...
}
如果对象需要移动,cp->space->forward()
会将新地址放入对象的mark word里面。计算对象新地址里面有个小技巧可以参见上图图2,当扫描到连续多个未存活对象的时候,它把第一个未存活对象设置为该片区域结尾的指针,这样下一次扫描到第一个对象可以直接跳到区域尾,节约时间。
3. 阶段3:调整对象指针
第二阶段设置了所有对象的新地址,但是没有改变对象的相对地址和GC Root。比如GC Root指向对象A,B,C,这时候A、B、C都有新地址A',B',C',GC Root应该相应调整为指向A',B',C':
第三阶段就是干这件事的。还记得第一阶段GC Root的标记行为吗?
JVM在
process_string_table_roots()
和process_roots()
中会遍历所有类型的GC Root,然后使用XX::oops_do(root_closure)
从该GC Root出发标记所有存活对象。XX
表示GC Root类型,root_closure
表示标记存活对象的方法(闭包)。
第三阶段和第一阶段一样,只是第一阶段传递的root_closure表示标记存活对象的闭包(FollowRootClosure
),第三阶段传递的root_closure表示调整对象指针的闭包AdjustPointerClosure
:
// hotspot\share\gc\serial\markSweep.inline.hpp
inline void AdjustPointerClosure::do_oop(oop* p) { do_oop_work(p); }
template <typename T>
void AdjustPointerClosure::do_oop_work(T* p) { MarkSweep::adjust_pointer(p); }
template <class T> inline void MarkSweep::adjust_pointer(T* p) {
T heap_oop = RawAccess<>::oop_load(p);
if (!CompressedOops::is_null(heap_oop)) {
// 从地址p处得到对象
oop obj = CompressedOops::decode_not_null(heap_oop);
// 从对象mark word中得到新对象地址
oop new_obj = oop(obj->mark_raw()->decode_pointer());
if (new_obj != NULL) {
// 将地址p处设置为新对象地址
RawAccess<IS_NOT_NULL>::oop_store(p, new_obj);
}
}
}
AdjustPointerClosure
闭包会遍历所有GC Root然后调整对象指针,注意,这里和第一阶段有个重要不同是第一阶段传递的FollowRootClosure
闭包会从GC Root出发标记所有可达对象,但是AdjustPointerClosure
闭包只会标记GC Root出发直接可达的对象,
从对象出发寻找可达其他对象这一步是使用的另一个闭包GenAdjustPointersClosure
,它会调用CompactibleSpace::scan_and_adjust_pointers遍历整个堆空间然后调整存活对象的指针:
//hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_adjust_pointers(SpaceType* space) {
// 扫描指针
HeapWord* cur_obj = space->bottom();
// 最后一个标记对象
HeapWord* const end_of_live = space->_end_of_live;
// 第一个未标记对象
HeapWord* const first_dead = space->_first_dead;
const intx interval = PrefetchScanIntervalInBytes;
// 扫描老年代
while (cur_obj < end_of_live) {
Prefetch::write(cur_obj, interval);
// 如果扫描指针指向的对象是存活对象
if (cur_obj < first_dead || oop(cur_obj)->is_gc_marked()) {
// 调整该对象指针,调整方法和AdjustPointerClosure所用一样
size_t size = MarkSweep::adjust_pointers(oop(cur_obj));
size = space->adjust_obj_size(size);
// 指针前移
cur_obj += size;
} else {
// 否则扫描指针指向未存活对象,设置扫描指针为下一个存活对象,加速前移
cur_obj = *(HeapWord**)cur_obj;
}
}
}
4. 阶段4:移动对象
第四阶段传递GenCompactClosure
闭包,该闭包负责对象的移动:
移动的代码位于CompactibleSpace::scan_and_compact:
//hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_compact(SpaceType* space) {
verify_up_to_first_dead(space);
// 老年代起始位置
HeapWord* const bottom = space->bottom();
// 最后一个标记对象
HeapWord* const end_of_live = space->_end_of_live;
// 如果该区域所有对象都存活,或者没有任何对象,或者没有任何存活对象
// 就不需要进行移动
if (space->_first_dead == end_of_live && (bottom == end_of_live || !oop(bottom)->is_gc_marked())) {
clear_empty_region(space);
return;
}
const intx scan_interval = PrefetchScanIntervalInBytes;
const intx copy_interval = PrefetchCopyIntervalInBytes;
// 设置扫描指针cur_obj为空间底部
HeapWord* cur_obj = bottom;
// 跳到第一个存活的对象
if (space->_first_dead > cur_obj && !oop(cur_obj)->is_gc_marked()) {
cur_obj = *(HeapWord**)(space->_first_dead);
}
// 从空间开始到最后一个存活对象为截止进行扫描
while (cur_obj < end_of_live) {
// 如果cur_obj执行的对象未标记
if (!oop(cur_obj)->is_gc_marked()) {
// 扫描指针快速移动至下一个存活的对象(死对象的第一个word
// 存放了下一个存活对象的地址,这样就可以快速移动)
cur_obj = *(HeapWord**)cur_obj;
} else {
Prefetch::read(cur_obj, scan_interval);
size_t size = space->obj_size(cur_obj);
// 获取对象将要被移动到的新地址
HeapWord* compaction_top = (HeapWord*)oop(cur_obj)->forwardee();
Prefetch::write(compaction_top, copy_interval);
// 移动对象,并初始化对象的mark word
Copy::aligned_conjoint_words(cur_obj, compaction_top, size);
oop(compaction_top)->init_mark_raw();
// 扫描指针前移
cur_obj += size;
}
}
clear_empty_region(space);
}
[Inside HotSpot] Serial垃圾回收器 (一) Full GC的更多相关文章
- [Inside HotSpot] Serial垃圾回收器 (二) Minor GC
Serial垃圾回收器Minor GC 1. DefNewGeneration垃圾回收 新生代使用复制算法做垃圾回收,比老年代的标记-压缩简单很多,所有回收代码都位于DefNewGeneration: ...
- Hotspot JVM垃圾回收器
前两篇<JVM入门——运行时数据区><JVM常见垃圾回收算法>所提到的实际上JVM规范以及常用的垃圾回收算法,具体的JVM实现实际上不止一种,有JRockit.J9等待,当然最 ...
- HotSpot的垃圾回收器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现.这里讨论的收集器基于JDK 1.7 Update 14之后的 HotSpot 虚拟机,这个虚拟机包含的所有收集器如下图所示 上图 ...
- 【转】Java学习---垃圾回收算法与 JVM 垃圾回收器综述
[原文]https://www.toutiao.com/i6593931841462338062/ 垃圾回收算法与 JVM 垃圾回收器综述 我们常说的垃圾回收算法可以分为两部分:对象的查找算法与真正的 ...
- 垃圾回收算法与 JVM 垃圾回收器综述(转)
垃圾回收算法与 JVM 垃圾回收器综述 我们常说的垃圾回收算法可以分为两部分:对象的查找算法与真正的回收方法.不同回收器的实现细节各有不同,但总的来说基本所有的回收器都会关注如下两个方面:找出所有的存 ...
- 深入理解JVM虚拟机3:垃圾回收器详解
JVM GC基本原理与GC算法 Java的内存分配与回收全部由JVM垃圾回收进程自动完成.与C语言不同,Java开发者不需要自己编写代码实现垃圾回收.这是Java深受大家欢迎的众多特性之一,能够帮助程 ...
- JVM性能调优(2) —— 垃圾回收器和回收策略
一.垃圾回收机制 1.为什么需要垃圾回收 Java 程序在虚拟机中运行,是会占用内存资源的,比如创建的对象.加载的类型数据等,而且内存资源都是有限的.当创建的对象不再被引用时,就需要被回收掉,释放内存 ...
- JVM 垃圾回收器工作原理及使用实例介绍(转载自IBM),直接复制粘贴,需要原文戳链接
原文 https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/ 再插一个关于线程和进程上下文,待判断 http://b ...
- 【JVM】垃圾回收器总结(2)——七种垃圾回收器类型
七种垃圾回收器类型 GC的约定参数 DefNew——Default New Generation Tenured——Serial Old ParNew——Parallel New Generation ...
随机推荐
- jmeter通过BeanShell,实现对接口参数HmacSHA256加密(转)
jmeter通过BeanShell,实现对接口参数HmacSHA256加密2019-04-29 05:10 ps. 最近抓包网站的登陆请求,发现就2个参数,用户名和密码,通过工具去请求这个接口,一直返 ...
- 微信小程序:防止多次点击跳转(函数节流)
场景 在使用小程序的时候会出现这样一种情况:当网络条件差或卡顿的情况下,使用者会认为点击无效而进行多次点击,最后出现多次跳转页面的情况,就像下图(快速点击了两次): 解决办法 然后从 轻松理解JS函数 ...
- Python实现Newton和lagrange插值
一.介绍Newton和lagrange插值:给出一组数据进行Newton和lagrange插值,同时将结果用plot呈现出来1.首先是Lagrange插值:根据插值的方法,先对每次的结果求积,在对结果 ...
- python中用分别用selenium、requests库实现Windows认证登录
最近在搞单位的项目,实现python自动化,结果在第一步就把我给拒之门外,查资料问大佬,问我们开发人员,从周一折腾到周五才搞定了 接下给大家分享一下 项目背景:我们系统是基于Windows平台实现的, ...
- 配置kubectl在Mac(本地)远程连接Kubernetes集群
集群部署在云服务器的ECS上,但是有时需要本地原创连接集群,这就需要通过ApiServer的外网地址去访问集群,但是-/.kube/config下的地址又都是内网,所以可以使用如下方式解决: Mac安 ...
- 详细的Hadoop的入门教程-伪分布模式Pseudo-Distributed Operation
一. 伪分布模式Pseudo-Distributed Operation 这里关于VM虚拟机的安装就不再介绍了,详细请看<VMware虚拟机的三种网络管理模式>一章介绍.这章只介绍hado ...
- react,react-router,redux+react-redux 构建一个React Demo
创建初始化应用 加速我们的npm. npm install -g cnpm --registry=https://registry.npm.taobao.org 利用create-react-app ...
- hibernate Criteria中多个or和and的用法 and ( or or)
/s筛选去除无效数据 /* detachedCriteria.add( Restrictions.or( Restrictions.like("chanpin", &qu ...
- 编写可维护的JavaScript-随笔(一)
一.基本的格式化 1. 缩进层级 a) 制表符缩进 i. 好处:制表符和缩进层级是一对一的关系是符合逻辑的,文本编辑器可以配置制表符的展示长度,可以随意调节 ii. ...
- Vue学习之Webpack小结(十二)
一.nrm: nrm是专门用来管理和快速切换私人配置的registry; nrm提供了一些最常用的npm包镜像地址,能够让我们快速的切换安装包时候的服务器地址: 二.镜像: 原来 包 刚一开 ...