ygc整理
young generation garbage collection 整理
-
DefNew, ParNew, PSYoungGen, etc.. 这些cryptic名字来由? RednaxelaFX解惑
- DefNew->default new generation
- ParNew->parallel new generation
- 原本HotSpot VM里没有并行GC,当时就只有NewGeneration,后来准备要加入young gen的并行GC,就把原本的NewGeneration改名为DefNewGeneration,然后把新加的并行版叫做ParNewGeneration
- 这些XXXGeneration都在HotSpot VM的“分代式GC框架”内。
本来HotSpot VM鼓励开发者尽量在这个框架内开发GC,但后来有个开发就是不愿意被这框架憋着
自己硬写了个没有使用已有框架的新并行GC,并拉拢性能测试团队用这个并行GC来跑分,成绩也还不错,
于是这个GC就放进HotSpot VM里了。这就是我们现在看到的 ParallelScavenge - PSYoungGen->Parallel scavenge young generation
- 结果就是HotSpot GC组不得不维护两个功能几乎一样、但各种具体细节不同的并行GC。其实是件很头疼的事情
- Scavenge或者叫scavenging GC,其实就是copying GC的另一种叫法而已
HotSpot VM里的GC都是在minor GC收集器里用scavenging的
DefNew、ParNew和ParallelScavenge都是,只不过DefNew是串行的copying GC,而后两者是并行的copying GC
由此名字就可以知道,“ParallelScavenge”的初衷就是把“scavenge”给并行化。换句话说就是把minor GC并行化
至于full GC,那不是当初关注的重点。
把GC并行化的目的是想提高GC速度,也就是提高吞吐量(throughput)
所以其实ParNew与ParallelScavenge都可叫做Throughput GC
但是在HotSpot VM的术语里“Throughput GC”通常特指“ParallelScavenge” - ParallelScavenge和ParNew都是并行GC,主要是并行收集young gen,目的和性能其实都差不多。最明显的区别有下面几点
- PS以前是广度优先顺序来遍历对象图的,JDK6的时候改为默认用深度优先顺序遍历,并留有一个UseDepthFirstScavengeOrder参数来选择是用深度还是广度优先
在JDK6u18之后这个参数被去掉,PS变为只用深度优先遍历。ParNew则是一直都只用广度优先顺序来遍历 - PS完整实现了adaptive size policy,而ParNew及“分代式GC框架”内的其它GC都没有实现完(倒不是不能实现,就是麻烦+没人力资源去做)
所以千万千万别在用ParNew+CMS的组合下用UseAdaptiveSizePolicy,请只在使用UseParallelGC或UseParallelOldGC的时候用它 - 由于在“分代式GC框架”内,ParNew可以跟CMS搭配使用,而ParallelScavenge不能。当时ParNew GC被从Exact VM移植到HotSpot VM的最大原因就是为了跟CMS搭配使用
- 在PS成为主要的throughput GC之后,它还实现了针对NUMA的优化;而ParNew一直没有得到NUMA优化的实现
- PS以前是广度优先顺序来遍历对象图的,JDK6的时候改为默认用深度优先顺序遍历,并留有一个UseDepthFirstScavengeOrder参数来选择是用深度还是广度优先
- ParallelScavenge并行收集young gen,那old/perm gen呢?
- 其实最初的ParallelScavenge的目标只是并行收集young gen,而full GC的实际实现还是跟serial GC一样
只不过因为它没有用HotSpot VM的generational GC framework,自己实现了一个CollectedHeap的子类ParallelScavengeHeap
里面都弄了独立的一套接口,而跟HotSpot当时其它几个GC不兼容。其实真的有用的代码大部分就在PSScavenge(=“ParallelScavenge的Scavenge”)里,也就是负责minor GC的收集器
而负责full GC的收集器叫做PSMarkSweep(=“ParallelScavenge的MarkSweep”)
其实只是在serial GC的核心外面套了层皮而已,骨子里是一样的LISP2算法的mark-compact收集器(别被名字骗了,它并不是一个mark-sweep收集器) - 当启用-XX:+UseParallelGC时,用的就是PSScavenge+PSMarkSweep的组合。 这是名副其实的“ParallelScavenge”——只并行化了“scavenge”
- 不知道后来什么原因导致full GC的并行化并没有在原本的generational GC framework上进行,而只在ParallelScavenge系上进行了。其成果就是使用了LISP2算法的并行版的full GC收集器,名为PSCompact(=“ParallelScavenge-MarkCompact”),收集整个GC堆
- 当启用-XX:+UseParallelOldGC时,用的就是PSScavenge+PSCompact的组合,此时ParallelScavenge其实已经名不符实了——它不只并行化了“scavenge”(minor GC),也并行化了“mark-compact”(full GC)
- 其实最初的ParallelScavenge的目标只是并行收集young gen,而full GC的实际实现还是跟serial GC一样
-
HotSpot VM的GC组老人之一Jon Masamitsu Our Collectors
- 各种gc实现的组合关系 图在这里
- 忽略G1,shenandoah(not product-ready)
- young
- Serial -> CMS,Serial Old(MSC)
- ParNew -> CMS,Serial Old(MSC)
- Parallel Scavenge -> Serial Old(MSC),Parallel Old
- old
- CMS -> Serial,ParNew,Serial Old(MSC)
- Serial Old(MSC)->CMS, Serial, ParNew, Parallel Scavenge
- Parallel Old -> Parallel Scavenge
- 简单介绍各个gc的特点
- "Serial" is a stop-the-world, copying collector which uses a single GC thread.
- "ParNew" is a stop-the-world, copying collector which uses multiple GC threads. It differs
from "Parallel Scavenge" in that it has enhancements that make it usable with CMS.
For example, "ParNew" does the synchronization needed so that it can run during the concurrent phases of CMS. - "Parallel Scavenge" is a stop-the-world, copying collector which uses multiple GC threads.
- "Serial Old" is a stop-the-world,mark-sweep-compact collector that uses a single GC thread.
- "CMS" is a mostly concurrent, low-pause collector.
- "Parallel Old" is a compacting collector that uses multiple GC threads. Using the -XX flags for our collectors for jdk6,
- USAGE
- UseSerialGC is "Serial" + "Serial Old"
- UseParNewGC is "ParNew" + "Serial Old"
- UseConcMarkSweepGC is "ParNew" + "CMS" + "Serial Old". "CMS" is used most of the time to collect the tenured generation. "Serial Old" is used when a concurrent mode failure occurs.
- UseParallelGC is "Parallel Scavenge" + "Serial Old"
- UseParallelOldGC is "Parallel Scavenge" + "Parallel Old"
- FAQ
- How do I use "CMS" with "Serial"?
- -XX:+UseConcMarkSweepGC -XX:-UseParNewGC.
Don't use -XX:+UseConcMarkSweepGC and -XX:+UseSerialGC.
Although that's seems like a logical combination, it will result in a message saying something about
conflicting collector combinations and the JVM won't start. Sorry about that.
Our bad
- -XX:+UseConcMarkSweepGC -XX:-UseParNewGC.
- G1的暂时没看
- If G1 works out as we expect, it will become our low-pause collector in place of
"ParNew" + "CMS". And if you're about to ask when will it be ready, please don't
be offended by my dead silence. It's the highest priority project for our team,
but it is software development so there are the usual unknowns. It will be out
by JDK7. The sooner the better as far as we're concerned.
- How do I use "CMS" with "Serial"?
- 各种gc实现的组合关系 图在这里
-
实现算法
-
Minor GC只收集young generation,而使用Serial GC时这个young generation的实现类叫做DefNewGeneration
-
FastScanClosure只在DefNewGeneration的收集中有用到。
- HotSpot VM里有很多以*-Closure方式命名的类。它们其实是封装起来的回调函数。为了让GC的具体逻辑与对象内部遍历字段的逻辑能松耦合,这部分都是通过回调函数来连接到一起的。
- ScanClosure与FastScanClosure都可用于DefNewGeneration的扫描
-
HotSpot VM Serial GC的minor GC使用的是Cheney算法的变种,所以先理解基本的Cheney算法有助理清头绪
-
Cheney algorithms 这个算法的原始论文是C. J. Cheney在1970年发表的:A nonrecursive list compacting algorithm 维基百科解释
- is a stop and copy method of tracing garbage collection in computer software systems. In this scheme, the heap is divided into two equal halves, only one of which is in use at any one time. Garbage collection is performed by copying live objects from one semispace (the from-space) to the other (the to-space), which then becomes the new heap. The entire old heap is then discarded in one piece. It is an improvement on the previous stop and copy technique
- Cheney's algorithm reclaims items as follows
- Object references on the stack
- Object references on the stack are checked. One of the two following actions is taken for each object reference that points to an object in from-space
- If the object has not yet been moved to the to-space, this is done by creating an identical copy in the to-space, and then replacing the from-space version with a forwarding pointer to the to-space copy. Then update the object reference to refer to the new version in to-space
- If the object has already been moved to the to-space, simply update the reference from the forwarding pointer in from-space
- Objects in the to-space
- The garbage collector examines all object references in the objects that have been migrated to the to-space,
- and performs one of the above two actions on the referenced objects
- Object references on the stack
- Once all to-space references have been examined and updated, garbage collection is complete
- The algorithm needs no stack and only two pointers outside of the from-space and to-space:
a pointer to the beginning of free space in the to-space,
and a pointer to the next word in to-space that needs to be examined. - For this reason, it's sometimes called a "two-finger" collector --- it only needs "two fingers" pointing into the to-space to keep track of its state.
- The data between the two fingers represents work remaining for it to do.
- The forwarding pointer (sometimes called a "broken heart") is used only during the garbage collection process;
when a reference to an object already in to-space (thus having a forwarding pointer in from-space) is found,
the reference can be updated quickly simply by updating its pointer to match the forwarding pointer. - Because the strategy is to exhaust all live references, and then all references in referenced objects, this is known as a breadth-first list copying garbage collection scheme
- Equivalence to tri-color abstraction
- Cheney's algorithm is an example of a tri-color marking garbage collector. The first member of the gray set is the stack itself. Objects referenced on the stack are copied into the to-space, which contains members of the black and gray sets.
- The algorithm moves any white objects (equivalent to objects in the from-space without forwarding pointers) to the gray set by copying them to the to-space. Objects that are between the scanning pointer and the free-space pointer on the to-space area are members of the gray set still to be scanned. Objects below the scanning pointer belong to the black set. Objects are moved to the black set by simply moving the scanning pointer over them.
- When the scanning pointer reaches the free-space pointer, the gray set is empty, and the algorithm ends.
-
每个版本的算法描述都稍微不同,我的伪代码也跟这两本书写的方式稍微不同,但背后要表达的核心思想是一样的就OK了。
-
Tracing GC的核心操作之一就是从给定的根集合出发去遍历对象图。对象图是一种有向图,该图的节点是对象,边是引用。遍历它有两种典型顺序:深度优先(DFS)和广度优先(BFS)
-
广度优先遍历的典型实现思路是三色遍历:给对象赋予白、灰、黑三种颜色以标记其遍历状态:
- 白色:未遍历到的对象
- 灰色:已遍历到但还未处理完的对象(意味着该对象尚有未遍历到的出边)
- 黑色:已遍历完的对象
-
遍历过程:
- 一开始,所有对象都是白色的;
- 把根集合能直接碰到的对象标记为灰色。在只有一个根对象的地方就只要把那个对象标记为灰色。但GC通常不是只有一个特定根对象,而是有一个集合的引用作为根,这些引用能直接碰到的对象都要标记为灰色。
- 然后逐个扫描灰色对象的出边,把这些边能直接碰到的对象标记为灰色。每当一个对象的所有出边都扫描完了,就把这个对象标记为黑色。
- 重复第3步直到不再有灰色对象,遍历结束。
-
这黑白灰要怎么跟实际实现联系起来呢?基本算法会使用一个队列(queue)与一个集合set
void breadth_first_search(Graph* graph) { // 记录灰色对象的队列 Queue<Node*> scanning; // 1. 一开始对象都是白色的 // 2. 把根集合的引用能碰到的对象标记为灰色 // 由于根集合的引用有可能有重复,所以这里也必须 // 在把对象加入队列前先检查它是否已经被扫描到了 for (Node* node : graph->root_edges()) { // 如果出边指向的对象还没有被扫描过 if (node != nullptr && !node->is_marked()) { node->set_marked(); // 记录下它已经被扫描到了 scanning.enqueue(child); // 也把该对象放进灰色队列里等待扫描 } } // 3. 逐个扫描灰色对象的出边直到没有灰色对象 while (!scanning.is_empty()) { Node* parent = scanning.dequeue(); for (Node* child : parent->child_nodes() { // 扫描灰色对象的出边 // 如果出边指向的对象还没有被扫描过 if (child != nullptr && !child->is_marked()) { child->set_marked(); // 把它记录到黑色集合里 scanning.enqueue(child); // 也把该对象放进灰色队列里等待扫描 } } } }
-
Cheney算法正如上面说的一样,用一个队列来实现对象图的遍历(每个节点可以标记状态) 比较完整的伪代码可以参考我发这节开头给的链接,其中核心的部分抽取出来如下:
`void garbage_collect(Heap* heap) {
Semispace* to_space = heap->to_space();
// 记录灰色对象的队列:从scanned到to_space->top()
address scanned = to_space->bottom();
// 1. 一开始对象都是白色的
// 2. 把根集合的引用能碰到的对象标记为灰色
// 由于根集合的引用有可能有重复,所以这里也必须
// 在把对象加入队列前先检查它是否已经被扫描到了
for (Object** refLoc : heap->root_reference_locations()) {
Object* obj = *refLoc;
if (obj != nullptr) {
if (!obj->is_forwarded()) {
// 记录下它已经被扫描到了,也把该对象放进灰色队列里等待扫描
size_t size = obj->size();
address new_addr = to_space->allocate(size);// address Semispace::allocate(size_t size) { // if (_top + size < _end) { // address new_addr = _top; // _top += size; // return new_addr; // } else { // return nullptr; // } // } // to_space->allocate()移动了to_space->top()指针, // 等同于scanning.enqueue(obj); copy(/* to */ new_addr, /* from */ obj, size); Object* new_obj = (Object*) new_addr; obj->forward_to(new_obj); // 设置转发指针(forwarding pointer) *refLoc = new_obj; // 修正指针指向新对象 } else { *refLoc = obj->forwardee(); // 修正指针指向新对象 } } } // 3. 逐个扫描灰色对象的出边直到没有灰色对象 while (scanned < to_space->top()) { Object* parent = (Object*) scanned; // 扫描灰色对象的出边 for (Object** fieldLoc : parent->object_fields()) { Object* obj = *fieldLoc; // 如果出边指向的对象还没有被扫描过 if (obj != nullptr) { if (!obj->is_forwarded()) { // 尚未被扫描过的对象 // 记录下它已经被扫描到了,也把该对象放进灰色队列里等待扫描 size_t size = obj->size(); address new_addr = to_space->allocate(size); // to_space->allocate()移动了to_space->top()指针, // 等同于scanning.enqueue(obj); copy(/* to */ new_addr, /* from */ obj, size); Object* new_obj = (Object*) new_addr; obj->forward_to(new_obj); // 设置转发指针(forwarding pointer) *fieldLoc = new_obj; // 修正指针指向新对象 } else { // 已经扫描过的对象 *fieldLoc = obj->forwardee(); // 修正指针指向新对象 } } } scanned += parent->size(); // 移动scanned指针等同于scanning.dequeue(parent); }
}`
-
它的设计非常精妙
- 它使用一块连续的地址空间来实现GC堆,并将其划分为2个半分空间(semispace),分别称为from-space与to-space。平时只用其中一个,也就是from-space;
- 逐个扫描指针,每扫描到一个对象的时候就把它从from-space拷贝到to-space,并在原来的对象里记录下一个转发指针(forwarding pointer),记住该对象被拷贝到哪里了。要知道一个对象有没有被扫描(标记)过,只要看该对象是否有转发指针即可;
- 每扫描完一个指针就顺便把该指针修正为指向拷贝后的新对象。这样,对象的标记(mark)、整理(compaction)、指针的修正就合起来在一步都做好了;
- 它不需要显式为扫描队列分配空间,而是复用了to-space的一部分用作隐式队列。用一个scanned指针来区分to-space的对象的颜色:
在to-space开头到scanned指针之间的对象是黑色的,
在scanned指针到to-space已分配对象的区域的末尾之间的对象是灰色的。
如何知道还有没有灰色对象呢?
只要scanned追上了to-space已分配对象区域的末尾就好了。
这种做法也叫做“两手指”(two-finger):
“scanned”与“free”。只需要这两个指针就能维护隐式扫描队列。
“free”在我的伪代码里就是to_space->top()。
-
Cheney算法GC工作时,to-space中各指针的样子如下:
|[ 已分配并且已扫描完的对象 ]|[ 已分配但未扫描完的对象 ]|[ 未分配空间 ]|
^ ^ ^ ^
bottom scanned top end
在GC结束时,不需要对原本的from-space做什么清理动作,只要把它的分配指针(top)设回到初始位置(bottom)即可。
之前在里面的对象就当作不存在了。
自然,也就不需要清理其中设置了的转发指针。 -
Cheney算法是一个非常非常简单且高效的GC算法。看前面我写的伪代码就可以有直观的感受它有多简单。它的实现代码恐怕比简易mark-sweep还简单。
但为啥很多简易的VM宁可采用mark-sweep而不用Cheney算法的copying GC呢?
因为mark-sweep GC的常规实现不移动对象,而copying GC必须移动对象。
移动对象意味着使用GC的程序(术语叫做mutator)需要做更多事情,例如说要能准确定位到所有的指针,以便在对象移动之后修正指针。
很多简易VM都偷懒不想记住所有指针的位置,所以无法支持copying GC。 -
Cheney算法的简单优雅之处来自它通过隐式队列来实现广度优先遍历,但它的缺点之一却也在此:
广度优先的拷贝顺序使得GC后对象的空间局部性(memory locality)变差了。
但是如果要改为真的深度优先顺序就会需要一个栈,无论是隐式(通常意味着递归调用)或者是显式。
使用递归实现的隐患是容易爆栈,有没有啥办法模拟深度优先的拷贝顺序但不用栈呢?这方面有很多研究。其中一种有趣的做法是IBM的hierarchical copying GC -
相比基本的Cheney算法,HotSpot VM Serial GC有什么异同呢?
- 相同点:
- 使用广度优先遍历;
- 使用隐式队列;
- copy等同mark + relocate (compact) + remap (pointer fixup)三件事一步完成。
- 在HotSpot VM里,copying GC用了scavenge这个名字,说的是完全相同的事
- 相异点:
- 基本Cheney算法不分代,而HotSpot的GC分两代
- 基本Cheney算法使用2个半分空间(semispace),而HotSpot的GC在young generation使用3个空间——1个eden与两个survivor space。注意这两个survivor space就与semispace的作用类似。
- 在G1 GC之前,所有HotSpot VM的GC堆布局都继承自1984年David Ungar在Berkeley Smalltalk里所实现的Generation Scavenging
- 相同点:
-
那我们一点点把基本的Cheney算法映射过来
- 基本Cheney算法用from-space和to-space,而HotSpot VM的DefNewGeneration有三个空间,eden space、from-space、to-space。后者的eden space + from-space大致等于前者的from-space,而后者的to-space + old gen的一部分大致等于前者的to-space
- 拷贝对象的目标空间不一定是to-space,也有可能是old generation,也就是对象有可能会从young generation晋升到old generation。
为了实现这一功能,对象头的mark word里有一小块地方记录对象的年龄(age),也就是该对象经历了多少次minor GC。如果扫描到一个对象,并且其年龄大于某个阈值(tenuring threshold),
则该对象会被拷贝到old generation;如果年龄不大于那个阈值则拷贝到to-space。
要留意的是,基本Cheney算法中2个半分空间通常一样大,所以可以保证所有from-space里活着的对象都能在to-space里找到位置。但HotSpot VM的from-space与to-space通常比eden space小得多,不一定能容纳下所有活的对象。
如果一次minor GC的过程中,to-space已经装满之后还遇到活对象要拷贝,则剩下的对象都得晋升到old generation去。这种现象叫做过早晋升(premature tenuring),要尽量避免 - 既然拷贝去的目标空间不一定是to-space,那原本Cheney算法里的隐式扫描队列会在哪里?
答案是既在to-space,也在old generation。很简单,在这两个空间都记录它们的scanned指针(叫做“saved mark”),这俩空间各自原本也记录着它们的分配指针(“top”),之间的部分就用作扫描队列 - Forwarding pointer安装在对象(oopDesc)头部的mark word(markOop)里。只有在minor GC的时候才会把已拷贝的对象的mark word借用来放转发指针
- 通过回调,把遍历逻辑与实际动作分离。
例如说,遍历根集合的逻辑封装在GenCollectedHeap::gen_process_strong_roots()、SharedHeap::process_strong_roots()里,
遍历对象里的引用类型字段的逻辑封装在oopDesc::oop_iterate()系的函数里;
而实际拷贝对象的动作则由FastScanClosure::do_work()负责调用 - 基本Cheney算法的“scanned”指针,在HotSpot Serial GC里是每个space的“saved mark”。相关操作的函数名是:“save_marks()” / “set_saved_mark()”、“reset_saved_mark()”、“no_allocs_since_save_marks()” / “saved_mark_at_top()”
- 看看遍历循环的结束条件(循环条件的反条件)bool saved_mark_at_top() const { return saved_mark_word() == top(); } 跟基本Cheney算法的循环条件 scanned != top() 一样
- 为啥我的伪代码里scanned是个局部变量,而HotSpot里变成了每个空间的成员字段?因为使用回调函数来分离遍历逻辑与实际动作,代码结构变了,这个scanned指针也只好另找地方放来让需要访问它的地方都能访问到
- HotSpot VM的分代式GC需要通过写屏障(write barrier)来维护一个记忆集合(remember set)——记录从old generation到young generation的跨代引用的数据结构。具体在代码中叫做CardTable。
在minor GC时,old generation被remember set所记录下的区域会 被看作根集合的一部分。而在minor GC过程中,每当有对象晋升到old generation都有可能产生新的跨代引用。
所以FastScanClosure::do_work()里也有调用写屏障的逻辑:OopsInGenClosure::do_barrier() - HotSpot VM要支持Java的弱引用。在GC的时候有些特殊处理要做
- HotSpot VM的GC必须处理一些特殊情况,一个极端的例子是to-space和old generation的剩余空间加起来都无法容纳eden与from-space的活对象,导致GC无法完成。这使得许多地方代码看起来很复杂。但要了解主要工作流程的话可以先不关心这些旁支逻辑
-
一次YGC过程主要分成两个步骤:
1、查找GC Roots,拷贝所引用的对象到 to 区;
2、递归遍历步骤1中对象,并拷贝其所引用的对象到 to 区,当然可能会存在自然晋升,或者因为 to 区空间不足引起的提前晋升的情况; -
SharedHeap::process_strong_roots()扫描了所有一定是GC Roots的内存区域
Universe类中所引用的一些必须存活的对象 Universe::oops_do(roots)
所有JNI Handles JNIHandles::oops_do(roots)
所有线程的栈 Threads::oops_do(roots, code_roots)
所有被Synchronize锁持有的对象 ObjectSynchronizer::oops_do(roots)
VM内实现的MBean所持有的对象 Management::oops_do(roots)
JVMTI所持有的对象 JvmtiExport::oops_do(roots)
(可选)所有已加载的类 或 所有已加载的系统类 SystemDictionary::oops_do(roots)
(可选)所有驻留字符串(StringTable) StringTable::oops_do(roots)
(可选)代码缓存(CodeCache) CodeCache::scavenge_root_nmethods_do(code_roots)
(可选)PermGen的remember set所记录的存在跨代引用的区域 rem_set()->younger_refs_iterate(perm_gen(), perm_blk) -
如果一个old generation的对象引用了young generation,那么这个old generation的对象肯定也属于Strong root的一部分,
这部分逻辑并没有在process_strong_roots中实现,而是在绿色框中实现了,其中rem_set中保存了old generation中dirty card的对应区域,
每次对象的拷贝移动都会检查一下是否产生了新的跨代引用,比如有对象晋升到了old generation,而该对象还引用了young generation的对象,
这种情况下会把相应的card置为dirty,下次YGC的时候只会扫描dirty card所指内存的对象,避免扫描所有的old generation对象 -
遍历活跃对象
- 在查找GC Roots的步骤中,已经找出了第一批存活的对象,这些存活对象可能在 to-space,也有可能直接晋升到了 old generation,这些区域都是需要进行遍历的,保证所有的活跃对象都能存活下来
- 每个内存区域都有两个指针变量,分别是 _saved_mark_word 和 _top,其中_saved_mark_word 指向当前遍历对象的位置,_top指向当前内存区域可分配的位置,其中_saved_mark_word 到 _top之间的对象是已拷贝,但未扫描的对象
- GC Roots引用的对象拷贝完成后,to-space的_saved_mark_word和_top的状态如上图所示,假设期间没有对象晋升到old generation。
每次扫描一个对象,_saved_mark_word会往前移动,期间也有新的对象会被拷贝到to-space,_top也会往前移动,直到 _saved_mark_word追上_top,说明to-space的对象都已经遍历完成 - 其中while循环条件 while (!_gch->no_allocs_since_save_marks(_level),就是在判断各个内存代中的_saved_mark_word是否已经追到_top,如果还没有追上,就执行_gch->oop_since_save_marks_iterate进行遍历
- 从代码实现可以看出对新生代、老年代和永久代都会进行遍历,其中新生代的遍历实现
- 这里会对eden、from和to分别进行遍历,第一次看这块逻辑的时候很纳闷,为什么要对eden和from-space进行遍历,from倒没什么问题,_saved_mark_word和_top一般都是相同的,
但是eden区的_saved_mark_word明显不会等于_top,一直没有找到在eden区分配对象时,改变_top的同时也改变_saved_mark_word的逻辑,
后来发现GenCollectedHeap::do_collection方法中,在调用各个代的collect之前,会调用save_marks()方法,将_saved_mark_word设置为_top,这样在发生YGC时,eden区的对象其实是不会被遍历的,被这个疑惑困扰了好久,结果是个遗留代码 - to-space对象的遍历实现
- 在FastScanClosure回调函数的do_oop_work方法实现中,红框的是重要的部分,因为可能存在多个对象共同引用一个对象,所以在遍历过程中,可能会遇到已经处理过的对象,如果遇到这样的对象,就不会再次进行复制了,
如果该对象没有被拷贝过,则调用 copy_to_survivor_space 方法拷贝对象到to-space或者晋升到old generation,
这里提一下ParNew的实现,因为是并发执行的,所以可能存在多个线程拷贝了同一个对象到to-space,不过通过原子操作,保证了只有一个对象是有效的 - 拷贝对象的目标空间不一定是to-space,也有可能是old generation,如果一个对象经历了很多次YGC,会从young generation直接晋升到old generation,
为了记录对象经历的YGC次数,在对象头的mark word 数据结构中有一个位置记录着对象的YGC次数,也叫对象的年龄,如果扫描到的对象,其年龄小于某个阈值(tenuring threshold),
该对象会被拷贝到to-space,并增加该对象的年龄,同时to-space的_top指针也会往后移动,这个新对象等待着被扫描 - 如果该对象的年龄大于某个阈值,会晋升到old generation,或者在拷贝到to-space时空间不足,也会提前晋升到old generation,晋升过程通过老年代_next_gen的promote方法实现,
如果old generation也没有足够的空间容纳该对象,则会触发晋升失败
-
-
card table
- 在进行YGC时,如果young generation的Y对象被old generation中O对象引用,那么称O对象存在跨代引用,而且Y对象应该在本次垃圾回收中存活下来,所以old generation的对象在YGC时也是Strong root的一部分,如果每次YGC都去扫描old generation中所有对象的话,肯定会非常耗时,那么有什么好的解决方案呢
- 如果只扫描那些有young generation对象引用的对象,是不是效率可以达到最高,不过使用这种方式,需要有一个地方保存这些对象的引用,是一个不小的内存开销,所以Hotspot实现中,并没采用这样方式,而是使用一个GenRemSet数据结构,记录包含这些对象的内存区域是clean or dirty状态
- 盗图一张-remset
- CardTable是GenRemSet的一种实现,类似于一个数组,每个元素对应着堆内存的一块区域是否存在跨代引用的对象,如果存在,该Card为dirty状态
- GenRemSet随着堆内存一起初始化,通过具体的垃圾收集策略进行创建,比如CMS和G1是不一样的,其中CMS对应的是CardTable
- 接上文中YGC遍历old generation的逻辑
rem_set()->younger_refs_iterate(_gens[i], older_gens);
这里rem_set()方法返回的就是已经初始化的CardTableRS对象,调用younger_refs_iterate,传入的参数分别是old generation的引用和负责遍历old generation对象的回调函数FastScanClosure,一步一步调用下去,最终调用到ClearNoncleanCardWrapper::do_MemRegion方法 - 其中参数MemRegion相当于堆内存的一块区域,这里指向old generation从_bottom 到 _top的区间
- 具体看http://www.jianshu.com/p/5037459097ee
- 每次的动作是先清除Card的dirty状态,对象拷贝完成再判断是否要设置为dirty,即非clean
- unread