C、C++等语言中,内存的分配和释放由程序代码来完成,容易出现因为程序员漏写内存释放代码引发的内存泄露,最终致使系统内存耗尽。
Java代码运行在JVM中,由JVM来管理 堆Heap 内存的分配和回收(Garbage Collection),把程序员从繁琐的内存管理工做中释放出来,更专一于业务开发。Java内存回收工做由标记(识别可回收对象)和回收(释放可回收对象)两个步骤组成。
和程序代码释放内存相比,内存自动管理会占用一部分CPU时间,Stop The World特色回暂停业务程序运行,很是影响执行效率。Java各版本中,一直致力于内存管理算法的优化,造成了一套针对各类内存分区(新生代、老年代)和运行场景(单核、多核、客户端、服务端)的特色而针对性设计的内存回收算法。程序员
这里说的内存回收机制,主要是指针对 堆Heap和 元空间Metaspace内存的回收,线程相关内存(栈、本地栈、程序计数器)内存随线程建立和回收,直接内存的释放由其在堆内存中引用释放时触发。
在内存被回收前,系统必须标记哪些内存已经没有人使用能够释放,这个工做就由内存标记算法的来完成,在Java各版本中,使用过以下几种标记算法。算法
这是早期的内存标记算法,每一个堆中分配的对象都有一个引用计数器,计数一个对象被引用的次数。当对象建立并赋值给变量时,计数为1,当有其余变量引用该对象时,引用计数+1;但引用此对象的变量超出存活范围或释放对对象引用(包括变量引用了其余对象或变量被设置为null等),引用计数-1。对象的引用计数为0时,表示此对象可被垃圾收集器回收。
引用计数法的有点是简单,执行速度快,只要变量一遍对象检测引用计数是否为0便可判断是否可回收;肯定是没法检测出循环引用而致使内存没法回收。多线程
采用跟搜索算法,搜索算法引入了图论,把全部对象间的关系当作一张图,内存标记从一组根节点(GC Root Set)开始,经过递归搜索,创建对象的引用关系图,当搜索完毕后,图外的对象就是可回收对象。这是目前Java中使用的内存标记算法。并发
可做为GC Root的对象包括:框架
采用跟踪算法标记内存对象后,再扫描堆内存中未被标记的对象,进行回收。此算法不移动对象,仅对不存活对象进行回收,在存活对象占比高的状况下处理效率高,但不移动对象会引发内存碎片。性能
此方法和标记-清理算法使用相同标记算法,但在对不存活对象回收时,会把存活对象向内存前部空闲区域移动,同时更新对象的指针。此方法在清理的基础上,会对对象进行移动,执行成本较高,但可解决内存碎片问题。基于此算法的内存回收实现,通常会增长句柄和句柄表。优化
该算法把内存分为空闲区和对象区,新建对象存储到对象区中。当对象区满时,先采用跟踪算法对对象进行标记,再把存活对象拷贝到空闲区,清空原对象区,空闲区和对象区互换角色。在拷贝过车中,程序须要暂停,此算法适用于存活对象叫少的状况,能够解决内存碎片问题。spa
JDK8中,堆中移除了永生代区域,堆内存主要由新生代和老年代两部分组成。其中新生代由一个伊甸园(Eden)和两个幸存者Survivor From和Survivor To 3部分组成,新建立对象首先保存在Eden中,当Eden中对象达到必定数量时,JVM触发Minor GC,GC时,先把Eden和From中的存活对象拷贝到Survivor To区,再清除Eden和From两个区域的数据,最后From和To互换身份,完成一次内存回收。新生代区域对象数量大,存活时间短,通常采用复制-清除算法,经过这种结构和回收方式来提升垃圾回收效率,减小内存碎片。
通过若干(默认15)次后还存活的对象,将进入老年代区,当老年代数据满时,会触发Major GC(又称Full GC),此时新生代、老年代、元区域、直接内存区域都会执行GC操做。
老年代:新生代的内存大小默认比例为2:1。Eden和两个Survivor的比例为8:1:1。线程
上述的内存标记算法、回收方式和分代策略是垃圾回收的方法,根据这些方法,针对不一样的用户场景(Server、Client)和系统配置(单线程、多线程),JVM实现了适用于各场景的垃圾回收器。设计
年轻代收集器
老年代收集器
混合收集器
Serial是单线程收集器,Serial收集器只能使用单个线程进行收集工做,在收集的时候必须得停掉其它线程,等待收集工做完成其它线程才能够继续工做。
Serial收集器是JVM中最先的垃圾收集器,也是JDK1.3前的惟一收集器,再也不适用于现代多核CPU和Server(服务端)场景,可是很是的适合单核CPU和Client场景。
ParNew是Serial的升级版,其工做的流程和Serial基本一致,主要的改进是支持多线程同时执行垃圾回收工做,即上图中的GC Thread支持多线程,能够充分利用多核CPU的性能。它是HotSpot上第一个真正意义实现并发的收集器。GC默认开启线程数等于CPU数量,可经过 -XX:ParallelGCThreads
来控制垃圾收集线程的数量。
Parallel Scavenge是吞吐量优先的收集器,其工做方式和ParNew基本同样,可是它以提升系统吞吐量(Throughput)为设计目标,吞吐量=业务运行时间/系统总运行(业务+GC)时间。
ParNew等收集器的关注点是尽可能缩小垃圾回收的停顿时间,而缩短停顿时间必然须要提升垃圾回收的频率,致使业务线程和GC线程间频繁的切换,从而增长CPU在现场切换上的损耗。
而以吞吐量为设计目标的Parallel Scavenge收集器,能够经过扩大新生代内存容量,减小垃圾回收发生的次数,虽然提升了单次GC的时长,但减小了线程切换开销,从总体上能够提升系统的吞吐量。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是:
参数 | 做用 | 说明 |
---|---|---|
-XX:MaxGCPauseMillis | 控制最大垃圾收集停顿时间 | 单次GC的最大毫秒数 |
-XX:GCTimeRatio | 设置吞吐量大小 | 业务:GC时间比例,默认为99,即GC时间占比为 1/(1+99)=1% |
单次GC时间参数并不是设置的越小越好,而是一把双刃剑,若是减小单次GC时间,必然致使GC频率的上升;而设置的增大,则必然须要更大的内存来支撑。
因为Parallel Scavenge和其余收集器(Serial、ParNew、CMS等)使用了不用的设计框架,致使其没法和CMS协同工做。
工做模式基本和新生代的Serial同样为单线程,它采用标记-整理算法,这个模式主要是给Client模式下的JVM使用。若是是Server模式有两大用途:
Parallel Scavenge的老年版本,JDK6开始出现,采用标记-整理算法。Parallel Old的出现结合Parallel Scavenge,真正的造成“吞吐量优先”的收集器组合。JDK7和8中,做为老年代默认的收集器。
在JDK6之前,新生代的Parallel Scavenge只能和Serial Old配合使用,而Serial Old为单线程,Server模式下没法充分利用多核CPU,这种组合没法让应用的吞吐量最大化。
CMS收集器是以最短回收停顿时间为目标的收集器。重视响应,以带来好的用户体验,是并发低停顿收集器,经过-XX:+UseConcMarkSweepGC参数启用CMS收集器。
CMS采用支撑多线程并发的标记-清除算法,它的运做分为4个阶段:
CMS在初始标记和从新标记阶段须要暂停业务线程,在执行时间上,初始标记 < 从新标记 < 并发标记,因此时间最长的并发标记,业务线程和GC线程并发运行,因此用户感觉上,GC暂停的时间很短。但其也存在几个缺点,具体以下:
(CPU数+3)/4
,性能很容易受CPU核数影响
为了解决CMS致使的内存碎片问题,CMS模式提供了
-XX:+UseCMSCompactAtFullCollection
选项,选项默认开启,用于CMS要进行Full GC时进行内存碎片整理,因为内存整理的过程没法并发,须要中止业务进程,因此启这个选项会影响性能。