Bitget下载

注册下载Bitget下载,邀请好友,即有机会赢取 3,000 USDT

APP下载   官网注册

GC是GarbageCollection资源回收的英文缩写。是一个应用程序不可或缺的机制。不管用什么语言开发的应用,都离不开它,但是,我们平时讲的最多的是java语言开发项目的GC。首先,java是当前应用服务开发中使用最多的程序语言;其次,java语言有一套完整、易懂的内存管理机制,而且还在不断完善之中。这两点,就足以成为大家讨论的焦点。另外,对于一个应用服务而言,总是在不断追求性能提升之中,而只有搞懂它,才能让项目的性能更加优秀。

所以,做为一位性能测试人员,我们需要懂得一些GC知识。

在讲解GC知识之前,我们还需要试图理解一下java的内存知识。

java内存知识

一个java程序启动,会向内存条中申请一段内存空间,用来存放程序运行所需要的基础数据,这个空间的大小,可以通过java运行容器即JVM(java虚拟机)配置参数进行配置。java程序启动,要保持运行状态,就需要把一些数据放入内存中,而这些数据种类非常多,每个种类的占用空间大小也各不相同,他们分别存放在内存中的什么位置,也都会记录在虚拟内存中。

使用项目功能时,不同的功能,需要不同类型的数据,数据量大小也不一样,CPU就会去问虚拟内存,获取数据类型以及其在内存中的位置,如果内存中拿不到,就去磁盘或其他外部输入上获取数据。

新的数据,也将暂时保存到内存中,时间一久,内存中放置的数据就会越来越多,如果,不进行空间回收,再大的内存空间也不够用,更何况内存空间不可能无限大,所以,内存需要GC资源回收,这样,才能保障项目持久运行

但是,GC资源回收,也不能一刀切,什么都回收,而应该只回收无用的‘垃圾’,所以,有时也把GC叫做‘垃圾回收’。

既然不能一刀切,那就得仔细分析一番了。

首先,一个项目在内存中的数据种类很多,但是,最主要的还是堆Heap类型,这个区域几乎占据了整个内存空间的95%以上。所以,GC资源回收问题的核心就是对堆区资源的回收。

这么大一个区域,它也是有细分的,以JDK_HostSpot虚拟机为例(当下java程序使用最广泛的JVM),传统的分法是把堆区分为:新生代New区,老年代Tenured区,持久代PermGen(java7及以上)或叫元空间MetaSpace(java8及以后)

项目运行过程中,新进入内存的数据,首先进入的是新生代New区,所以,理论上期望新生代越大越好,这样就能装更多的数据,但是,进入新生代的数据,大多数,只会使用少量几次,就再不会被用到,如果不进行清理,那就是浪费宝贵的内存空间,所以,需要制定清理策略,清理无用的数据,腾出空间,临时存放新数据,从而提高新生代空间复用率。但是,这个清理,就像‘打扫家庭卫生’一样,清理一遍,就会有些数据位置移动,移动数据,就需要更新虚拟内存,而如果此时要往新生代中丢入数据,或读取数据,都无法操作,所以,每次清理,都会出现短暂卡顿。哪为什么项目的使用者,感受不到这个卡顿呢?这是因为卡顿的时间很短,一般人是直接感受不出来的,如果人都感受出来了,那么,此时,项目的性能就非常糟糕,用户体验就非常差了。

对新生代进行多次清理之后,如果还有一些数据‘赖’着不走,就会被移动到老年代中,同时更新虚拟内存,告诉该数据已经转移到老年代中,后续要用,就去老年代的内存地址中去读取,这就是老年代中数据的来由。老年代和新生代一样,理论上也期望能无限大,但是,实际是不可能的,所以,它也需要制定清理策略,从而提高空间利用率。

持久代(元空间),这个区域,主要是存放一些代码类,相对来说变动的可能性比较小,所以叫‘持久’代嘛,但是,也并不是说就永远不会变,只是相对变动比较小,也是需要定期清理再利用的。

明白了这些之后,java内存空间,视乎就成为了一个三元一次方程。假设X为新生代,Y为老年代,Z为元空间,N为单位时间新生代回收次数,T为单位时间老年代回收次数,P为单位时间元空间回收次数,x为新生代资源每次回收卡顿时长,y为老年代资源每次回收卡顿时长,z为元空间资源每次回收卡顿时长,他们的关系是:

X + Y + Z = 总内存NX + TY + PZ = 总利用内存NXx + TYy + PZz = 总卡顿时长

如果,把新生代X调大,N就会变小,x就会变大;把老年代Y调大,T就会变小,y就会变大;把元空间Z调大,P就会变小,z就会变大。而且,老年代资源每次回收造成的卡顿时间y,一般是远大于新生代资源每次回收卡顿时长x。所以,最后java内存性能优化,就变成了这样一个三元一次方式求最优解了。

这样,一个三元一次方程,不是一个简单数学题,而是一个工程题

优化新生代、老年代资源回收策略,降低每次卡顿时长,就能整体降低总卡顿时长;优化新生代和老年代的空间大小,就能改变资源回收的频率,从而改变总卡顿时长;所以,在java语言的迭代更新中,我们看到,不断的在优化资源回收策略。

GC资源回收策略

从目前的讨论最多的GC回收策略来看,主要集中在如下集中。

GC回收策略

开启参数

解析

Serial收集器(串行回收器)

-XX:+UseSerialGC

最老最稳定的高效回收器,使用一个线程去回收新生代和老年代,新生代为复制算法(Copy),老年代用标记-清理(Mark-Sweep)清理算法,回收时会暂停服务,导致服务卡顿。

ParNew收集器

-XX:+UseParNewGC

-XX:ParallelGCThreads 限制线程数

是Serial收集器的多线程版本,新生代并行,老年代串行。

Parallel收集器(并行收集器)

-XX:+UseParallelGC

Parallel收集器(PS)可以通过参数,动态调节以提供最合适的停顿时间或最大吞吐量。用复制算法(Copy)并行回收新生代,用标记-整理(Mark-Compact)串行回收老年代。

Parallel Old收集器(并行回收老年代)

-XX:+UseParallelOldGC

ParallenOld收集器,新生代和老年代,都并行回收。

CMS(ConcurrentMarkSweep)收集器(并发标记清除收集器)

-XX:+UseConcMarkSweepGC

一种以获取最短卡顿时间为目标的收集器,新生代依然使用ParNew收集算法,而老年代使用标记-清理(Mark-Sweep)算法。

G1收集器(GarbageFist)

-XX:+UseG1GC

java9开始的默认回收器,这种收集器,将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,新生代和老年代已经不是连续的内存空间,它们都是一部分(可以不连续)Region的集合。资源回收时,采用标记-压缩(Mark-Compact)算法,不会产生内存碎片。

注意:每一种资源回收机制,都有很多配置参数,这里只列出开启某中回收机制的配置参数,其他配置参数,需要自己拓展。但是,要注意,每种回收机制的配置参数都不尽相同,不要混用。

很多人可能都知道,新的jdk版本如11、17、19都在宣传性能上得到很大提升,其提升的一个重要手段就是采用G1收集器,并不断优化执行效率。

知道有这些回收策略可选,那怎么知道哪些‘垃圾’,是可以被回收的呢?

哪些可以被回收?

这个一般有两种算法:

  • 引用计数法:每个对象都有一个引用计数属性,新增一个引用时,计数就加1,释放时,就减1,当计数为0时,说明该对象没有被引用了,可以被回收。
  • 是否可达法:从GC根对象开始往下查找引用链路,如果某个对象,没有任何引用链路可达,就说明该对象是一个数据‘孤岛’,可以被回收。

什么时候回收?

这个就很容易了,常见的也有两种:

  • 定时回收:就是设定一个间隔时间长度,每隔一个间隔时间,就进行一次回收。
  • 触发回收:配置一个触发阈值,如设置‘-XX:CMSInitiatingOccupancyFraction=70’是使用CMS收集器时,内存空间使用率达到70%就触发资源回收。

具体怎么回收?

常见的有四种:

  • 复制算法(COPY):就是将内存划分为两个相等的内存空间,每次只使用其中一块,当其中一块内存满时,将依然存活的对象复制移动到另外一块空的内存,然后再把已使用过的内存空间一次清理掉。
  • 标记-清理(Mark-Sweep):首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
  • 标记-压缩(Mark-Compact):它的过程与标记-清理相似,不同的是,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的对象。
  • 分代收集算法(Generational-Collection):是以上几种算法的结合使用,将内存划分为几个区,结合每一块内存的功能和对象生存周期,为每块区域使用不通的收集算法,实现垃圾收集的效率最大化。

好了,现在我们对GC有了基本了解了,可以先自己思考下,如何通过调整GC和其参数,改变项目性能?同时,也可以思考一下,为什么有GC资源回收机制,还会出现OOM内存溢出的问题呢?

#头条创作挑战赛#

#大有学问#