垃圾回收基础概述
垃圾回收机制最早诞生于Lisp编程语言,但Lisp的作者McCathy在第一次现场演示Lisp时却因中途耗尽全部32KB内存以及一些其他原因只能草草收场。60年后的今天,垃圾回收技术再也不是一个笑话,它俨然成为诸如Java、C#、Python、Erlang、Golang编程语言的核心组件。
Java最吸引人的特性之一就是它的垃圾回收技术:程序员负责创建对象、使用对象,垃圾回收器负责回收资源,做好善后工作。它从GCRoot出发标记存活对象,清理未被标记的对象,这种方式又被称为追踪式回收。Java的所有垃圾回收器都使用追踪式回收,只是具体的算法细节不尽相同。本章将讨论HotSpot VM中现存的所有垃圾回收器,在这之前,有必要先了解下垃圾回收的一些基础知识。
GC Root
GC Root又叫根集,它是垃圾回收器扫描存活对象的起始地点。举个简单的例子,如代码清单10-1所示:
代码清单10-1 GC Root示例
public static void test(){
Struct free = new Struct(“a”);
Struct obj = new Struct(“b”);
obj.field1 = new Object();
obj.field2 = 12;System.gc(); // (1)
free = null;
System.gc(); // (2)
}
假设(1)处成功触发垃圾回收,那么垃圾回收器将不能回收任何对象,因为线程栈上包括free和obj引用,它们分别指向对象a和对象b。
在(2)处调用成功后,垃圾回收器可以回收对象a但不能回收对象b,因为栈上存在指向对象b的引用obj,而指向对象a的引用free被赋予null值,即再没有指向对象a的引用,因此对象a被视作垃圾,可回收处理。
代码清单10-1中的free和obj所在的线程栈即GC Root之一,垃圾回收器以它们为起点找到存活对象:凡是从线程栈出发没有触及的对象就可以被认为是死亡对象,继而可以被回收。除了线程栈外,HotSpot VM还有一些地方也可以作为GC Root:
1)所有已加载的类的对象引用(
ClassLoaderDataGraph::roots_cld_do);
2)所有线程栈上的对象引用(
Threads::possibly_parallel_oops_do);
3)虚拟机内部使用的Java对象引用(Universe::oops_do,SystemDictionary::oops_do);
4)JNI Handle(JNIHandles::oops_do);
5)被synchronized锁住的对象引用(
ObjectSynchronizer::oops_do);
6)Java工具用到的对象引用(Management::oops);
7)JVMTI导出对象引用(JvmtiExport::oops_do);
8)AOT堆对象引用(AOTLoader::oops);
9)CodeCache代码引用(CodeCache::blobs_do);
10)String常量池对象引用(StringTable::oops_do)。
安全点
在垃圾回收器的眼中只有垃圾回收线程和修改对象的线程,后者被称为Mutator线程。由于垃圾回收线程也需要修改对象,尤其是在垃圾回收过程中可能有移动对象的情况,如果Mutator线程在移动对象的同时修改对象,势必会造成错误,因此在垃圾回收时一般需要全过程,或者部分过程暂停Mutator线程,这种暂停Mutator线程的现象又叫作世界停顿(Stop The World,STW)。一般来说,Mutator线程可以主动或者被动达到STW,在HotSpot VM中,使用安全点(Safepoint)作为主动STW机制。安全点本质上是一页内存,如代码清单10-2所示:
代码清单10-2 安全点创建
void SafepointMechanism::default_initialize() {
if (ThreadLocalHandshakes) {
...// 分配两页内存,一页用于bad_page,一页用于good_page
char* polling_page = os::reserve_memory(...);
char* bad_page = polling_page;
char* good_page = polling_page + page_size;
// bad_page表示这片内存不可读不可写,good_page表示可读
os::protect_memory(bad_page, page_size, os::MEM_PROT_NONE);
os::protect_memory(good_page, page_size, os::MEM_PROT_READ);
os::set_polling_page((address)(bad_page));
...
} else {
// 分配一页内存
char* polling_page = os::reserve_memory(...);
os::commit_memory_or_exit(...);
// 将它设置为可读
os::protect_memory(polling_page, page_size, os::MEM_PROT_READ);
os::set_polling_page((address)(polling_page));
}
}
虚拟机将“读取安全点内存页”的操作安插在一些合适的地方。当程序没有请求垃圾回收时(实际上除了垃圾回收外,还有其他操作可能会请求安全点),安全点内存页可读,Mutator线程对安全点的访问不会引发任何问题。当需要垃圾回收时,VMThread将安全点设置为不可读不可写,然后等待所有Mutator线程走到安全点。由于Mutator线程访问不可读不可写的内存时会引发异常信号,虚拟机可通过内部的信号处理器捕获并停止Mutator线程的执行,这样一来相当于让所有Mutator线程主动停止。
在具体实现中,
SafepointSynchronize::begin()和SafepointSynchronize::end()分别表示安全点的开启和关闭,两者之间构成一个安全区域,它们只能被VMThread调用。代码清单10-3展示了安全点开启的代码实现:
代码清单10-3
SafepointSynchronize::begin
void SafepointSynchronize::begin() {
...
// 设置状态为安全点开启中
_state = _synchronizing;
// 如果使用全局安全点,修改安全点内存页,将其设置为不可读不可写
// (对应还有如果使用线程握手的处理,这里已省略)
if (SafepointMechanism::uses_global_page_poll()) {
Interpreter::notice_safepoints();
PageArmed = 1 ;
os::make_polling_page_unreadable();
}
...
while(still_running > 0) {
jtiwh.rewind();
// 对于当前所有运行的线程
for (; JavaThread *cur = jtiwh.next(); ) {
// 获取线程状态
ThreadSafepointState *cur_state = cur->safepoint_state();// 如果还在运行
if (cur_state->is_running()) {
// 检查线程是不是suspend或者其他情况,并处理它
cur_state->examine_state_of_thread();
// 再次检查线程是否还在运行
if (!cur_state->is_running()) {
// 如果没有运行,计数减一(still_running表示当前还在运行的线程)
still_running--;
}
}
}
... // 如果循环太多次,可能会使当前线程暂停
}
// VMThread等待所有线程停下来
while (_waiting_to_block > 0) {
... Safepoint_lock->wait(true, remaining_time / MICROUNITS);
}
// 安全点开启成功,设置状态,计数增加
_safepoint_counter ++;
_state = _synchronized;
OrderAccess::fence();
... // 日志记录等
}
VMThread会等待所有线程,直到都达到安全点,此时安全点开启成功。开启安全点的核心是线程状态的转换,不同线程进入安全点的方式也不尽相同。
1)解释器线程:第5章提到过,VMThread调用
TemplateInterpreter::notice_safepoints通知模板解释器将模板表切换为安全点表(这意味着执行完一条字节码后遇到一个安全点时,可以进入安全点),安全点表除了执行字节码代码外还负责安全点处理,其中就包括进入安全点。
2)执行native代码的线程:VMThread不会暂停执行native代码的线程,但是当线程从native代码返回到Java代码时,需要检查_state,如果发现是_synchronizing则线程停止。
3)执行编译后的代码的线程:开启安全点后,执行编译后的代码的线程使用test指令访问安全点,此时安全点不可读,所以引发异常信号,异常信号会被虚拟机的信号处理器(在Linux平台上是handle_linux_signal)捕获,然后阻塞线程。
4)已经阻塞的线程:对于已经阻塞的线程,继续保持阻塞状态即可,在安全点操作没有结束前不允许醒来。
5)执行虚拟机内部代码或者正在状态转换的线程:Java线程大部分时间在执行字节码,有时也会执行虚拟机自身的一些代码,这些线程会在状态转换时阻塞自身。
线程局部握手
上节节的代码清单10-2展示的代码中有一个线程局部握手(ThreadLocal Handshakes)标志,它是JEP 312引入的特性。根据上面的描述,安全点是一个全局的内存页,一旦VMThread开启安全点(将内存设置为不可读不可写)后,所有Mutator线程都会继续运行直到遇到附近的安全点读取,再通过异常处理机制主动停止。但是有时并不需要停止所有Mutator线程,如偏向锁撤销,或者打印某个线程的线程栈,在这些情况下,VMThread只需要停止某个指定的线程并打印线程栈即可。基于这些考虑,HotSpot VM引入了线程局部握手机制,使VMThread可以有选择性地针对某个线程开启或者关闭线程局部的安全点。
GC屏障
GC屏障即后缀为BarrierSet的一系列类,它们的作用是在字段读操作或者写操作前后插入一段代码,执行某些垃圾回收必要的逻辑,如代码清单10-4所示:
代码清单10-4 GC屏障
public void barrier(Struct obj){
// Write_barreir_pre();
obj.field = new Object();
// Write_barreir_post();
}
虚拟机在字段写操作前后可以分别插入前置写屏障、后置写屏障(读屏障同理),这些写屏障会执行一些GC必要的逻辑,如检测到对象引用关系的修改并记录到记忆集中。GC屏障对性能有较大影响,因为字段读写操作是程序最常见的行为,所以不应该在GC屏障中放置“重量级”代码。
本文给大家讲解的内容是深入解析java虚拟机:垃圾回收,垃圾回收基础概述
- 下篇文章给大家讲解的是深入解析java虚拟机:垃圾回收,Epsilon GC;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!