深入解析java虚拟机:垃圾回收,垃圾回收基础概述

垃圾回收基础概述

垃圾回收机制最早诞生于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_pagechar* 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;觉得文章不错的朋友可以转发此文关注小编;感谢大家的支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章