- 一、什么是垃圾Garbage?
- 二、如何定位垃圾?
- 1.引用计数算法
- 2.根可达算法
- 三、GC Roots 根对象的枚举
- 四、JVM 对 GC Roots 根对象枚举过程的优化
- 1. OopMap(Ordinary Object Pointer)
- 2. CardTable
- 总结
一、什么是垃圾Garbage?
没有任何引用指向一个对象或者多个对象(多个对象之间循环引用)
二、如何定位垃圾? 1.引用计数算法reference count: 引用计数,即在对象上记录着有多少个引用指向它。
2.根可达算法root searching: 根可达算法,根对象包含 线程栈变量,静态变量,常量池,JNI指针。
根对象包含:
- JVM stack:虚拟机栈(栈桢中本地变量表)中引用的对象
- native method stack:本地方法栈中JNI(即一般说的Native方法)中引用的对象
- runtime constant pool:运行时常量池引用的对象
- static reference in method area:方法区中类静态属性
- Class:方法区中Class对象
由于引用计数的算法存在无法解决循环引用的的问题,所以目前的垃圾回收器的查找需要回收的对象都是采用的根可达算法。
三、GC Roots 根对象的枚举JVM经过这么多年的进步,现如今主流的垃圾回收器为了能够减少GC占用的资源,已经能够做到在部分垃圾回收的阶段实现并发标记,并发清理,在用户线程运行的同时,去进行垃圾的回收,从而避免长时间挂起用户线程。
为什么说是部分阶段实现并发标记,清理呢?
这是由于目前主流的JVM的垃圾回收器仍然都是基于准确式的垃圾回收,在去定位那些需要回收的无用的对象就需要非常的精确,基于根可达算法,在GC的初始阶段(比如 根对象的枚举)还是需要暂停用户线程,但是这些阶段暂停的时间也都是可控的。
为什么说该阶段的暂停时间可控呢?
一个原因在于GC Roots对象相对于堆中的对象来说,数量相对较少,其次主要的原因还是在于JVM对这一步骤进行了一系列的优化,下面我们就看看JVM在这一步骤中进行的两种优化:
在固定根对象中,其中虚拟机栈是运行时产生的,每个线程运行时对应一个栈,一个栈又由多个栈帧组成,一个栈帧对应着一个方法,GC线程进行扫描时,需要去扫描线程栈上内存的区域,以确定
在虚拟机栈的本地变量表中哪些地方对堆中的对象持有引用关系。
而扫描整个虚拟机栈并非易事,因为GC回收只应该只关心那些是Reference类型的数据,而那些非Reference类型的,如果GC线程也进行了扫描,那就是对资源和时间的浪费。
所以JVM就想了一个办法,可以利用空间换时间的方式,JVM在扫描虚拟机栈的时候为了避免全栈扫描而采用了一种OopMap(Ordinary Object Pointer Map)的数据结构。
可以把oopMap简单理解成是调试信息。 在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。OopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。
这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。
对于JIT编译后的某些方法也会在特定的一些位置去维护OopMap,这些位置称为安全点(Safe Point),之所以要选择一些特定的位置来记录OopMap,
是因为如 果对每条指令(的位置)都记录OopMap的话,这些记录就会比较大,那么空间开销会显得不值得。选用一些比较关键的点来记录就能有效的缩小需要记录的数据量,但仍然能达到区分引用的目的。因为这样,HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。
通过OopMap,这样GC线程就避免了去扫描整个堆栈,直接去OopMap中找到那些属于GC Roots的引用,开始进行遍历,这样可以大大缩小根节点枚举的时间。
2. CardTablehttps://www.cnblogs.com/plxx/p/4217812.html
有一种情况的跨代引用如果按照简单的根可达算法进行扫描会产生性能问题,比如:老年代对象持有年轻代对象的引用,在年轻代进行YGC的时候,如果要确定年轻代对象是否没有被老年代对象引用指向,就需要去扫描整个老年代,从而产生性能问题,所以为了解决这个问题,所以在HotSpot虚拟机中提出了一个CardTable的概念,用于记录下这些跨代引用的关系。
CardTable也利用了物理内存中的Page页概念,JVM也是将堆内存分为一个个大小为2的幂次方的一个个CardPage,每个Card Page大小介于128~512个字节,而CardTable的数据结构其实就是一个字节数组,
在数组中记录哪些CardPage是Dirty,具体实现如下:
首先在JDK声明CardTable的源码大致如下(假设设定的CardPage的大小为512个字节):
CardTable[this.address >> 9] = 0;
将当前空间的起始地址值向右移9位(即除以2的9次方,512)得到整个CardTable,所以CardTable的每个元素的索引即代表0, 512, 1024…对应的CardPage起始地址,依次类推…
从上图可以看出,起始数组对应的下标 index * CardPageSize 即每一个CardPage的起始地址值,CardTable数组中的每个元素即代表CardPage是否为Dirty。
这样当老年代引用了年轻代的对象的时候,就会通过写屏障(Write Barrier)将老年代对象所在的CardPage标记为Dirty,这样在GC进行回收标记的时候,就可以避免去扫描整个老年代,直接去扫描CardTable找到哪些为Dirty的CardPage即可。
写屏障(Write Barrier)可以看做虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用类型赋值时,会产生一个环绕通知(Around),供程序执行额外的动作,也就是赋值的前后都是在写屏障的覆盖范围之内。在赋值前的部分的写屏障称为写前屏障(Pre-write Barrier),在赋值之后的称为写后屏障。
在并发情况下,如果多个线程都产生了写屏障,必然会影响程序的性能能,所以JVM利用-XX:+UseCondCardMark进行判断,如果当期CardPage已经标记为Dirty,则不进行 *** 作,
所以通过这个判断减少并发写 *** 作,可以避免在高并发情况下可能发生的并发写卡表问题。
来源于:https://www.jianshu.com/p/968215c6a924
总结
综上可以看出,对于仅仅根对象的枚举,JVM都做了这么多的优化 *** 作,最终的目的就是能够减少应用的停顿时间,提升GC的效率。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)