JVM调优(四)GC算法与垃圾回收器

JVM调优(四)GC算法与垃圾回收器

GC

在C/C++语言中,没有自动垃圾回收机制,通过new关键字申请内存资源,通过delete关键字释放内存资源, 如果某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源,最终可能会导致内存溢出.

在Java语言中,有自动的垃圾回收机制,内存的释放由系统自动识别完成, 我们只需要关心内存的申请即可, 此时自动的垃圾回收的算法非常重要,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出.


常见的GC算法

自动化的管理内存资源,垃圾回收机制必须要有一套算法来进行计算,哪些是有效的对象,哪些是无效的对象,对于无效的对象就要进行回收处理.
常见的垃圾回收算法有:引用计数法、标记清除法、标记压缩法、复制算法、分代算法等

  • 1. 引用计数法

假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器
就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收

优点:

  1. 实时,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收.
  2. 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象

缺点:

  1. 每次对象被引用时,都需要去更新计数器,有时间开销
  2. 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计
  3. 无法解决循环引用问题(最大的缺点)
class TestA {
    public TestB b;
}

class TestB {
    public TestA a;
}

public class Main {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.b = b;
        b.a = a;
	// 虽然a和b都为null, 但由于a和b存在循环引用, 永远都不会被回收
        a = null;
        b = null;
    }
}
  • 2. 标记清除法

标记清除算法,是将垃圾回收分为2个阶段:

  1. 标记:从根节点开始标记引用的对象。
  2. 清除:未被标记引用的对象就是垃圾对象,可以被清理

模拟运行期间程序内的所有对象的状态, 初始标记状态都为0(也就是未标记):
image.png
当内存耗尽时, JVM暂停程序, 开始GC线程, 进行标记工作, 其中, 所有从根节点可达的对象都会标记为1, 此时完成第一阶段的标记
image.png
标记完成之后, 开始执行第二阶段的清除工作, 将所有未标记状态的对象全部清除掉, 并将留下的对象标记位重置为0
image.png
清除阶段完成之后, JVM唤醒停止的应用线程, 程序继续运行

优点:

  1. 解决了引用计数法的循环引用问题, 所用从根结点不可达的对象都会被回收
    缺点:
  2. 效率低, 标记和清除两个阶段都要遍历所有对象
  3. 清理之后的内存不连续, 碎片化严重, 留下的对象可能分散在内存的各个角落
  • 3. 标记压缩算法

标记压缩算法在标记清除算法的基础之上进行优化。

  1. 标记: 也是从根节点开始,对对象的引用进行标记
  2. 清理: 并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题

image.png

优点:

  1. 解决了标记清除算法的碎片化的问题
    缺点:
  2. 多了一步, 移动对象的内存位置,效率有一定的影响
  • 4. 复制算法

将内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收.

image.png

JVM中年轻代的内存空间
young_gc.png

  1. GC开始前, 对象只会存在于Eden区和名为From的一个survivor区, 另一个名为To的survivor区是空的
  2. GC时, Eden区所有存活对象都被复制到To区, From区中依然存活的对象根据年龄值决定去向, 年龄达到阀值的对象会被移动到老年代, 未达到阀值的对象被复制到To区
  3. GC结束之后, Eden区和From区清空, 此时From区和To区交换角色, 旧的From区成为新的To区, 旧的To区成为新的From区
  4. 每次GC都会持续这个过程, 直至To区填满, 将所有对象移动到老年代

优点:

  1. 如果内存中的垃圾对象较多, 则需要复制的存活对象就少, 效率高
  2. 回收后没有内存碎片
    缺点:
  3. 如果内存中垃圾对象较少, 则要复制的存活对象较多, 效率低, 比如老年代
  4. 两块内存区域, 同一时刻只能使用一块, 空间浪费
  • 5. 分代算法

由于各种垃圾回收算法各有优点, 都无法被替代, 因此可以根据垃圾回收对象的特点选择不同的算法.

分代算法根据回收对象的特点进行选择, 在JVM中, 年轻代适合GC频繁且每次存活对象较少, 适合使用复制算法, 老年代的对象存活时间较长, 适合使用标记清除算法或者标记压缩算法.


垃圾收集器以及内存分配

在JVM中, 根据GC算法实现了多种垃圾收集器, 包括串行垃圾收集器, 并行垃圾收集器, 并发垃圾收集器, G1垃圾收集器.

1. 串行垃圾收集器

串行垃圾收集器, 意指使用一个线程进行垃圾回收, 在执行时Java应用中的所有线程都要暂停, 等待垃圾回收完成.
在JavaWeb应用中一般不采用此垃圾收集器.

  • 编写代码
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Random;

public class TestGC {

    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<Object>();
        while (true){
            int sleep = new Random().nextInt(100);
            if(System.currentTimeMillis() % 2 ==0){
                list.clear();
            }else{
                for (int i = 0; i < 10000; i++) {
                    Properties properties = new Properties();
                    properties.put("key_"+i, "value_" + System.currentTimeMillis() + i);
                    list.add(properties);
                }
            }

           // System.out.println("list大小为:" + list.size());

            Thread.sleep(sleep);
        }
    }
}
  • 设置垃圾回收为串行收集器

在程序运行参数中添加2个参数,如下:

-XX:+UseSerialGC
指定年轻代和老年代都使用串行垃圾收集器

-XX:+PrintGCDetails
打印垃圾回收的详细信息

为了测试GC,将堆的初始和最大内存都设置为16M
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m

  • 启动程序
$ java -XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m -jar ZhiHuPicSpider.jar
[GC (Allocation Failure) [DefNew: 4350K->512K(4928K), 0.0029083 secs] 4350K->1479K(15872K), 0.0036821 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 4928K->349K(4928K), 0.0023885 secs] 5895K->1828K(15872K), 0.0024112 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
...
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.2.RELEASE)
...

可以看到控制台打印GC信息
GC日志信息解读:

年轻代的内存GC前后的大小:

  • DefNew
    • 表示使用的是串行垃圾收集器。
  • 4416K->512K(4928K)
    • 表示,年轻代GC前,占有4416K内存,GC后,占有512K内存,总大小4928K
  • 0.0046102 secs
    • 表示,GC所用的时间,单位为毫秒。
  • 4416K->1973K(15872K)
    • 表示,GC前,堆内存占有4416K,GC后,占有1973K,总大小为15872K
  • Full GC
    • 表示,内存空间全部进行GC

2. 并行垃圾收集器

并行垃圾收集器在串行垃圾收集器的基础之上做了改进,将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间。(这里是指,并行能力较强的机器)

并行垃圾收集器在收集的过程中也会暂停应用程序,这个和串行垃圾回收器是一样的,只是并行执行,速度更快些,暂停的时间更短一些。

  • ParNew垃圾收集器

ParNew垃圾收集器是工作在年轻代上的,只是将串行的垃圾收集器改为了并行。
通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器。

#参数
-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m

#打印出的信息
[GC (Allocation Failure) [ParNew: 4350K->512K(4928K), 0.0014631 secs] 4350K->1525K(15872K), 0.0015065 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 4928K->511K(4928K), 0.0008421 secs] 5941K->2143K(15872K), 0.0008652 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
  • ParallelGC垃圾收集器

ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。

相关参数如下:

-XX:+UseParallelGC
年轻代使用ParallelGC垃圾回收器,老年代使用串行回收器。

-XX:+UseParallelOldGC
年轻代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。

-XX:MaxGCPauseMillis
设置最大的垃圾收集时的停顿时间,单位为毫秒
需要注意的时,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能。
该参数使用需谨慎。

-XX:GCTimeRatio
设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)。
它的值为0~100之间的数字,默认值为99,也就是垃圾回收时间不能超过1%

-XX:UseAdaptiveSizePolicy
自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。
一般用于手动调整参数比较困难的场景,让收集器自动进行调整。

测试使用ParallelGC垃圾收集器

#参数
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m

#打印的信息
[GC (Allocation Failure) [PSYoungGen: 4096K->480K(4608K)] 4096K->1840K(15872K), 0.0034307 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

[Full GC (Ergonomics) [PSYoungGen: 505K->0K(4608K)] [ParOldGen: 10332K->10751K(11264K)] 10837K->10751K(15872K), [Metaspace: 3491K->3491K(1056768K)], 0.0793622 secs] [Times: user=0.13 sys=0.00, real=0.08 secs] 

3. CMS垃圾收集器

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的.
通过参数 -XX:+UseConcMarkSweepGC进行设置.

CMS垃圾回收器的执行过程如下:
2184951-cd90513432053c9a.png

  • 初始化标记(CMS-initial-mark) ,标记root,会导致stw;
  • 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  • 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  • 重新标记(CMS-remark) ,会导致stw;
  • 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  • 调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
  • 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
#设置启动参数
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m

#运行日志
[GC (Allocation Failure) [ParNew: 4926K->512K(4928K), 0.0041843 secs] 9424K->6736K(15872K), 0.0042168 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

#第一步,初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6224K(10944K)] 6824K(15872K), 0.0004209 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第二步,并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第三步,预处理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第四步,重新标记
[GC (CMS Final Remark) [YG occupancy: 1657 K (4928 K)][Rescan (parallel) , 0.0005811 secs][weak refs processing, 0.0000136 secs][class unloading, 0.0003671 secs][scrub symbol table, 0.0006813 secs][scrub string table, 0.0001216 secs][1 CMS-remark: 6224K(10944K)] 7881K(15872K), 0.0018324 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第五步,并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
#第六步,重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

4. G1垃圾收集器

G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。

G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的最大内存
  3. 第三步,设置最大的停顿时间
    G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
  • G1垃圾回收器的原理

G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。

这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。

20161222153407_691.png