本文共 4738 字,大约阅读时间需要 15 分钟。
之前我们介绍了jvm的内存结构,了解到对于程序计数器、jvm虚拟机栈和本地方法栈,是属于线程私有的,每个线程对应一套,而对于堆和方法区,是属于线程共享的,我们讨论的垃圾回收也主要是针对堆和方法区的内存回收
虚拟机当中将堆内存从逻辑上分成了三大块:年轻代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation)
新生代:
新生代由 Eden和Survivor Space(S0,S1)组成,通过-Xmn20M可以设定新生代的大小,-XX:SurvivorRatio=8, 表明Eden区和Survivor区比例是=8:2
Eden区:一般新创建的对象都会存放在这个区中,如果Eden区满了,会触发一次Minor GC
Survivor区:存活区,触发MinorGC时,会把Eden区和S0区当中存活的对象复制到S1区当中,然后清空Eden区和S0区,如果一个对象存活的年龄超过了设定的-XX:MaxTenuringThreshold 值将会进入到老年底
老年代
老年代用用来存放超过一定年龄的对象,或者大对象(对象大小超过设定的值,-XX:PreTenureSizeThreshold),当老年代空间不足时,会触发一次FullGC
持久代(永久代)
在JDK8之前的hotspot当中,类的元数据如方法数据,方法信息,运行时常量池等信息保存在永久代当中,通过-XX:MaxPermSize设定,一旦类的元数据大小超过了设定值,就会抛出OOM错误
在JDK8当中,不再使用永久代,而是将类的元数据保存在了本地内存区域(堆外内存),称之为元空间。
原因:对于永久代的调优很困难,并且无法确定其大小,其中保存了太多的信息,类的总数,常量池大小和方法总数,并且随着FullGC而移动。在JDK8当中,保存在了本地内存当中,元空间的最大空间就是系统可用内存空间,可以避免永久代内存溢出问题,但是一旦发生内存溢出,会占用大量的本地内存。
对象存活判断
判断对象是否存活有以下两种方式
引用计数
在每个对象上都有一个引用计数,新增一个引用时,引用计数加1,引用释放时,引用计数减1,当计数等于0时,可以进行对象的回收。引用计数存在一个问题就是无法解决循环引用的问题。我们看一个例子
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { // 定义两个对象 ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); // 给对象的成员赋值,即存在相互引用情况 objA.instance = objB; objB.instance = objA; // 将引用设为空,即没有到堆对象的引用了 objA = null; objB = null; // 进行垃圾回收 System.gc(); } public static void main(String[] args) { testGC(); }}
在上述例子当中,我们把jvm参数设置成如下: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
分别对象的是,Xms 最小堆内存 ,Xmx 最大堆内存 Xmn 新生代容量 PrintGCDetails 打印gc的log日志 SurvivorRatio 新生代当中eden的容量占80%,另外两个survivor分别占10%
以上代码,如果采用的是,引用计数将永远不会被回收掉。
可达性分析
为了解决循环依赖问题,我们采用了可达性分析算法,选取一系列的GC Roots作为起点,开始向下搜索,搜索走的路径被称作引用链,如果一个对象到GC Roots没有任何引用链相连,可以证明这个对象是不可达的。
GC Roots包括以下几种
1、虚拟机栈中引用的对象
2、方法区中静态属性实体引用的对象
3、方法区中常量引用的对象
4、本地方法栈中JNI引用的对象
obj8, obj9, obj10和GCRoots之间没有关联关系,都可以进行垃圾回收
垃圾回收算法
标记-清除算法:标记出所有需要清除的对象,然后进行清理
缺点:碎片化比较严重,大对象的话,找不到可以利用的空间
复制算法:将内存分为相等的两块区域,每次只使用其中的一块,当其中一块使用完了之后,将还存活的对象复制到另一半空间当中,把已经使用过的空间清理掉。这种方式,不用担心碎片化问题,只需要移动堆顶的指针,按顺序分配,实现简单,运行高效。缺点就是,复制大量存活周期长的对象造成性能上的浪费。
标记-整理算法:考虑到以上两种算法的缺点,设计出了标记-整理算法,现将需要回收的对象进行标记,但是不是直接清理,而是将所有存活的对象都移动到一端,直接清理掉边界以外的对象
分代收集算法:将java堆当中的对象分为两大类,老年代和新生代,在新生代当中,每次回收时,发现有大量的对象死去,只有少量的存活,那么就会选择使用复制算法。老年代当中,因为对象存活率高,没有额外的空间对他们进行分配担保,就可以使用标记-清理算法或者是标记-整理算法来进行回收
新生代当中分为Eden区和两个survivor区,我们内次使用的时候,都是使用Eden区和其中的另一个( From)survivor区,当需要gc时,会把Eden区和其中一块(From)survivor区当中存活的对象拷贝到另一个(To)survivor区。当To survivor不足以存储某个对象时,就会把这个对象存放至老年代当中去。在每次GC之后,使用的就是Eden区和From survivor区,如此反复,当对象在survivor区躲过一次之后,年龄就会加1,年龄超过15就会存放到老年代当中去。
典型的垃圾收集器
垃圾收集算法是垃圾收集器的理论基础,而垃圾收集器是垃圾收集算法的具体实现
Serial收集器
最古老的收集器,使用它进行收集时,必须暂停用户线程,只使用一个线程进行回收,针对新生代,使用复制算法,针对老年代使用标记-压缩算法,优点:稳定,高效,缺点:暂停用户线程,如果是客户端服务的话,默认就是Serial收集器。
参数控制:-XX:+UseSerialGC
ParNew收集器
是Serial收集器的多线程版本,对于新生代收集是并行的,对于老年代的收集是串行的,新生代采用复制算法,老年代使用标记-压缩算法
配置参数
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
Parallel Scavenge收集器
parallel scavenge收集器类似于parNew收集器,新生代并行收集,采用复制算法,主要是为了提高吞吐量,通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或者最大的吞吐量。
参数配置:-XX:+UseParallelGC -XX:MaxGCPauseMills 设置垃圾回收时间 -XX:GCTimeRatio 设置吞吐量大小
Parallel Old
parallel old是parallel scavenge 的老年代收集算法,使用多线程和标记-压缩算法
参数配置: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
Serial Old
老年代采用Serial的收集方式,采用标记-整理算法,jdk在client模式下,默认设置成Serial
CMS收集器(Concurrent Mark Sweep)
cms收集器是以最小回收时间停顿为目标的并发收集器,采用标记-清理算法。并发标记的实现会比较复杂,相对于前面的收集器来说,整个过程分为以下4个步骤:
1、初始标记:标记GCRoots能直接关联到的对象,时间比较短 暂停工作线程
2、并发标记:进行GCRoots Tracing过程(可达性分析过程),时间比较长
3、重新标记 :为了修正因为并发标记期间,用户程序继续进行而导致标记产生变动的那部分对象的标记记,暂停工作线程
4、并发清除:回收内存空间,时间比较长
总体来说,CMS收集器的内存收集过程是和用户线程一起并发执行的,所以优点就是并发收集、低停顿。缺点就是:产生大量的空间碎片,并发阶段降低吞吐量。
缺点:
1、在并发阶段,虽然不会暂停用户线程,但是占用一定的CPU资源,会降低系统的吞吐量
2、无法处理浮动垃圾,即在并发清除过程中,正在产生的垃圾,这部分垃圾只能在下次GC时触发
3、由于采用标记-清除算法,会产生大量的碎片,会导致老年代空间很大,但是没有连续的空间,提前导致FullGC
如果在CMS运行期间,预留的内存空间不够用户线程使用,触发Concurrent Mode Failure。这个时候就会启动Serial Old进行老年代的垃圾回收,暂停的时间就更长了。
参数配置:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
G1收集器
G1收集器有以下特点
1、并行与并发,在多核情况下,充分利用cpu,可以缩短stop the world的时间
2、G1收集器采用的是标记-压缩的算法,这样不会在没有大的空间时,就进行一次GC
3、使用者来设定每次GC需要停顿多少毫秒,从而使垃圾回收消耗的时间不会超过设定的毫秒数
4、分代收集,G1收集器不需要其他的收集器的配合就可以对整个堆上面的内容进行收集,能够通过不同的方式就可以处理好新生成对象和熬过多次旧对象的GC。
G1收集的步骤:
1、初始标记,这个阶段是stop the world,并且会触发一次minor gc
2、并发标记,从GCRoots开始进行可达性分析,与用户程序并发执行
3、再标记,会有短暂停顿,再标记阶段是用来收集并发标记阶段产生的新的垃圾,G1当中采用了比CMS更快的初始快照算法
4、清除回收,将回收区域的存活对象拷贝到新区域,并发清空回收区域并把它返回到空闲区域链表中。
参考: