本文将主要讲解GC调优需要知道的一些基础知识,会涉及到一些GC的实现细节,但不会对实现细节做很全面的阐述,如果你看完本文之后,能对GC有一个大致的认识,那本文的写作目的也就达到了。由于在这次写作过程中,恰逢Java 9正式版发布,之前都是依赖Java 8的文档写的,如果有不正确的地方还望指正。本文将包含以下内容:
1.GC的作用范围
2.GC负责的事情
3.JVM中的4种GC
4.G1的一些细节
5.使用Java 9正式版对G1进行测试
6.一些简单的GC调优方法
一、GC的作用范围
要谈GC的作用范围,首先要谈JVM的内存结构,JVM内存中主要有以下几个区域:堆、方法区(JVM规范中的叫法,Hotspot大致对应的是Metaspace)、栈、本地方法栈、PC等,其中GC主要作用在堆上,如下图所示:
JVM内存结构
其中堆和方法区是所有线程共享的,其他则为线程独有,HotSpot JVM使用基于分代的垃圾回收机制,所以在堆上又分为几个不同的区域(在G1中,各年龄代不再是连续的一整片内存,为了描述方便,这里还使用传统的表示方法),具体如下图所示:
JVM堆中的分区
二、GC负责的事情
GC的发展是随着JDK(Standard Edition)的发展一步步发展起来的,垃圾回收(Garbage Collection)可以说是JDK里最影响性能的行为了。GC做的事情,说白了就是「通过对内存进行管理,以保障在内存足够的时候,程序可以正常的使用内存」。具体而言,GC通常做的事情有以下3个:
1. 分配对象和对象的年龄管理
通常而言,GC需要管理「在上图中的年轻代(Young)分配对象,然后通过一系列的年龄管理,将之销毁或晋升到老年代(Tenured)中去」的过程。这个过程会伴随着若干次的Minor GC。
对于普通的对象而言,分配内存是一件很简单而且快速的事情。在对象还未创建时,其所占内存大小通过类的元数据就可以确定,而Eden区域的内存可以认为是连续的,所以给对象分配内存要做的只是在上图中Eden区域中把指针移动相应的长度,并将地址返回给对象的引用即可。当然实际的过程比这个复杂,在下文中会提到。
不过,有时候一个对象会直接在老年代中创建,这个点也会在后边提到。
2. 在老年代中进行标记
老年代的GC算法可以大致是认为是一个标记-整理(Mark-Compact,其实是混合了标记-清理,标记-复制和标记-整理)算法,所以老年代的垃圾清理首先要做的就是在老年代对存活的对象(可达性分析,关于不同的可达性可以参考JDK解构 – Java中的引用和动态代理的实现)进行标记,对于寻求大吞吐量的服务器应用来说,这个过程往往需要是并发的。
标记的过程发生在Major GC被触发之后,不同的GC对于MajorGC的触发条件和标记过程的实现也不尽相同。
3. 在老年代中进行压缩
在上一条的基础上,将还存活的对象进行压缩(CMS和G1的行为与此有些不同之处),压缩的过程就是将存活的对象从老年代的起点进行挨个复制,使得老年代维持在一片连续的内存中,消除内存碎片,对于内存分配速度的提升会有很大的帮助。
三、GC的种类
Hotspot会根据宿主机的硬件特性和操作系统类型,将之分为客户端型(client-class)或者服务器型(server-class),如果是服务器型主机,Java 9之前默认使用Parallel GC,Java 9中默认使用G1。对于服务器型主机的选择标准是「CPU核心数大于1,内存大于2GB」,所以现在大部分的主机都可以认为是服务器型主机。
这里讨论的所有GC都是基于分代垃圾回收算法的。
1. Serail
Serail是最早的一款GC,它只使用一个线程来做所有的Minor和Major垃圾回收。它在运行时,其他所有的事情都会暂停。其工作方式十分简单,在需要GC的安全点,它会停止所有其他线程(Stop-The-World),对年轻代进行标记-复制,或对老年代进行标记-整理。
可以使用JVM参数-XX:+UseSerialGC来开启此GC,当使用此参数时,年轻代和老年代将都是用Serial来做垃圾回收。在年轻代使用标记-复制算法,将Eden中存活的对象和非空的Suvivor区&