聊一聊GC的内容

堆区的内存清理

Java的一大特性就是支持垃圾的自动回收,这里的垃圾就是不再使用的对象,而完成自动回收的垃圾回收器(简称GC)。Java领域中,主流的Java虚拟机采用可达性分析算法来管理内存。更加简单的引用计数法更多使用在一些脚本语言如python,因为引用计数法最大特点就是回收及时,一旦引用为0就可以清除,而可达性分析通常至少需要标记和清除(这一步也可能是其他的处理)两个阶段,一个垃圾对象至少经过两轮标记才会被清除

python的GC主流实现中,对于不可能产生循环引用的对象如数值、字符串采用引用计数,而对于集合、自定义对象等采用类似标记-清除的算法进行回收。

大部分对象存放在堆内存,而如果堆内存不足以用户申请的对象,则会触发GC进行垃圾回收,而如果最终仍然无法放下,将终止运行,并抛出OutOfMemoryError的异常。(在这里是堆内存溢出,而该异常也会出现在方法区溢出、直接内存溢出的场景)

JVM中可以通过 -Xms和-Xmx来调整堆内存的范围。其中-X代表标准,所有JVM标准下的JVM实现都遵循-X,ms就是memory size 即JVM给堆内存的最小值,随着对象的创建堆的大小肯定是不断变大(动态扩展),而mx 就是memory max 即堆的最大值。
如果想要禁止堆的动态扩展,而限制堆内存为一个固定大小,则可以将-Xms和-Xmx指定一个同样的大小

可达性分析

可达性分析是主流JVM实现用于判断对象是否存活的方法。首先需要通过根结点枚举确定根结点集合GC roots,然后从这些节点开始,根据引用关系向下搜索对象图。这个对象图就是与GC roots可达的对象。(具体标记哪个对象与具体GC实现有关)

根对象集合包括(这里可以理解为引用,或者引用直接指向的堆中对象):
【1】栈内存中,包括操作数栈、局部变量表引用的对象(具体表现为使用到的局部变量、返回值、参数)
【2】方法区引用的对象:classLoader和class对象
【3】常量池引用的对象,如常量池中的(引用的)字符串对象
【4】被同步锁synchronized持有的对象
【5】JVM内部的引用如系统类加载器、一些常驻的异常对象
【6】类所属的静态变量
如果一个指针保存了堆中对象的地址,但是自己由不存放在堆中(存于栈中),那么它就是一个root

常驻JVM内存的对象基本都是不能轻易回收的,因为它们总是被生命周期更长的对象引用(例如JVM实例本身可以看作一个进程对象),他们就可以作为可达性分析的根对象,方法区总是保留引用的两个对象classLoader和class对象、常量池中保存的字符串字面量是什么——堆中字符串对象的引用、monitor关联的对象、总是需要用到的异常对象、与类同生共死的静态变量指向的对象、栈帧中各种结果指向的对象等

根结点枚举

这里存在两个待优化或解决的问题:

【1】 如果使用可达性分析算法进行判断,则分析工作必须在一个能保证一致性的快照中进行,否则无法保证结果的准确性。因此需要stop the world,停止所有的用户线程工作,使得GC线程可以在一个一致性快照中进行垃圾回收工作。

STW原因
【1】可行性分析工作必须在一个能确保一致性的快照中进行,分析期间整个执行系统需要是静止在某一个时间点的。
【2】如果分析过程中,对象引用关系在不断变化,会使得分析结果的准确性收到影响。
STW无法被避免,是JVM在后台自动发起和自动完成的,对用户透明。垃圾回收器的升级不断优化STW的时间

不断的GC可能造成频繁的STW,这会使得程序运行显得很慢,本质上是由于内存吃紧而不断触发GC。

系统频繁变慢和通常和频繁STW有关,频繁STW是频繁GC的表现,一般是因为内存不够用了,再不GC就OOM了,这时候可以看一下是不是存在内存泄露了或者被外部DDOS攻击了。

【2】对象那么多,如果全部遍历一遍去进行根结点枚举,那么无疑是大海捞针,而且根结点枚举在垃圾回收器的大部分实现中,都是无法避免STW的,因此需要找到一种方法进行优化,以保证快速地完成根结点枚举。
主流的JVM使用的都是准确式垃圾收集(JVM可以知道内存中某个位置的数据具体是什么类型——字面量还是引用),JVM不需要查找所有GC root,而是可以使用一组数据结构,它指明了哪些位置存在对象引用。Hotspot中,使用一组称为OopMap的数据结构来快速完成GC root枚举。
一旦类加载完毕,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,并保存在oopMap中。

导致OOPMap内容变化的指令很多(程序运行期间很多指令都有可能修改引用关系),Hotspot没有为所有指令生成OOPMap,而是在安全点记录了OOPMap相关信息,只有在安全点才会对oopMap做一个统一的更新,因此只有安全点位置的oopMap一定是准确的,因此只能在安全点处理GC行为,进而STW一般也只发送在安全点

一段程序被若干个安全点切出若干程序段,而CPU执行到程序安全点时才会GC,因为HotSpot只在安全点记录了oopMap的信息,而oopMap用于实现快速枚举GC root。
源码中变量都是有类型的,但是一旦经过编译后,变量就只有在局部变量表中slot的位置,oopMap用来说明栈上某个位置存放的变量原来是一个什么类型的
换句话说,假如我们不计成本和实现复杂度,在任何位置、为每条指令的位置都记录oopMap,那么HotSpot的GC可以在任何时候进入GC,因为任何位置都是一个安全点。

安全点

程序执行过程中并不是在任意位置都能够停下来开始GC,而是到达“安全点”。安全点通常为与方法调用处、循环跳转、异常跳转位置——经常被复用的指令、执行时间较长的指令
主流JVM采用主动式中断——垃圾收集器需要中断线程时,设置一个中断标志,各个线程在运行过程时不停地主动轮询这个标志,如果为真则在最近的安全的主动挂起

这个中断标志就设置在安全点以及所有创建对象和其他需要在堆上分配内存的地方(为了没有足够内存分配新对象)

但是对于处于阻塞或等待状态的线程,就无法执行中断操作,因为它们此时无法进入运行状态,这时需要引入安全空间。(如果一个线程长期sleep,那么GC将暂时无法回收该线程在堆中产生的垃圾)
安全空间可以看作安全点的拉伸,线程要么冻结在安全点的位置,要么冻结在安全区域的范围

安全区域:指的是一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的

我的理解:安全空间内,保证了oopMap的内容不会被改变,即使此时sleep的线程没有被主动挂起进入STW状态,GC也可以对该线程进行根结点枚举,相当于该线程运行到了安全点,而涉及sleep、blocked的代码都属于安全区域代码,因为它不会改变某些对象的引用关系

【1】当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,如果这段时间发生GC,那么JVM会忽略标识为“已进入安全区域”状态的线程(因为JVM知道安全区域代码段内的oopMap内容不会变)。(进入安全空间后,即使程序因为某些原因无法响应JVM的中断请求,JVM也可以开始进行根结点枚举工作)

一致性视图是指 “一致的引用关系” ,如果一段代码不会改变这个一致性视图,那么STW并GC和边GC边使得线程继续执行这段“不影响”的代码之间的效果差不多,但是如果线程想要退出这段安全区域代码,则需要“经过JVM的允许”

【2】当线程即将离开安全区域的时候,会检查JVM是否已经完成了根结点枚举,完成了则继续执行。否则需要等待接收JVM“允许离开安全空间”的通知。

因为根结点枚举是STW的,如果没有进行完毕,那么用户线程需要停在安全区间,虽然它没有被STW,但是需要逻辑上达成STW的效果。因为离开安全空间时,可以看作达到安全点(离开安全点之后无法保证oopMap的准确性),而此时的JVM可能只在对程序进行根结点枚举,如果不经过JVM通知就离开安全空间相当于为枚举工作“添乱”,使得GC使用的oopMap不准确。

总结
GC roots枚举需要保证:
【1】确保一致性快照
一致性视图:分析期间,整个系统需要是静止在某个时间点的,并且能保证所有线程都能够响应JVM的

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值