相关文章:
1.安全点的相关知识
http://blog.csdn.net/youyou1543724847/article/details/52728148
1.1 OOPMap
http://blog.csdn.net/youyou1543724847/article/details/52728154
2. GC基本算法
http://blog.csdn.net/youyou1543724847/article/details/52728210
3. G1算法
http://blog.csdn.net/youyou1543724847/article/details/52728244
4.通过Reference和GC交互
http://blog.csdn.net/youyou1543724847/article/details/52728290
5.GC友好编程
http://blog.csdn.net/youyou1543724847/article/details/52728301
6.其他
http://blog.csdn.net/youyou1543724847/article/details/52733325
主要有如下几个问题:
1.什么是安全点
2.安全点的位置
3.安全点的管理、实现
4.什么场景、功能需要安全点的配合
5.Safe Region
安全点Safe Point:safepoint 安全点顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread’s representation of it’s Java machine state is well described),比如记录OopMap的状态,从而确定GC Root的信息,使JVM可以安全的进行一些操作,比如开始GC。
安全区Safe Region:见下
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint)程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
这些特定的位置主要在:
1、循环的末尾
2、方法临返回前 / 调用方法的call指令后
3、可能抛异常的位置
(关于 OOPMap的基本知识下面会讲述)
对于Sefepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension),其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
而主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。下面代码清单3-4中的test指令是HotSpot生成的轮询指令,当需要暂停线程时,虚拟机把0x160100的内存页设置为不可读,线程执行到test指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样一条汇编指令便完成安全点轮询和触发线程中断。
Java线程的状态
通常在java 进程中的Java 的线程有几个不同的状态,如何让这些线程进入safepoint 的状态中,jvm是采用不同的方式
a. 正在解释执行
由于java是解释性语言,而线程在解释java 字节码的时候,需要dispatch table,记录方法地址进行跳转的,那么这样让线程进入停止状态就比较容易了,只要替换掉dispatch table 就可以了,让线程知道当前进入softpoint 状态。
java里会设置3个DispatchTable, _active_table, _normal_table, _safept_table
_active_table 正在解释运行的线程使用的dispatch table
_normal_table 就是正常运行的初始化的dispatch table
_safept_table safe point需要的dispatch table
解释运行的线程一直都在使用_active_table,关键处就是在进入saftpoint 的时候,用_safept_table替换_active_table, 在退出saftpoint 的时候,使用_normal_table来替换_active_table
b. 运行在native code
如果线程运行在native code的时候,vm thread 是不需要等待线程执行完的,只需要在从native code 返回的时候去判断一下 _state 的状态就可以了。
当Java线程正在执行native code的时候,这种情况最复杂,篇幅也写的最多。当VM thread看到一个Java线程在执行native code,它不需要等待这个Java线程进入阻塞状态,因为当Java线程从执行native code返回的时候,Java线程会去检查safepoint看是否要block(When returning from the native code, a Java thread must check the safepoint _state to see if we must block)
后面说了一大堆关于如何让读写safepoint state和thread state按照严格顺序执行(serialized),主要用两种做法,一种是加内存屏障(Memeory barrier),一种是调用mprotected系统调用去强制Java的写操作按顺序执行(The VM thread performs a sequence of mprotect OS calls which forces all previous writes from all Java threads to be serialized. This is done in the os::serialize_thread_states() call)(其实实现就是polling page,设置一个只读页,然后去读这个页上地址,产生一个中断,在中断处理中,blocking线程)
JVM采用的后者,因为内存屏障是一个很重的操作,要强制刷新CPU缓存,所以JVM采用了serialation page的方式。
说白了,就是在Java线程从执行native code状态返回的时候要作线程同步,采用serialtion page的方式做了线程同步,而不是采用内存屏障的方式。熟悉Java内存模型的同学知道,类似volatie这种轻量级同步变量采用的就是内存屏障的方式。
为什么要做线程同步呢?
这段代码首先将当前线程(不妨称为thread A)状态置为_thread_in_native_trans状态,然后读sync_state,看是否有线程准备进行GC,有则将当前线程block,等待GC线程进行GC。
由于读sync_state的过程不是原子的,存在一个可能的场景是thread A刚读到sync_stated,且其值是 _not_synchronized,这时thread A被抢占,CPU调度给了准备发起GC的线程(不妨称为thread B),该线程将 sync_stated设置为了_synchronizing,然后读其他线程的状态,看其他线程是否都已经处于block状态或者_thread_in_native状态,是的话该线程就可以开始GC了,否则它还需要等待。
如果thread A在写线程状态与读sync_state这两个动作之间缺少membar指令,那么上述过程就有可能出现一个场景,就是thread A读到了sync_stated为_not_synchronized,而thread B还没有看到thread A的状态变为_thread_in_native_trans。这样thread B就会认为thread A已经具备GC条件(因为处于_thread_in_native状态),如果其他线程此时也都准备好了,那thread B就会开始GC了。而thread A由于读到的sync_state是_not_synchronized,因此它不会block,而是会开始执行java代码,这样就会导致GC出错,进而系统崩溃。
主要原因就是读写safepoint state和thread state是不是原子的,需要同步操作,采用了serialization page是一个轻量级的同步方法。
c. 运行编译的代码
基本概念:
Poling page 页面:Poling page是在jvm初始化启动的时候会初始化的一个单独的内存页面,这个页面是让运行的编译过的代码的线程进入停止状态的关键。
编译:java 的JIT 会直接编译一些热门的源码到机器码,直接执行而不需要在解释执行从而提高效率,在编译的代码中,当函数或者方法块返回的时候会去访问一个内存poling页面.
当进入safepoint时,(SafepointSynchronize::begin 函数开始)函数体将会调用os:make_polling_page_unreadable();在linux os 下具体实现是调用了mprotect(bottom,size,prot) 使polling 内存页变成不可读。
信号:到当编译好的程序尝试在去访问这个不可读的polling页面的时候,在系统级别会产生一个错误信号SIGSEGV。
回到safepoint.cpp中,SafepointSynchronize::handle_polling_page_exception通过取出线程的safepoint_stat,调用函数void ThreadSafepointState::handle_polling_page_exception,最后通过调用SafepointSynchronize::block(thread()); 来block当前线程
d. block 状态
当线程进入block状态的时候,继续保持block状态。
4.什么场景、功能需要安全点的配合
JVM在很多场景下使用到safepoint, 最常见的场景就是GC的时候。对一个Java线程来说,它要么处在safepoint,要么不在safepoint。
注意:GC stop the world的时候,所有运行Java code的线程被阻塞,如果运行native code线程不去和Java代码交互,那么这些线程不需要阻塞。VM操作相关的线程也不会被阻塞。
5. Safe Region
safepoint只能处理正在运行的线程,它们可以主动运行到safepoint。而一些Sleep或者被blocked的线程不能主动运行到safepoint。这些线程也需要在GC的时候被标记检查,JVM引入了safe region的概念。safe region是指一块区域,这块区域中的引用都不会被修改,比如线程被阻塞了,那么它的线程堆栈中的引用是不会被修改的,JVM可以安全地进行标记。线程进入到safe region的时候先标识自己进入了safe region,等它被唤醒准备离开safe region的时候,先检查能否离开,如果GC已经完成,那么可以离开,否则就在safe region呆在。这可以理解,因为如果GC还没完成,那么这些在safe region中的线程也是被stop the world所影响的线程的一部分,如果让他们可以正常执行了,可能会影响标记的结果
safepoint只能处理正在运行的线程,它们可以主动运行到safepoint。而一些Sleep或者被blocked的线程不能主动运行到safepoint。这些线程也需要在GC的时候被标记检查,JVM引入了safe region的概念。safe region是指一块区域,这块区域中的引用都不会被修改,比如线程被阻塞了,那么它的线程堆栈中的引用是不会被修改的,JVM可以安全地进行标记。线程进入到safe region的时候先标识自己进入了safe region,等它被唤醒准备离开safe region的时候,先检查能否离开,如果GC已经完成,那么可以离开,否则就在safe region呆在。这可以理解,因为如果GC还没完成,那么这些在safe region中的线程也是被stop the world所影响的线程的一部分,如果让他们可以正常执行了,可能会影响标记的结果