什么是JVM
-
1.包含类装载子系统(ClassLoader)、运行时数据区、执行引擎、内存回收这四个部分组成
什么是安全点 参考文章://www.greatytc.com/p/c79c5e02ebe6
- 1.safePoint是程序中的某些位置,线程执行到这些位置时,线程中的某些状态是确定的,在safePoint可以记录OopMap信息,线程在safePoint停顿,虚拟机进行GC。
- 2.一般会在发生GC,Deoptimization时候使用安全点暂停线程.
- 3.从线程角度看当线程执行到这些位置的时候说明虚拟机当前的状态是·安全的`,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停暂停所以活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束
什么地方可以放safepoint
- 下面以Hotspot为例,简单的说明一下什么地方会放置safepoint
1、理论上,在解释器的每条字节码的边界都可以放一个safepoint,不过挂在safepoint的调试符号信息要占用内存空间,如果每条机器码后面都加safepoint的话,需要保存大量的运行时数据,所以要尽量少放置safepoint,在safepoint会生成polling代码询问VM是否要“进入safepoint”,polling操作也是有开销的,polling操作会在后续解释。
2、通过JIT编译的代码里,会在所有方法的返回之前,以及所有非counted loop的循环(无界循环)回跳之前放置一个safepoint,为了防止发生GC需要STW时,该线程一直不能暂停。另外,JIT编译器在生成机器码的同时会为每个safepoint生成一些“调试符号信息”,为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针
线程如何被挂起
- 1.如果触发GC动作,VM thread会在VMThread::loop()方法中调用SafepointSynchronize::begin()方法,最终使所有的线程都进入到safepoint。
- 2.在safepoint实现中Java threads可以有多种不同的状态,所以挂起的机制也不同,一共列举了5中情况:
1、执行Java code
- 在执行字节码时会检查safepoint状态,因为在begin方法中会调用Interpreter::notice_safepoints()方法,通知解释器更新dispatch table(JVM内部的一个数据结构,保存字节码和机器码)。
- 当线程在解释模式下执行的时候,让JVM发出请求之后,解释器会把指令跳转到检查safepoint的状态,比如检查某个内存页位置,从而让线程阻塞
2、执行native code
- 如果VM thread发现一个Java thread正在执行native code,并不会等待该Java thread阻塞,不过当该Java thread从native code返回时,必须检查safepoint状态,看是否需要进行阻塞。
- 这里涉及到两个状态:Java thread state和safepoint state,两者之间有着严格的读写顺序,一般可以通过内存屏障实现,但是性能开销比较大,Hotspot采用另一种方式,调用os::serialize_thread_states()把每个线程的状态依次写入到同一个内存页中.
- 当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)
3、执行complied code
- 如果想进入safepoint,则设置polling page不可读,当Java thread发现该内存页不可读时,最终会被阻塞挂起
- 当JVM以JIT编译模式运行的时候,就是最初说的在编译后代码插入一个检查全局的safepoint polling page,VM thread把它设置为不可读,让Java线程挂起
4、线程处于Block状态
- 即使线程已经满足了block condition,也要等到safepoint operation完成,如GC操作,才能返回。
- 当线程本来就是阻塞状态的时候,采用了safe region的方式,处于safe region的代码只有等到被允许的时候才能离开safe region.
5、线程正在转换状态
- 会去检查safepoint状态,如果需要阻塞,就把自己挂起。
- 当线程处在状态转化的时候,线程会去检查safepoint状态,如果要阻塞,就自己阻塞了
最终实现
- 当线程访问到被保护的内存地址时,会触发一个SIGSEGV信号,进而触发JVM的signal handler来阻塞这个线程.
- 那么线程到底是如何自己就阻塞了呢?在第2条的时候说了JVM可以使用mprotect 系统调用来保护一些所有线程可写的内存位置让他们不可写,当线程访问到这些被保护的内存位置时,会触发一个SIGSEGV信号,从而可以触发JVM的signal handler来阻塞这个线程
- JVM要阻塞全部的Java线程的时候,要先检查所有的Java线程所处的状态,通过mprotect系统调用来保护一块全局的内存区域,然后让Java线程进入安全点去polling这个内存位置,当线程访问到这个forbidden内存位置的时候会触发JVM的signal handler来阻塞线程。
线程如何恢复
- 有了begin方法,自然有对应的end方法,在SafepointSynchronize::end()中,会最终唤醒所有挂起等待的线程,大概实现如下:
- 1、重新设置pooling page为可读
- 2、设置解释器为ignore_safepoints
- 3、唤醒所有挂起等待的线程
进入safePoint 慢的影响
- 一个大概率的原因是当发生GC时,有线程迟迟进入不到safepoint进行阻塞,导致其他已经停止的线程也一直等待,VM Thread也在等待所有的Java线程挂起才能开始GC,这里需要分析业务代码中是否存在有界的大循环逻辑,可能在JIT优化时,这些循环操作没有插入safepoint检查。
safepoint的总结
- 1.主要有GC safepoint和deoptimization safepoint
- 2.最简洁的定义是“A point in program where the state of execution is known by the VM”即可以被VM知道在这个点是一个执行状态。
- 3.GC safepoint需要知道在哪个程序位置上,调用栈、寄存器等一些重要的数据区域里什么地方包含了GC管理的指针;如果要触发一次GC,那么JVM里的所有Java线程都必须到达GC safepoint;
- 4.Deoptimization safepoint需要知道在那个程序位置上,原本抽象概念上的JVM的执行状态(所有局部变量、临时变量、锁,等等)到底分配到了什么地方,是在栈帧的具体某个slot还是在某个寄存器里,之类的。如果要执行一次deoptimization,那么需要执行deoptimization的线程要在到达deoptimization safepoint之后才可以开始deoptimize。
- 5.不同JVM实现会选用不同的位置放置safepoint。以hotspot为例子:在解释器里每条字节码的边界都可以是一个safepoint,因为HotSpot的解释器总是能很容易的找出完整的“state of execution”。
- 6.而在JIT编译的代码里,HotSpot会在所有方法的临返回之前,以及所有非counted loop的循环的回跳之前放置safepoint。
- 7.HotSpot的JIT编译器不但会生成机器码,还会额外在每个safepoint生成一些“调试符号信息”,以便VM能找到所需的“state of execution”。
- 8.为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针;
- 9.为deoptimization生成的符号信息是debugInfo,指出如果要把当前栈帧从compiled frame转换为interpreted frame的话,要从哪里把相应的局部变量、临时变量、锁等信息找出来。
- 10.还有一种情况是当某个线程在执行native函数的时候。此时该线程在执行JVM管理之外的代码,不能对JVM的执行状态做任何修改,因而JVM要进入safepoint不需要关心它。所以也可以把正在执行native函数的线程看作“已经进入了safepoint”,或者把这种情况叫做“在safe-region里”。JVM外部要对JVM执行状态做修改必须要通过JNI。所有能修改JVM执行状态的JNI函数在入口处都有safepoint检查,一旦JVM已经发出通知说此时应该已经到达safepoint就会在这些检查的地方停下来把控制权交给JVM。
之所以只在选定的位置放置safepoint是因为:
- 挂在safepoint的调试符号信息要占用空间。如果允许每条机器码都可以是safepoint的话,需要存储的数据量会很大(当然这有办法解决,例如用delta存储和用压缩)
- safepoint会影响优化。特别是deoptimization safepoint,会迫使JVM保留一些只有解释器可能需要的、JIT编译器认定无用的变量的值。本来JIT编译器可能可以发现某些值不需要而消除它们对应的运算,如果在safepoint需要这些值的话那就只好保留了。这才是更重要的地方,所以要尽量少放置safepoint
- 像HotSpot VM这样,在safepoint会生成polling代码询问VM是否要“进入safepoint”,polling也有开销所以要尽量减少。
safeRegion
- 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所影响的线程的一部分,如果让他们可以正常执行了,可能会影响标记的结果
什么是OopMap
- 1.JVM维护了一个专门的映射表(OopMap)记录哪些位置存放着对象引用,来快速完成根节点枚举过程。
- 2.为每一个操作记录OopMap不现实,HotSpot虚拟机引入了safePoint。
- 3.safePoint是程序中的某些位置,线程执行到这些位置时,线程中的某些状态是确定的,在safePoint可以记录OopMap信息,线程在safePoint停顿,虚拟机进行GC。
- 使用例子:JVM 通过JIT编译器为每个safepoint生成一些调试符号信息---OopMap(栈上和寄存器里哪里有GC管理的指针)。
- jvm在安全点更新OopMap中的引用,而不需要一发生改变就去更新这个映射表,这也就是为什么我们说在安全点我们的状态是确定的。
- 当垃圾回收时,收集线程会对栈上的内存进行扫描,看看那些位置上存储了Reference类型。如果发现了某个位置上存储的是Reference类型,就意味着这个引用所指向的对象在这一次垃圾回收过程中不能够回收。
- 栈上的本地变量表里面只有一部分数据是Reference类型的,为了避免每次都扫描整个栈,所以采用空间换时间的策略。在某个时候(安全点)把栈上代表引用的位置记录下来,GC时直接读取,避免了全部扫描
RememberedSet
- 新生代 GC(发生得非常频繁)。一般来说, GC过程是这样的:首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。但问题是,确实可能存在位于老年代的某个 GC Root,它引用了新生代的某个对象,这个对象你是不能清除的。那怎么办呢?
- 事实上,对于位于不同年代对象之间的引用关系,虚拟机会在程序运行过程中给记录下来。对应上面所举的例子,“老年代对象引用新生代对象”这种关系,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是RememberedSet,RememberedSet记录的是新生代的对象被老年代引用的关系。所以“新生代的 GC Roots ” + “ RememberedSet 存储的内容”,才是新生代收集时真正的 GC Roots