天天看点

闲谈JVM(六):JVM垃圾回收概述前言JVM垃圾回收机制结语

文章目录

  • 前言
  • JVM垃圾回收机制
      • 何为垃圾
    • 为何分代管理
    • 生代内存分配
    • 吞吐量 VS 暂停时间
      • 吞吐量(throughput)
      • 暂停时间(pause times)
    • 垃圾收集器种类
      • 新生代收集器
      • 老生代收集器
    • Partial GC VS Full GC
  • 结语

前言

深入理解JVM虚拟机:(二)垃圾收集器概述

闲谈JVM(三):浅析本地元空间参数配置

闲谈JVM(二):浅析新老生代参数配置

闲谈JVM(一):浅析JVM Heap参数配置

本篇,我们聊聊JVM的垃圾回收机制。

JVM垃圾回收机制

众所周知,Java语言的核心特性之一,就是JVM的垃圾回收(GC)机制,可以让开发者无需关注程序的内存分配问题,将这部分琐碎且容易出错的操作交给JVM来进行处理。

但是这并不代表着作为开发者不需要去了解JVM的垃圾收集机制,当垃圾收集是主要瓶颈时,了解此垃圾收集实现的某些方面会非常有用。

何为垃圾

那在JVM中,哪些对象可以被标记为“垃圾”?

官方文档对此的定义是:

An object is considered garbage when it can no longer be reached from any pointer in the running program.

在程序运行过程中,没有任何指针指向的对象,可以被认为是“垃圾对象”。

最简单的垃圾回收算法会遍历每个可访问的对象,剩下的任何对象都被视为垃圾。

但是这种方法花费的时间与活动对象的数量成正比,这对于维护大量活动数据的大型应用程序是不可行的。

Java虚拟机结合了许多不同的垃圾收集算法,这些算法使用分代收集进行组合。

为何分代管理

闲谈JVM(六):JVM垃圾回收概述前言JVM垃圾回收机制结语

在Java虚拟机中,对象的生命周期大致可以如上图所示,x轴是对象寿命,以分配的字节为单位。y轴上的字节数是具有相应生存期的对象中的总字节数。

绝大多数的对象的生命周期是非常短暂的,只有少部分的对象生命周期非常的长,通常有一些在初始化时分配的对象,这些对象一直存在,直到进程退出。

由此,为了针对这种情况进行优化,Java虚拟机采用了分代管理的策略,即将整个堆区切分成几个存储着不同年龄对象的内存池,将其分为了新生代、老生代、本地元空间(永生代/方法区)。

闲谈JVM(六):JVM垃圾回收概述前言JVM垃圾回收机制结语

当某个分代内存不足时,垃圾回收会在该分代中进行,而会影响其他分代。

绝大多数对象分配在新生代中,并且大多数对象在那里死亡。因此,新生代的垃圾回收会频繁进行,经过多次垃圾回收仍存活的对象,可以进行“晋升”,进入到老年代当中。当然也有一部分大对象,其大小超过指定的阈值时,会直接被分配在老生代中,当老生代内存不足时,会进行老生代的垃圾回收,周而复始,保持整个堆区的内存可用性。

生代内存分配

对于新老生代的内存大小,我们可以通过参数进行配置,但是这部分物理内存真正全部分配给JVM了么?

我们来看一下官方文档的解释:

At initialization, a maximum address space is virtually reserved but not allocated to physical memory unless it is needed. The complete address space reserved for object memory can be divided into the young and tenured generations.

在JVM初始化阶段时,最大的地址空间实际上是保留的,除非需要,否则不会分配给物理内存。

这部分内存空间会被保留,等待分配至新老生代。

也就是说,当我们使用-Xmn128m 参数指定了新生代内存大小,但JVM初始化阶段,并未真的使用了OS这么大的内存,而是预先占用,在真正使用的时候,才进行分配。

闲谈JVM(六):JVM垃圾回收概述前言JVM垃圾回收机制结语

对于新生代内存,JVM又将其分为三部分,eden、survivor01、survivor02,大多数对象初始化阶段会被分配在eden区域,而两个survivor区域的其中一个会一直保持为空,用于GC收集阶段的对象整理。

划分出新生代的另一个好处是某种程度上解决了碎片化问题,或者说将最坏的情况推迟了。

那些存活时间短的小对象本来可能产生碎片化问题,但都在新生代的垃圾收集中被清理了。

由于存活时间长的对象被移到老年代时被更紧凑的分配空间,老年代也更加紧凑了。

随着时间推移(如果你的应用运行时间足够长),老年代也会产生碎片化,这时需要运行一次或是几次完全垃圾收集,同时JVM也有可能抛出内存溢出错误。

但是划分出新生代推迟了出现最坏情况的时间,这对于很多应用程序来说已经足够了。对于多数应用程序而言,它的确降低了stop-the-world垃圾收集的频率和内存溢出错误的机会。

由此可以看出堆区内存分代管理的好处,可以将各生代的内存根据实际情况更加合理的利用与调配。

吞吐量 VS 暂停时间

对于大多数的应用领域,评估一个垃圾收集(GC)算法如何根据如下两个标准:

  • 吞吐量越高算法越好
  • 暂停时间越短算法越好

首先让我们来明确垃圾收集(GC)中的两个术语:吞吐量(throughput)和暂停时间(pause times)。

吞吐量(throughput)

JVM在专门的线程(GC threads)中执行GC。只要GC线程是活动的,它们将与应用程序线程(application threads)争用当前可用CPU的时钟周期。

简单点来说,吞吐量是指应用程序线程用时占程序总用时的比例。例如,吞吐量99/100意味着100秒的程序执行时间应用程序线程运行了99秒, 而在这一时间段内GC线程只运行了1秒。

暂停时间(pause times)

暂停时间是指一个时间段内应用程序线程让与GC线程执行而完全暂停。 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。

如果说一个正在运行的应用程序有100毫秒的“平均暂停时间”,那么就是说该应用程序所有的暂停时间平均长度为100毫秒。同样,100毫秒的“最大暂停时间”是指该应用程序所有的暂停时间最大不超过100毫秒。

高吞吐量最好是指这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。

直觉上,吞吐量越高程序运行越快。 低暂停时间最好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。 这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。

因此,具有低的最大暂停时间是非常重要的,特别是对于一个交互式应用程序。

垃圾收集器种类

在JVM中,由于分代管理的存在,垃圾收集器可以分为两大类,新生代收集器与老生代收集器,下面我们来进行简单的介绍。

新生代收集器

新生代的垃圾收集器,主要有三种,分别如下:

1、Serial 收集器,它是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会是使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

2、ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本。ParNew 最重要的一点,是唯一的可以与老生代的CMS 收集器配合使用的新生代收集器,可以通过参数-XX:+UseParNewGC进行指定。

3、Parallel Scavenge 收集器,它与其他收集器的不同之处在于:它的关注点与其他收集器不同。CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量( Throughput)。

老生代收集器

老生代的垃圾收集器,主要分为四种,分别如下:

1、Serial Old 收集器,Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 “标记-整理” 算法。一般情况下,不会使用。

2、Parallel old 收集器,Parallel Scavenge 收集器的老年代版本,使用多线程和 “标记-整理” 算法。

3、CMS 收集器,以获取最短回收停顿时间为目标,目前较为推荐的GC 收集器,多数应用于互联网站或者B/S系统的服务器端上。

4、G1 收集器,Java 9以后的默认收集器,当前最炙手可热的GC 收集器,可以说兼顾了性能与时间的GC收集器。

在Java8中,默认的GC收集器采用了Parallel GC,也可以通过参数

-XX:+UseParallelGC

进行指定,来看一下Oracle官方的说法:

The parallel collector (also referred to here as the throughput collector) is a generational collector similar to the serial collector; the primary difference is that multiple threads are used to speed up garbage collection. The parallel collector is enabled with the command-line option

-XX:+UseParallelGC

. By default, with this option, both minor and major collections are executed in parallel to further reduce garbage collection overhead.

The parallel collector is selected by default on server-class machines. In addition, the parallel collector uses a method of automatic tuning that allows you to specify specific behaviors instead of generation sizes and other low-level tuning details. You can specify maximum garbage collection pause time, throughput, and footprint (heap size).

Partial GC VS Full GC

前面的介绍中,我们提到了新生代的GC与老生代的GC,但事实上,针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

Partial GC:并不收集整个GC堆的模式。

  • Young GC:只收集新生代的GC
  • Old GC:只收集老生代的GC。只有CMS的concurrent collection是这个模式
  • Mixed GC:收集整个新生代以及部分老生代的GC。只有G1有这个模式

Full GC:收集整个堆,包括新生代、老生代、永生代(本地元空间)等所有部分的模式。

关于新老生代的GC触发时机,我们前面已经提及,当所属生代的空间不足时,会触发所在生代的GC操作,

但Full GC的触发时机则有所不同,其可能的触发时机如下:

1、当需要在永生代(本地元空间)分配空间但已经没有足够空间时;

2、在代码中执行System.gc(),或者执行JVM命令

jmap -histo:live <pid>

3、当准备要触发一次Young GC时,如果发现统计数据说之前新生代的平均晋升大小比目前老生代剩余的空间大,则不会触发Young GC而是转为触发Full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集老生代的GC都会同时收集整个GC堆,包括新生代,所以不需要事先触发一次单独的young GC)

Full GC的执行代价会非常的高,它会对整个堆区进行整理收集,并进行Stop-the-world,暂停全部用户线程,直到Full GC执行结束。

结语

本篇我们对JVM的垃圾回收机制进行了简单的概述,在下一篇中,将会对具体的垃圾回收器进行详细的分析,敬请期待。