java GC

作者: 瓢鳍小虾虎 | 来源:发表于2020-12-31 14:54 被阅读0次

jvm的堆内存回收也叫GC。该过程的第一步叫“标记”,就是先确认哪些内存已经不被使用,应该被回收。第二步就是“回收”,具体也有一些算法,适用不同场景。

标记

标记的方法有3类:
标记方式1- 引用计数:简单理解就是遍历判断哪些对象被引用了,哪些没有被引用的对象就会被GC回收。
引用计数有个缺陷就是“循环引用”问题:有2个对象obj1和obj2,这2个对象相互引用了,但是整个系统根本没有用到这2个对象,这两个对象成了孤岛,本应被回收却一直占着内存。

标记方式2- 可达性分析:从所有当前活动的对象(也叫GC Root)出发,顺着引用关系遍历,筛选出没有被引用到的对象,则认为是可以回收。目前GC回收标记方式都是用的可达性分析。

这个GC Root的选取规则:

1)jvm栈中引用的对象

  • 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
  • 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  • 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

2)本地方法栈中引用的对象
本地方法栈中执行非java语言编写的代码,例如C或者C++。

3)静态属性引用的对象
静态属性在jvm方法区中存放

4)方法区常量引用的对象
方法区在jvm范畴内,是一块所有线程共享的内存区域,它用于存储已被虚拟机加载的类的信息,常量,静态变量,及时编译器编译后的代码数据。

简单说就是从栈和方法区开始找,栈中只要引用到的都算,方法区的常量和静态属性引用到的都算。栈分为2个地方,jvm栈和本地方法栈。

对象的引用关系并不是都一样的,可达性分析中引用还会细分为一下4种类型:

引用类型

1) 强引用 Strong Reference:普通对象的引用关系都是强引用
2)软引用 Soft Reference:这种引用关系可以在jvm发现内存不足时候主动回收软引用指向的对象。典型场景是缓存。
3)弱引用Weak Reference:弱引用指向的对象随时都能被jvm回收。
4)虚引用Phantom Reference:通常无法通过这种引用关系访问对象,通常在回收(finalize)后执行指定逻辑的机制(例如Cleaner)。通常用的场景是GC相关操作或者对象监控等。

根据引用类型,对象的可达性可以有以下5种:

可达性级别

1)强可达 Strong Reachable:对象可以通过强引用关系访问就是强可达。
2)软可达 Soft Reachable:对象只有软引用就是软可达
3)弱可达 Weak Reachable:对象只有弱引用就是弱可达。只要引用被清除对象就可以销毁。
4)幻象可达 Phantom Reference:对象只有虚引用指向,并且finalize过了。
5)不可达 unreachable:没有引用指向,对象可以被回收了。

标记方式3- 方法区回收:主要是指类卸载的规则。

1)该Class的所有实例都被GC。
2)该Class的ClassLoader实例被GC。

回收算法

回收算法有3种:
标记回收算法 Mark-Sweep:先标记需要清除的对象内存空间,然后直接清除。这是最初的算法,其他算法都是沿用这个算法的思路。这个算法的问题在于有内存碎片化的问题。

复制算法 Copying:内存区域分为等额的2部分,标记过后把活着的对象复制到另一部分,并且顺序放置。这样避免了内存碎片化,但是整体看来浪费了一定的内存空间。

标记整理算法 Mark-Compact:在标记回收算法的基础上把活着的对象移动到了一起。这种算法优点是避免了内存碎片化,避免了复制算法内存浪费的问题,但是缺点是要消耗更多的执行时间。

既然以上算法各有优缺点,那我们该怎样使用呢?

jvm是这样做的,把内存空间划分为2个大的区域:新生代和老年代,空间比例1:2。新生代主要用复制算法,老年代主要使用标记整理算法。

新生代又细分为:Eden区,S0区(survivor的意思),S1区,比例为8:1:1。

新生代的回收过程是这样的:
第一次Eden区内存满的时候就开始使用复制算法回收,把存活的对象复制到S0区,Eden区清空。
第二次Eden区内存满的时候把Eden区和S0区活着的对象复制到S1区,然后清空Eden区和S0区。
第三次Eden区内存满的时候把Eden区和S1区活着的对象复制到S0区,清空Eden区和S1区。
然后像第二次和第三次这样执行往复循环下去。

新生代中活着的对象每经历过一次回收就会有一次计数+1,类似于年龄,年龄达到一定数值后就会被jvm认为此对象是常用对象,然后被移动到老年代。
新生代S区放不下的时候也会把对象移动到老年代。
大对象创建的时候也会直接被放到老年代,毕竟大对象来回折腾对性能也有消耗。

以上就是主流jvm的垃圾回收算法,执行垃圾回收算法需要垃圾收集器,jvm的垃圾收集器也分以下几种:

垃圾收集器

  1. 串行收集器:细分为2种新生代串行收集器和老年代串行收集器。串行收集器是单线程的,通常只会用在单处理器的终端系统。目前基本淘汰。

新生代串行收集器 Serial GC (-XX:+UseSerialGC): 只用于新生代,采用复制算法。
老年代串行收集器Serial Old (-XX:+UseSerialOldGC): 只用于老年代,采用标记-整理算法。

串行收集器执行的时候全程会出现STW现象(Stop The World),所有线程都停下来给GC让路,GC执行完毕再继续执行。

  1. 并行收集器:并行收起器是server模式jvm的默认设定。

并行收集器实现算法和串行收集器是类似的,也分新生代并行收集器(Parallel GC -XX:+UseParallelGC)和老年代并行收集器(ParallelOld GC -XX:UseParallelOldGC)。

并行收集器和串行收集器相比的一个优势就是新生代和老年代都可以多线程并行执行回收。总体节约了停顿时间。

并行收集器也称吞吐量优先的GC。使用的时候还可以设置GC时间和吞吐量值等,它还可以自适应调整Eden、Survivor、MaxTenuringThreshold的值。

吞吐量 = 用户代码执行时间 / (GC时间+用户代码执行时间)

下面是一些具体的参数介绍:
-XX:ParallelGCThreads : 设置垃圾回收的线程数目,通常跟cpu数目相等即可。
-XX:MaxGCPauseMills : 最大GC停顿时间,是一个正整数。
-XX:GCTimeRatio : GC吞吐量设置,0-100的整数值。
-XX:UseAdaptiveSizePolicy : 打开自适应GC策略,默认是开启的。

  1. 并发收集器:

CMS GC(Cuncurrent Mark Sweep GC)-XX:+UseConcMarkSweepGC

并发收集器 CMSGC 专用于老年代。使用的是标记清除算法,会遇到内存碎片化的问题。长期执行会出现Full GC (全局堆内存GC)导致恶劣停顿。

并发收集器除了可以多线程执行,另一个优势就是执行回收的线程是可以和其他业务线程并发执行。某些程度上实现了“一边回收一边执行”的效果。

CMS执行流程:

image.png
初始标记:寻找GC Root,单线程,这个过程会出现STW。
并发标记:GC线程和用户应用线程可以并发执行,过程中GC线程会再次标记哪些对象被使用。这个过程GC线程会跟业务线程争抢CPU资源,所以GC线程太多也会影响系统性能。
重新标记:多个GC线程重新标记对象,这个过程也会出现STW。
并发清除:GC线程回收会跟业务线程同时进行。

总的来说,CMS进一步减少了GC停顿时间,比较适用于对响应比较敏感的互联网web业务,目前CMS还算主流。只是CMS的优化空间基本已经达到上限,官方已经声明停止维护CMS了。以后应该会被新的GC收集器替代。

  1. 并行收集器 ParNew GC -XX:+UseParNewGC

ParNew GC只能用于新生代,和Parallel GC差不多都是多线程,ParNew甚至有很多代码都是与串行收集器共用的。这个收集器主要的作用就是和CMS同时使用,也只有这个收集器能与CMS同时使用。

  1. 并发收集器 G1 -XX:+UseG1GC

G1是针对大堆内存设计的GC收集器,兼顾吞吐量和停顿时间,是jdk9后为默认选型,目标就是替代CMS。G1是新生代和老年代通用的。

G1的执行逻辑:
G1将堆内存划分为很多个等大小的区域,不同区域之间可以看作复制算法,整体来看则是标记-整理算法。目前评价还不错,只是还没到大范围推广的阶段。

相关文章

网友评论

      本文标题:java GC

      本文链接:https://www.haomeiwen.com/subject/kinhoktx.html