前言
Java相较于其余编程语言更加容易学习,这其中很大一部分起因要归功于JVM的主动内存管理机制。 对于从事C语言的开发者来说,他们领有每一个对象的「所有权」,更大的势力也意味着更多的职责,C开发者须要保护每一个对象「从生到死」的过程,当对象废除不必时必须手动开释其内存,否则就会产生内存透露。而对于Java开发者来说,JVM的主动内存管理机制解决了这个让人头疼的问题,不容易呈现内存透露和内存溢出的问题了,GC让开发者更加专一于程序自身,而不必去关怀内存何时调配、何时回收、以及如何回收。
- JVM运行时数据区
=============
在聊GC前,有必要先理解一下JVM的内存模型,晓得JVM是如何布局内存的,以及GC的次要作用区域。 如图所示,JVM运行时会将内存划分为五大块区域,其中「办法区」和「堆」随着JVM的启动而创立,是所有线程共享的内存区域。虚拟机栈、本地办法栈、程序计数器则是随着线程的创立被创立,线程运行完结后也就被销毁了。
1.1 程序计数器
程序计数器(Program Counter Register)是一块十分小的内存空间,简直能够忽略不计。 它能够看作是线程所执行字节码的行号指数器,指向以后线程下一条应该执行的指令。对于:条件分支、循环、跳转、异样等根底性能都依赖于程序计数器。
对于CPU的一个外围来说,任意时刻只能跑一个线程。如果线程的CPU工夫片用完就会被挂起,期待OS重新分配工夫片再继续执行,那线程如何晓得上次执行到哪里了呢?就是通过程序计数器来实现的,每个线程都须要保护一个公有的程序计数器。
如果线程在执行Java办法,计数器记录的是JVM字节码指令地址。如果执行的是Native办法,计数器值则为Undefined
。
程序计数器是惟一一个没有规定任何OutOfMemoryError状况的内存区域,意味着在该区域不可能产生OOM异样,GC不会对该区域进行回收!
1.2 虚拟机栈
虚拟机栈(Java Virtual Machine Stacks)也是线程公有的,生命周期和线程雷同。
虚拟机栈形容的是Java办法执行的内存模型,JVM要执行一个办法时,首先会创立一个栈帧(Stack Frame)用于寄存:局部变量表、操作数栈、动静链接、办法进口等信息。栈帧创立结束后开始入栈执行,办法执行完结后即出栈。
办法执行的过程就是一个个栈帧从入栈到出栈的过程。
局部变量表次要用来寄存编译器可知的各种根本数据类型、对象援用、returnAddress类型。局部变量表所需的内存空间在编译时就曾经确认,运行期间不会批改局部变量表的大小。
在JVM标准中,虚拟机栈规定了两种异样:
- StackOverflowError
线程申请的栈深度大于JVM所容许的栈深度。 栈的容量是无限的,如果线程入栈的栈帧超过了限度就会抛出StackOverflowError异样,例如:办法递归。
- OutOfMemoryError
虚拟机栈是能够动静扩大的,如果扩大时无奈申请到足够的内存,则会抛出OOM异样。
1.3. 本地办法栈
本地办法栈(Native Method Stack)也是线程公有的,与虚拟机栈的作用十分相似。 区别是虚拟机栈是为执行Java办法服务的,而本地办法栈是为执行Native办法服务的。
与虚拟机栈一样,JVM标准中对本地办法栈也规定了StackOverflowError和OutOfMemoryError两种异样。
1.4. Java堆
Java堆(Java Heap)是线程共享的,一般来说也是JVM治理最大的一块内存区域,同时也是垃圾收集器GC的次要治理区域。
Java堆在JVM启动时创立,作用是:寄存对象实例。 简直所有的对象都在堆中创立,然而随着JIT编译器的倒退和逃逸剖析技术逐步成熟,栈上调配、标量替换优化技术使得“所有对象都调配在堆上”不那么相对了。
因为是GC次要治理的区域,所以也被称为:GC堆。 为了GC的高效回收,Java堆外部又做了如下划分:
JVM标准中,堆在物理上能够是不间断的,只有逻辑上间断即可。通过-Xms -Xmx
参数能够设置最小、最大堆内存。
1.5. 办法区
办法区(Method Area)与Java堆一样,也是线程共享的一块内存区域。 它次要用来存储:被JVM加载的类信息,常量,动态变量,即时编译器产生的代码等数据。 也被称为:非堆(Non-Heap),目标是与Java堆辨别开来。
JVM标准对办法区的限度比拟宽松,JVM甚至能够不对办法区进行垃圾回收。这就导致在老版本的JDK中,办法区也别称为:永恒代(PermGen)。
应用永恒代来实现办法区不是个好主见,容易导致内存溢出,于是从JDK7开始有了“去永恒代”口头,将本来放在永恒代中的字符串常量池移出。到JDK8中,正式去除永恒代,迎来元空间。
- GC概述
========
垃圾收集(Garbage Collection)简称为「GC」,它的历史远比Java语言自身长远,在1960年诞生于麻省理工学院的Lisp是第一门开始应用内存动态分配和垃圾收集技术的语言。
要想实现主动垃圾回收,首先须要思考三件事件: 后面介绍了JVM的五大内存区域,程序计数器占用内存极少,简直能够忽略不计,而且永远不会内存溢出,GC不须要对其进行回收。虚拟机栈、本地办法栈随线程“同生共死”,栈中的栈帧随着办法的运行井井有条的入栈、出栈,每个栈帧调配多少内存在编译期就曾经根本确定,因而这两块区域内存的调配和回收都具备确定性,不太须要思考如何回收的问题。
办法区就不一样了,一个接口到底有多少个实现类?每个类占用的内存是多少?你甚至能够在运行时动静的创立类,因而GC须要针对办法区进行回收。
Java堆也是如此,堆中寄存着简直所有的Java对象实例,一个类到底会创立多少个对象实例,只有在程序运行时才晓得,这部分内存的调配和回收是动静的,GC须要重点关注。
2.1 哪些对象须要回收
实现主动垃圾回收的第一步,就是判断到底哪些对象是能够被回收的。一般来说有两种形式:援用计数算法和可达性剖析算法,商用JVM简直采纳的都是后者。
2.1.1 援用计数算法
在对象中增加一个援用计数器,每援用一次计数器就加1,每勾销一次援用计数器就减1,当计数器为0时示意对象不再被援用,此时就能够将对象回收了。
援用计数算法(Reference Counting)尽管占用了一些额定的内存空间,然而它原理简略,也很高效,在大多数状况下是一个不错的实现计划,然而它存在一个重大的弊病:无奈解决循环援用。
例如一个链表,按理只有没有援用指向链表,链表就应该被回收,然而很遗憾,因为链表中所有的元素援用计数器都不为0,因而无奈被回收,造成内存透露。
2.1.2 可达性剖析算法
目前支流的商用JVM都是通过可达性剖析来判断对象是否能够被回收的。 这个算法的基本思路是:
通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过援用关系向下搜查,搜查走过的门路称为「援用链」,如果某个对象到GC Roots没有任何援用链相连,就阐明该对象不可达,即能够被回收。
对象可达指的就是:单方存在间接或间接的援用关系。 根可达或GC Roots可达就是指:对象到GC Roots存在间接或间接的援用关系。
能够作为GC Roots的对象有以下几类: 可达性剖析就是JVM首先枚举根节点,找到一些为了保障程序能失常运行所必须要存活的对象,而后以这些对象为根,依据援用关系开始向下搜查,存在间接或间接援用链的对象就存活,不存在援用链的对象就回收。
对于可达性剖析的详细描述,能够看笔者的文章:《大白话了解可达性剖析算法》。
2.2 何时回收
JVM将内存划分为五大块区域,不同的GC会针对不同的区域进行垃圾回收,GC类型个别有以下几大类:
- Minor GC
也被称为“Young GC”、“轻GC”,只针对新生代进行的垃圾回收。
- Major GC
也被称为“Old GC”,只针对老年代进行的垃圾回收。
- Mixed GC
混合GC,针对新生代和局部老年代进行垃圾回收,局部垃圾收集器才反对。
- Full GC
整堆GC、重GC,针对整个Java堆和办法区进行的垃圾回收,耗时最久的GC。
什么时候触发GC,以及触发什么类型的GC呢?不同的垃圾收集器实现不一样,你还能够通过设置参数来影响JVM的决策。
一般来说,新生代会在Eden
区用尽后才会触发GC,而Old
区却不能这样,因为有的并发收集器在清理过程中,用户线程能够持续运行,这意味着程序依然在创建对象、分配内存,这就须要老年代进行「空间调配担保」,新生代放不下的对象会被放入老年代,如果老年代的回收速度比对象的创立速度慢,就会导致「调配担保失败」,这时JVM不得不触发Full GC,以此来获取更多的可用内存。
2.3 如何回收
定位到须要回收的对象当前,就要开始进行回收了。如何回收对象又成了一个问题。 什么样的回收形式会更加的高效呢?回收后是否须要对内存进行压缩整顿,防止碎片化呢?针对这些问题,GC的回收算法大抵分为以下三类:
- 标记-革除算法
- 标记-复制算法
- 标记-整顿算法
具体算法的回收细节,上面会介绍到。
- GC回收算法
==========
JVM将堆划分成不同的代,不同的代中寄存的对象特点不一样,针对不同的代应用不同的GC回收算法进行回收能够晋升GC的效率。
3.1 分代收集实践
目前大多数JVM的垃圾收集器都遵循“分代收集”实践,分代收集实践建设在三个假说之上。
3.1.1 弱分代假说
绝大多数对象都是朝生夕死的。
想想看咱们写的程序是不是这样,绝大多数时候,咱们创立一个对象,只是为了进行一些业务计算,失去计算结果后这个对象也就没什么用了,即能够被回收了。 再例如:客户端要求返回一个列表数据,服务端从数据库查问后转换成JSON响应给前端后,这个列表的数据就能够被回收了。 诸如此类,都能够被称为「朝生夕死」的对象。
3.1.2 强分代假说
熬过越屡次GC的对象就越难以回收。
这个假说齐全是基于概率学统计来的,经验过屡次GC都无奈被回收的对象,能够假设它下次GC时依然无奈被回收,因而就没必要高频率的对其进行回收,将其挪到老年代,缩小回收的频率,让GC去回收效益更高的新生代。
3.1.3 跨代援用假说
跨代援用绝对于同代援用是极少的。
这是依据前两条假说逻辑推理得出的隐含推论:存在相互援用关系的两个对象,应该偏向于同时生存或者同时沦亡的。 举个例子,如果某个新生代对象存在跨代援用,因为老年代对象难以沦亡,该援用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后降职到老年代中,这时跨代援用也随即被打消了。
3.2 解决跨代援用
跨代援用尽管极少,然而它还是可能存在的。如果为了极少的跨代援用而去扫描整个老年代,那每次GC的开销就太大了,GC的暂停工夫会变得难以承受。如果疏忽跨代援用,会导致新生代的对象被谬误的回收,导致程序谬误。
3.2.1 Remembered Set
JVM是通过记忆集(Remembered Set)来解决的,通过在新生代建设记忆集的数据结构,来防止回收新生代时把整个老年代也加进GC Roots的扫描范畴,缩小GC的开销。
记忆集是一种由「非收集区域」指向「收集区域」的指针汇合的形象数据结构,说白了就是把「年老代中被老年代援用的对象」给标记起来。记忆集能够有以下三种记录精度:
- 字长精度:记录准确到一个机器字长,也就是处理器的寻址位数。
- 对象精度:准确到对象,对象的字段是否存在跨代援用指针。
- 卡精度:准确到一块内存区域,该区域内的对象是否存在跨代援用。
字长精度和对象精度太精细化了,须要破费大量的内存来保护记忆集,因而许多JVM都是采纳的「卡精度」,也被称作:“卡表”(Card Table)。卡表是记忆集的一种实现,也是目前最罕用的一种模式,它定义了记忆集的记录精度、与对内存的映射关系等。
HotSpot应用一个字节数组来实现卡表,它将堆空间划分成一系列2次幂大小的内存区域,这个内存区域就被称作「卡页」(Card Page),卡页的大小个别都是2的幂次方数,HotSpot采纳2的9次幂,即512字节。字节数组的每一个元素都对应着一个卡页,如果某个卡页内的对象存在跨代援用,JVM就会将这个卡页标记为「Dirty」脏的,GC时只须要扫描脏页对应的内存区域即可,防止扫描整个堆。
卡表的构造如下图所示:
3.2.2 写屏障
卡表只是用来标记哪一块内存区域存在跨代援用的数据结构,JVM如何来保护卡表呢?什么时候将卡页变脏呢?
HotSpot是通过「写屏障」(Write Barrier)来保护卡表的,JVM拦挡了「对象属性赋值」这个动作,相似于AOP的切面编程,JVM能够在对象属性赋值前后染指解决,赋值前的解决叫作「写前屏障」,赋值后的解决叫作「写后屏障」,伪代码如下:
void setField(Object o){ before();//写前屏障 this.field = o; after();//写后屏障}复制代码
开启写屏障后,JVM会为所有的赋值操作生成相应的指令,一旦呈现老年代对象的援用指向了年老代的对象,HotSpot就会将对应的卡表元素置为脏的。
请将这里的「写屏障」和并发编程中内存指令重排序的「写屏障」辨别开,防止混同。
除了写屏障自身的开销外,卡表在高并发场景下还面临着「伪共享」的问题,古代CPU的缓存零碎是以「缓存行」(Cache Line)为单位存储的,Intel的CPU缓存行的大小个别是64字节,多线程批改相互独立的变量时,如果这些变量在同一个缓存行中,就会导致彼此的缓存行无端生效,线程不得不频繁发动load指令从新加载数据,而导致性能升高。
一个Cache Line是64字节,每个卡页是512字节,64✖️512字节就是32KB,如果不同的线程更新的对象处在这32KB之内,就会导致更新卡表时正好写入同一个缓存行而影响性能。为了防止这个问题,HotSpot反对只有当元素未被标记时,才将其置为脏的,这样会减少一次判断,然而能够防止伪共享的问题,设置-XX:+UseCondCardMark
来开启这个判断。
3.3 标记革除
标记革除算法分为两个过程:标记、革除。
收集器首先标记须要被回收的对象,标记实现后对立革除。也能够标记存活对象,而后对立革除没有被标记的对象,这取决于内存中存活对象和死亡对象的占比。
毛病:
- 执行效率不稳固
标记和革除的工夫耗费随着Java堆中的对象一直减少而减少。 2. 内存碎片 标记革除后内存会产生大量不间断的空间碎片,不利于后续持续为新生对象分配内存。
3.4 标记复制
为了解决标记革除算法产生的内存碎片问题,标记复制算法进行了改良。
标记复制算法会将内存划分为两块区域,每次只应用其中一块,垃圾回收时首先进行标记,标记实现后将存活的对象复制到另一块区域,而后将以后区域全副清理。
毛病是:如果大量对象无奈被回收,会产生大量的内存复制开销。可用内存放大为一半,内存节约也比拟大。 因为绝大多数对象都会在第一次GC时被回收,须要被复制的往往是极少数对象,那么就齐全没必要依照1:1去划分空间。 HotSpot虚拟机默认Eden区和Survivor区的大小比例是8:1,即Eden区80%,From Survivor区10%,To Survivor区10%,整个新生代可用内存为Eden区+一个Survivor区即90%,另一个Survivor区10%用于分区复制。
如果Minor GC后仍存活大量对象,超出了一个Survivor区的范畴,那么就会进行调配担保(Handle Promotion),将对象间接调配进老年代。
3.5 标记整顿
标记复制算法除了在对象大量存活时须要进行较多的复制操作外,还须要额定的内存空间老年代来进行调配担保,所以在老年代中个别不采纳这种回收算法。
可能在老年代中存活的对象,个别都是历经屡次GC后仍无奈被回收的对象,基于“强分代假说”,老年代中的对象个别很难被回收。针对老年代对象的生存特色,引入了标记整顿算法。
标记整顿算法的标记过程与标记革除算法统一,然而标记整顿算法不会像标记革除算法一样间接清理标记的对象,而是将存活的对象都向内存区域的一端挪动,而后间接清理掉边界外的内存空间。 标记整顿算法相较于标记革除算法,最大的区别是:须要挪动存活的对象。 GC时挪动存活的对象既有长处,也有毛病。
毛病 基于“强分代假说”,大部分状况下老年代GC后会存活大量对象,挪动这些对象须要更新所有reference援用地址,这是一项开销极大的操作,而且该操作须要暂停所有用户线程,即程序此时会阻塞进展,JVM称这种进展为:Stop The World(STW)。
长处 挪动对象对内存空间进行整顿后,不会产生大量不间断的内存碎片,利于后续为对象分配内存。
由此可见,不论是否挪动对象都有利弊。挪动则内存回收时负责、内存调配时简略,不挪动则内存回收时简略、内存调配时简单。从整个程序的吞吐量来思考,挪动对象显然更划算一些,因为内存调配的频率比内存回收的频率要高的多的多。
还有一种解决形式是:平时不挪动对象,采纳标记革除算法,当内存碎片影响到大对象调配时,才启用标记整顿算法。
- 垃圾收集器
=========
依照《Java虚拟机标准》实现的JVM就举不胜举,且每个JVM平台都有N个垃圾收集器供用户抉择,这些不是一篇文章能够说的分明的。当然,开发者也没必要理解所有的垃圾收集器,以Hotspot JVM为例,支流的垃圾收集器次要有以下几大类: 串行:单线程收集,用户线程暂停。 并行:多线程收集,用户线程暂停。 并发:用户线程和GC线程同时运行。
后面曾经说过,大多数JVM的垃圾收集器都遵循“分代收集”实践,不同的垃圾收集器回收的内存区域会有所不同,大多数状况下,JVM须要两个垃圾收集器配合应用,下图有虚线连贯的代表两个收集器能够配合应用。
4.1 新生代收集器
4.1.1 Serial
最根底,最早的垃圾收集器,采纳标记复制算法,仅开启一个线程实现垃圾回收,回收时会暂停所有用户线程(STW)。 应用-XX:+UseSerialGC
参数开启Serial收集器,因为是单线程回收,因而Serial的利用范畴很受限制:
- 应用程序很轻量,堆空间不到百MB。
- 服务器CPU资源缓和。
4.1.2 Parallel Scavenge
应用标记复制算法,多线程的新生代收集器。 应用参数-XX:+UseParallelGC
开启,ParallelGC的特点是十分关注零碎的吞吐量,它提供了两个参数来由用户控制系统的吞吐量: -XX:MaxGCPauseMillis:设置垃圾回收最大的进展工夫,它必须是一个大于0的整数,ParallelGC会朝着这个指标去致力,如果这个值设置的过小,ParallelGC就不肯定能保障了。如果用户心愿GC进展的工夫很短,ParallelGC就会尝试减小堆空间,因为回收一个较小的堆必定比回收一个较大的堆耗时短嘛,然而这样会更频繁的触发GC,从而升高零碎的吞吐量。
-XX:GCTimeRatio:设置吞吐量的大小,它的值是一个0~100的整数。假如GCTimeRatio为n,那么ParallelGC将破费不超过1/(1+n)
的工夫进行垃圾回收,默认值为19,意味着ParallelGC用于垃圾回收的工夫不会超过5%。
ParallelGC是JDK8的默认垃圾收集器,它是一款吞吐量优先的垃圾收集器,用户能够通过-XX:MaxGCPauseMillis
和-XX:GCTimeRatio
来设置GC最大的进展工夫和吞吐量。但这两个参数是互相矛盾的,更小的进展工夫就意味着GC须要更频繁进行回收,从而减少GC回收的整体工夫,导致吞吐量降落。
4.1.3 ParNew
ParNew也是一个应用标记复制算法,多线程的新生代垃圾收集器。它的回收策略、算法、及参数都和Serial一样,只是简略的将单线程改为多线程而已,它的诞生只是为了配合CMS
收集器应用而存在的。CMS
是老年代的收集器,然而Parallel Scavenge
不能配合CMS
一起工作,Serial是串行回收的,效率又太低了,因而ParNew就诞生了。
应用参数-XX:+UseParNewGC
开启,不过这个参数曾经在JDK9之后的版本中删除了,因为JDK9默认G1收集器,CMS曾经被取代,而ParNew就是为了配合CMS而诞生的,CMS废除了,ParNew也就没有存在价值了。
4.2 老年代收集器
4.2.1 Serial Old
应用标记整顿算法,和Serial一样,单线程独占式的针对老年代的垃圾收集器。老年代的空间通常比新生代要大,而且标记整顿算法在回收过程中须要挪动对象来防止内存碎片化,因而老年代的回收要比新生代更耗时一些。
Serial Old作为最早的老年代垃圾收集器,还有一个劣势,就是它能够和绝大多数新生代垃圾收集器配合应用,同时它还能够作为CMS并发失败的备用收集器。
应用参数-XX:+UseSerialGC
开启,新生代老年代都将应用串行收集器。和Serial一样,除非你的利用十分轻量,或者CPU的资源非常缓和,否则都不倡议应用该收集器。
4.2.2 Parallel Old
ParallelOldGC是一款针对老年代,多线程并行的独占式垃圾收集器,和Parallel Scavenge一样,属于吞吐量优先的收集器,Parallel Old的诞生就是为了配合Parallel Scavenge应用的。
ParallelOldGC应用的是标记整顿算法,应用参数-XX:+UseParallelOldGC
开启,参数-XX:ParallelGCThreads=n
能够设置垃圾收集时开启的线程数量,同时它也是JDK8默认的老年代收集器。
4.2.3 CMS
CMS(Concurrent Mark Sweep)是一款里程碑式的垃圾收集器,为什么这么说呢?因为在它之前,GC线程和用户线程是无奈同时工作的,即便是Parallel Scavenge,也不过是GC时开启多个线程并行回收而已,GC的整个过程仍然要暂停用户线程,即Stop The World。这带来的结果就是Java程序运行一段时间就会卡顿一会,升高利用的响应速度,这对于运行在服务端的程序是不能被接管的。
GC时为什么要暂停用户线程? 首先,如果不暂停用户线程,就意味着期间会一直有垃圾产生,永远也清理不洁净。 其次,用户线程的运行必然会导致对象的援用关系产生扭转,这就会导致两种状况:漏标和错标。
- 漏标
本来不是垃圾,然而GC的过程中,用户线程将其援用关系批改,导致GC Roots不可达,成为了垃圾。这种状况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。 2. 错标 本来是垃圾,然而GC的过程中,用户线程将援用从新指向了它,这时如果GC一旦将其回收,将会导致程序运行谬误。
为了实现并发收集,CMS的实现比后面介绍的几种垃圾收集器都要简单的多,整个GC过程能够大略分为以下四个阶段: 1、初始标记 初始标记仅仅只是标记一下GC Roots能间接关联到的对象,速度很快。初始标记的过程是须要触发STW的,不过这个过程十分快,而且初试标记的耗时不会因为堆空间的变大而变慢,是可控的,因而能够疏忽这个过程导致的短暂进展。
2、并发标记 并发标记就是将初始标记的对象进行深度遍历,以这些对象为根,遍历整个对象图,这个过程耗时较长,而且标记的工夫会随着堆空间的变大而变长。不过好在这个过程是不会触发STW的,用户线程依然能够工作,程序仍然能够响应,只是程序的性能会受到一点影响。因为GC线程会占用肯定的CPU和系统资源,对处理器比拟敏感。CMS默认开启的GC线程数是:(CPU外围数+3)/4,当CPU外围数超过4个时,GC线程会占用不到25%的CPU资源,如果CPU数有余4个,GC线程对程序的影响就会十分大,导致程序的性能大幅升高。
3、从新标记 因为并发标记时,用户线程仍在运行,这意味着并发标记期间,用户线程有可能扭转了对象间的援用关系,可能会产生两种状况:一种是本来不能被回收的对象,当初能够被回收了,另一种是本来能够被回收的对象,当初不能被回收了。针对这两种状况,CMS须要暂停用户线程,进行一次从新标记。
4、并发清理 从新标记实现后,就能够并发清理了。这个过程耗时也比拟长,且清理的开销会随着堆空间的变大而变大。不过好在这个过程也是不须要STW的,用户线程仍然能够失常运行,程序不会卡顿,不过和并发标记一样,清理时GC线程仍然要占用肯定的CPU和系统资源,会导致程序的性能升高。
CMS开拓了并发收集的先河,让用户线程和GC线程同时工作成为了可能,然而毛病也很显著: 1、对处理器敏感 并发标记、并发清理阶段,尽管CMS不会触发STW,然而标记和清理须要GC线程染指解决,GC线程会占用肯定的CPU资源,进而导致程序的性能降落,程序响应速度变慢。CPU外围数多的话还略微好一点,CPU资源缓和的状况下,GC线程对程序的性能影响十分大。
2、浮动垃圾 并发清理阶段,因为用户线程仍在运行,在此期间用户线程制作的垃圾就被称为“浮动垃圾”,浮动垃圾本次GC无奈清理,只能留到下次GC时再清理。
3、并发失败 因为浮动垃圾的存在,因而CMS必须预留一部分空间来装载这些新产生的垃圾。CMS不能像Serial Old收集器那样,等到Old区填满了再来清理。在JDK5时,CMS会在老年代应用了68%的空间时激活,预留了32%的空间来装载浮动垃圾,这是一个比拟偏激进的配置。如果理论援用中,老年代增长的不是太快,能够通过-XX:CMSInitiatingOccupancyFraction
参数适当调高这个值。到了JDK6,触发的阈值就被晋升至92%,只预留了8%的空间来装载浮动垃圾。 如果CMS预留的内存无奈包容浮动垃圾,那么就会导致「并发失败」,这时JVM不得不触发准备计划,启用Serial Old收集器来回收Old区,这时进展工夫就变得更长了。
4、内存碎片 因为CMS采纳的是「标记革除」算法,这就象征这清理实现后会在堆中产生大量的内存碎片。内存碎片过多会带来很多麻烦,其一就是很难为大对象分配内存。导致的结果就是:堆空间明明还有很多,但就是找不到一块间断的内存区域为大对象分配内存,而不得不触发一次Full GC,这样GC的进展工夫又会变得更长。 针对这种状况,CMS提供了一种备选计划,通过-XX:CMSFullGCsBeforeCompaction
参数设置,当CMS因为内存碎片导致触发了N次Full GC后,下次进入Full GC前先整顿内存碎片,不过这个参数在JDK9被弃用了。
4.2.3.1 三色标记算法
介绍完CMS垃圾收集器后,咱们有必要理解一下,为什么CMS的GC线程能够和用户线程一起工作。
JVM判断对象是否能够被回收,绝大多数采纳的都是「可达性剖析」算法,对于这个算法,能够查看笔者以前的文章:大白话了解可达性剖析算法。
从GC Roots开始遍历,可达的就是存活,不可达的就回收。
CMS将对象标记为三种色彩: 标记的过程大抵如下:
- 刚开始,所有的对象都是红色,没有被拜访。
- 将GC Roots间接关联的对象置为灰色。
- 遍历灰色对象的所有援用,灰色对象自身置为彩色,援用置为灰色。
- 反复步骤3,直到没有灰色对象为止。
- 完结时,彩色对象存活,红色对象回收。
这个过程正确执行的前提是没有其余线程扭转对象间的援用关系,然而,并发标记的过程中,用户线程仍在运行,因而就会产生漏标和错标的状况。
漏标 假如GC曾经在遍历对象B了,而此时用户线程执行了A.B=null
的操作,切断了A到B的援用。 原本执行了A.B=null
之后,B、D、E都能够被回收了,然而因为B曾经变为灰色,它仍会被当做存活对象,持续遍历上来。 最终的后果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算是浮动垃圾的一部分。
实际上,这个问题仍然能够通过「写屏障」来解决,只有在A写B的时候退出写屏障,记录下B被切断的记录,从新标记时能够再把他们标为红色即可。
错标 假如GC线程曾经遍历到B了,此时用户线程执行了以下操作:
B.D=null;//B到D的援用被切断A.xx=D;//A到D的援用被建设复制代码
B到D的援用被切断,且A到D的援用被建设。 此时GC线程持续工作,因为B不再援用D了,只管A又援用了D,然而因为A曾经标记为彩色,GC不会再遍历A了,所以D会被标记为红色,最初被当做垃圾回收。 能够看到错标的后果比漏表重大的多,浮动垃圾能够下次GC清理,而把不该回收的对象回收掉,将会造成程序运行谬误。
错标只有在满足上面两种状况下才会产生:
- 灰色指向红色的援用全副断开。
- 彩色指向红色的援用被建设。
只有突破任一条件,就能够解决错标的问题。
原始快照和增量更新 原始快照突破的是第一个条件:当灰色对象指向红色对象的援用被断开时,就将这条援用关系记录下来。当扫描完结后,再以这些灰色对象为根,从新扫描一次。相当于无论援用关系是否删除,都会依照刚开始扫描时那一瞬间的对象图快照来扫描。
增量更新突破的是第二个条件:当彩色指向红色的援用被建设时,就将这个新的援用关系记录下来,等扫描完结后,再以这些记录中的彩色对象为根,从新扫描一次。相当于彩色对象一旦建设了指向红色对象的援用,就会变为灰色对象。
CMS采纳的计划就是:写屏障+增量更新来实现的,突破的是第二个条件。
当彩色指向红色的援用被建设时,通过写屏障来记录援用关系,等扫描完结后,再以援用关系里的彩色对象为根从新扫描一次即可。
伪代码大抵如下:
class A{ private D d; public void setD(D d) { writeBarrier(d);// 插入一条写屏障 this.d = d; } private void writeBarrier(D d){ // 将A -> D的援用关系记录下来,后续从新扫描 }}复制代码
4.3 混合收集器
4.3.1 G1
G1的全称是「Garbage First」垃圾优先的收集器,JDK7正式应用,JDK9默认应用,它的呈现是为了代替CMS收集器。
既然要代替CMS,那么毫无疑问,G1也是并发并行的垃圾收集器,用户线程和GC线程能够同时工作,关注的也是利用的响应工夫。
G1最大的一个变动就是,它只是逻辑分代,物理构造上曾经不分代了。它将整个Java堆划分成多个大小不等的Region,每个Region能够依据须要表演Eden区、Survivor区、或者是老年代空间,G1能够对表演不同角色的Region采纳不同的策略去解决。
G1之前的所有垃圾收集器,回收的范畴要么是整个新生代(Minor GC)、要么是整个老年代(Major GC)、再就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它能够面向堆内任何局部来组成回收集(Collection Set,简称CSet
)进行回收,衡量标准不再是它属于哪个分代,而是判断哪个Region垃圾最多,抉择回收价值最高的Region回收,这也是「Garbage First」名称的由来。
尽管G1依然保留了分代的概念,然而新生代和老年代不再是固定不变的两块间断的内存区域了,它们都是由一系列Region组成的,而且每次GC时,新生代和老年代的空间大小会动静调整。G1之所以能管制GC的进展工夫,建设可预测的进展工夫模型,就是因为它将Region作为单次回收的最小单元,每次回收的内存空间都是Region大小的整数倍,这样就能够防止在整个Java堆内进行全区域的垃圾收集。
G1会跟踪每个Region的垃圾数量,计算每个Region的回收价值,在后盾保护一个优先级列表,而后依据用户设置的容许GC进展的工夫来优先回收“垃圾最多”的Region,这样就保障了G1可能在无限的工夫内回收尽可能多的可用内存。
G1的整个回收周期大略能够分为以下几个阶段:
- Eden区内存耗尽,触发新生代GC开始回收Eden区和Survivor区。新生代GC后,Eden区会被清空,Survivor区至多会保留一个,其余的对象要么被清理,要么被降职到老年代。这个过程中,新生代的大小可能会被调整。
- 并发标记周期 2.1 初始标记:仅标记GC Roots间接关联的对象,会随同一次新生代GC,且会导致STW。 2.2 根区域扫描:初始标记时触发的新生代GC会将Eden区清空,存活对象会挪动到Survivor区,这时就须要扫描由Survivor区间接可达的老年代区域,并标记这些对象,这个过程能够并发执行。 2.3 并发标记:和CMS相似会扫描并查找整个堆内存活的对象并标记,不会触发STW。 2.4 从新标记:触发STW,修改并发标记期间因为用户线程继续执行而导致对象间的援用被扭转。 2.5 独占清理:触发STW,计算各个Region的回收价值,对Region进行排序,辨认可供混合回收的区域。 2.6 并发清理:辨认并清理齐全闲暇的Region,不会造成进展。
- 混合回收:并发标记周期中的并发清理阶段,G1尽管也回收了局部空间,然而比例还是相当低的。然而在这之后,G1曾经明确晓得各个Region的回收价值了。在混合回收阶段G1会优先回收垃圾最多的Region,这些Region既蕴含了新生代,也蕴含了老年代,故称之为“混合回收”。被清理的Region内的存活对象会被挪动到其余Region,这也防止了内存碎片。
和CMS一样,因为并发回收时用户线程依然在运行,即分配内存,因而如果回收速度跟不上内存调配的速度,G1也会在必要的时候触发一个Full GC来获取更多的可用内存。
应用参数-XX:+UseG1GC
来开启G1收集器,-XX:MaxGCPauseMillis
来设置指标最大进展工夫,G1会朝着这个指标去致力,如果GC进展工夫超过了指标工夫,G1就会尝试调整新生代和老年代的比例、堆大小、降职年龄等一系列参数来希图达到预设指标。 -XX:ParallelGCThreads
用来设置并行回收时GC的线程数量,-XX:InitiatingHeapOccupancyPercent
用来指定整个Java堆的使用率达到多少时触发并发标记周期的执行,默认值是45。
4.3.2 面向未来的ZGC
ZGC是在JDK11才退出的具备实现性质的低提早垃圾收集器,它的指标是心愿在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都能够把GC的进展工夫管制在十毫秒以内。
ZGC面向的是超大堆,最大反对4TB
的堆空间,它和G1一样,也是采纳Region的内存布局模式。
ZGC最大的一个特点就是它采纳着色指针Colored Pointer
技术来标记对象。以往,如果JVM须要在对象上存储一些额定的、只供GC或JVM自身应用的数据时(如GC年龄、偏差线程ID、哈希码),通常会在对象的对象头上减少额定的字段来记录。ZGC就厉害了,间接把标记信息记录在对象的援用指针上。
Colored Pointer
是什么?为什么对象援用的指针自身也能够存储数据呢? 在64位零碎中,实践上能够拜访的内存大小为2的64次幂字节,即16EB。然而实际上,目前远远用不到这么大的内存,因而基于性能和老本的思考,CPU和操作系统都会施加本人的束缚。例如AMD64架构只反对54位(4PB)的地址总线,Linux只反对46位(64TB)的物理地址总线,Windows只反对44位(16TB)的物理地址总线。
在Linux零碎下,高18位不能用来寻址,残余的46位能反对最大64TB的内存大小。事实上,64TB的内存大小在目前来说也远远超出了服务器的须要。于是ZGC就盯上了这剩下的46位指针宽度,将其高4位提取进去存储四个标记信息。通过这些标记位,JVM能够间接从指针中看到其援用对象的三色标记状态、是否进入了重调配集(即被挪动过)、是否只能通过finalize()办法能力被拜访到。这就导致JVM能利用的物理地址总线只剩下42位了,即ZGC能治理的最大内存空间为2的42次幂字节,即4TB。 目前ZGC还处于试验阶段,能查到的材料也不多,笔者当前再整顿更新吧。
- 读懂GC日志
==========
待写......
- GC的调优
=========
待写......
参考:《2020最新Java根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/692408...