前言
垃圾收集器GC ,同通常需要考虑3个事情
- 1、那些内存需要回收
- 2、什么时候回收
- 3、如何回收
1、不需要回收的
程序计时器,虚拟机栈,本地方法栈这3个区域都是线程所私有的,随着线程而生,而死。 关于栈的话,基本上就是在运行方法的时候开启一个栈帧。他们的内存大小和声明周期是已知的,因此这几个区域内存分配和回收都具备确定性,不需要过多考虑回收问题,因为他们在方法结束者是线程结束,内存自然的就被回收了
2、需要回收的
JAVA堆和方法区 则是需要被垃圾收集器回收的
1、判断对象是否活着
1.1、引用计数法
解释:给对象添加一个计时器,每当引用的时候加1,当引用失效时候减1,任何时候为0的对象就是不能再被使用的。(书上说,这样表达不太好)
java虚拟机没有使用它来管理内存,因为它很难解决对象之间相互引用的问题
1.1.1、测试代码
1 | package com.hlj.jvm.GC; |
1.1.2、idea查看GC日志
1 | -XX:+PrintGCDetails |
1 | -XX:+PrintGC 输出GC日志 |
1.1.3、运行
名称通过收集器而定
1、这里的收集器是Parallel Scavenge。新生代为PSYoungGen,老年代为ParOldGen,Metaspace代表元空间(JDK 8中用来替代永久代PermGen)。
2、如果收集器为ParNew收集器,新生代为ParNew,Parallel New Generation
3、如果收集器是Serial收集器,新生代为DefNew,Default New Generation
1 | [GC (System.gc()) [PSYoungGen: 5980K->2752K(76288K)] 5980K->2760K(251392K), 0.0023918 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
这就说明JDK8的HotSpot虚拟机并没有采用引用计数算法来标记内存,它对上述代码中的两个死亡对象的引用进行了回收。(因为内存变小,肯定是回收了,要不然能变么) 具体看下面
1、可以看到上面有两种GC类型:GC和Full GC,有Full表示这次GC是发生了Stop-The-World(即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起,因为执行了System.gc();
)
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度非常快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,Major GC的速度一般会比Minor GC慢10倍以上。
1 | [GC (System.gc()) [PSYoungGen: 5980K->2752K(76288K)] 5980K->2760K(251392K), 0.0023918 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
2、上面方括号内部的5980K->2752K(76288K),表示GC前该新生代已使用容量->GC后该新生代已使用容量(也就是Survivor占用的控件为2752K),后面圆括号里面的76288K为该新生代的总容量(其实就是8+1 (Elen+Survivor 占比为9),观察下面的就可以看出来 eden(65536K)+from(10752K)=76288K)。
1 | PSYoungGen total 76288K, used 1966K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000) |
方括号外面的5980K->2760K(251392K),表示GC前Java堆已使用容量->GC后Java堆已使用容量,后面圆括号里面的251392K为Java堆总容量。
通过上面的可以计算出 老年代的空间了 Java堆-新生代 = 老年代 (251392k- 76288k=175 104k) GC前 (5980K-5980K=0k) GC后(2760K-2752K=8k)(就是说新生代GC后8K会进入老年代)
1 | [Full GC (System.gc()) [PSYoungGen: 2752K->0K(76288K)] [ParOldGen: 8K->2621K(175104K)] 2760K->2621K(251392K), [Metaspace: 3139K->3139K(1056768K)], 0.0057354 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] |
老年代GC, PSYoungGen: 2752K->0K(76288K) 老年代GC前新生代已使用容量->老年代GC后新生代已使用容量(2752k就是上面我们说到的新生代在Survivor中占据的容量)
[ParOldGen: 8K->2621K(175104K)] 2760K->2621K(251392K), 表示老年代GC前老年代已使用的容量->老年代GC后老年代已使用的容量(发现增长了 ,有趣吧),圆括号中的数值就是上面我们已经推算出的老年代的总容量(175104K) 2760K->2621K(251392K)则为老年代GC前java堆已经使用的总容量(发现其实就是新生代GC后JAVA堆的已经容量)->老年代GC后java推已使用的容量(其实也就是老年代的已使用的容量了,因为可以新生代都死了,只剩下老年代了)
1 | [Times: user=0.01 sys=0.00, real=0.00 secs] |
3、[Times: user=0.01 sys=0.00, real=0.00 secs]
- 分别表示用户消耗的CPU时间,
- 内核态消耗的CPU时间
- 操作从开始到结束所经过的墙钟时间(Wall Clock Time),
CPU时间和墙钟时间的差别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时。
2、可达性分析算法
解释:这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
可以作为GC Roots的对象包括下面几种
. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
. 本地方法栈中JNI(Native方法)的引用的对象
. 方法区中的类静态属性引用的对象
. 方法区中的常量引用的对象
2、垃圾收集,一定非死不可吗
解释:
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段, 要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,
- 筛选的条件是此对象是否有必要执行finalize() 方法。当对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。这样就会被垃圾收集器调用回收。如下
1 | package com.hlj.jvm.GC; |
运行结果,说明第一次成功逃脱,finalize为对象逃脱的最后一次机会
1 | 执行finalize方法 |
3、回收方法区
大部分都认为方法区(也叫永久代)是没有垃圾回收的,Java虚拟机规范中也说过不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集性价比很低。一帮情况下新生代中回收的性价比比较高
3.1、回收内容
废弃常亮和无用的类,当然回收是可以,而不是一定能够回收
3.1.2、无用的类
(1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.1.3、回收方法和场景
1、回收方法
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看类的加载和卸载信息。
回收场景
2、在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
4、垃圾收集算法
不同平台的虚拟机操作内存的方法是不同的,这里主要介绍下几种算法的思想和发展过程
4.1、标记-清除算法
很明显,两个阶段,标记和清除踏实最基础的算法,因为后续的手机算法都是基于这种思路并对他的不足进行改进而得到的
不足有两处
1、效率问题:这两个过程效率都不高
2、空间问题:标记清除会产生大量不连续的碎片,碎片太多费配给大的对象的时候,无法找到连续的控件而不得不触发另一次垃圾收集动作
4.2、复制算法
为了解决上面的效率问题,就出现了复制,它将内存分为大学相等的两块,每次只使用其中一块,
当这一块的内存满了
,就会将里面活着的对象复制到另一块上面,然后再把已经使用过的空间一次清理掉(牛逼了,相当于的夫妻二人大家,满了就跑)这样就不需要考虑是否存在碎片了,但是但是,它他妈的把内存缩小了一半,这代价太高了
4.2.1、使用
现在的商业虚拟机都采用这种收集算法手机
新生代
,IBM公司研究到其实新生代中的对象98%都是早上出生,晚上就挂了。所以其实不需要1:1来配置,而是分成3块,一块较大的和两块较小的 比为8:1:1。
每次使用的时候,都是使用一个快大的和一块小的,当垃圾收集器回收的时候,就会把这两个上面存活的对象放到另外一个小的上面。然后清理刚刚的那两个空间。 这个时候,如果继续使用的话,就会继续放到大的上面。也就是说,只会浪费10%的空间
从实际出发,其实我们不能保证每次都只有10%的对象存活,但是当它这个小的空间不够用的时候,会依赖其他内存进行分配担保。这个时候这些对象就会进入老年代
。关于担保后面讲吧,哈哈,是不是很简单呢
4.3、标记-整理算法
复制算法在存活率特别高的时候,效率就会降低,更关键的是,老年代存活率高,假如所有对象对100%存活,那么需要有额外的空间来进行担保。所以在老年代一般不能使用这种算法。老人不是喜欢收拾东西吗,哈哈,标记整理吧
这里不是讲标记的对象之间进行清理,而是先将可用的对象都像一边移动,然后之间清理掉除它以外的内容
4.4、分代收集算法
当前商业虚拟机都采用这种算法来收集,这种算法将对象存活周期的不同而将Java堆分为新生代和老年代,
1、在新生代总每次都有大量的对象死去,只有少量存活,就使用复制算法,这样就付出存活少量对象的复制成本就可以完成收集,2、但是老年代因为存活率高,没有额外的空间为它担保就必须使用标记-清除或者是标记-整理算法。
5、垃圾收集器
如果收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体表现,Java虚拟机堆垃圾收集器如何实现并没有任何规定,因此不同的厂家,不同版本的虚拟机所提供的垃圾收集器可能会有很大差别,并且一般都是提供参数,用户根据自己的特定和要求组合出各个年代所用的收集器。
如何两个收集器直接存在连线,就说明可以搭配使用,如果下面介绍的收集器进行比较,但是并没有哪个收集器是完美的,我们只是根据具体应用选择最合适的收集器
1 | 新生代收集器:Serial、ParNew、Parallel Scavenge; |
1-2、并发垃圾收集和并行垃圾收集的区别
(A)、并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;
(B)、并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);
5.1、Serial收集器 (串行收集器)
1 | -XX:+UseSerialGC:添加该参数来显式的使用串行垃圾收集器; |
针对新生代;采用复制算法;单线程收集;
进行垃圾收集时,必须暂停所有工作线程,直到完成;即会”Stop The World”; 相当于是妈妈在打扫房间,让我们乖乖在凳子上站着,等妈妈打扫完成。这种在用户不可见的情况下把用户正常的工作的线程全部关掉,这对于很多应用来说是不能够接受的
但是
1、它现在依然是client模式下的虚拟机默认新生代的收集器,简单而且高效,因为它是单线程的,没有线程加护的开销,专心做事。
2、在用户的桌面应用场景中,分配给虚拟机的内存不会很大,停顿时间非常少,只要这种听得不是频繁发生。这是可以接受的
总之 :Serial垃圾收集器在client模式下的虚拟机来说是一个不错的选择
5.2、ParNew收集器
解释:其实他是serial的多线程版本,与serial相比并没有太多的创新之处,但是它是server模式下迅疾中首选的新生代收集器,其中有一个性能更重要的原因是,除了serial外,目前只有它能够CMS垃圾收集器配合工作
1 | "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器; |
5.3、parallel Scavenge (:英文:平行打扫,吞吐量收集器)
1 | -XX:+UseParallelGC 明确指定使用Parallel Scavenge收集器 |
是JAVA虚拟机在Server模式下的默认值(比如我的电脑就是),使用Server模式后,java虚拟机使用Parallel Scavenge收集器(新生代)+ Serial Old收集器(老年代)、在JDK1.5及之前,JDK1.6之后有Parallel Old收集器可搭配) 的收集器组合进行内存回收。
新生代收集器,使用的也是复制算法,而且是并行的多线程收集器看上去和preNew一样,但是。
它的特点是与其他的垃圾收集器关注点不同,CMS等收集器所关注的是尽可能缩短垃圾收集器收集时候的用户线程的停顿时间,但是它的目标是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) 比如虚拟机总共运行了100分支,垃圾收集花掉1分钟,那么吞吐量就是99% 高的吞吐量就是可以高效的利用cpu时间
应用场景
主要适应主要适合在后台运算而不需要太多交互的任务。比如需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务等。
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;
1 | "-XX:MaxGCPauseMillis" 控制最大垃圾收集停顿时间(可以这样理解,每次1G的时候才清理垃圾,时间挺长的,但是当500M的时候,就清理,清理垃圾的时间就短了,剩下的可能与用户交互的时候用不到,就不清理,或者很长时间才清理呢 ),大于0的毫秒数;设置得稍小,停顿时间可能会缩短, |
1 | "-XX:GCTimeRatio" 设置垃圾收集时间占总时间的比率,0<n<100的整数; |
JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);
这是一种值得推荐的方式:
(1)、只需设置好内存数据大小(如”-Xmx”设置最大堆);
(2)、然后使用”-XX:MaxGCPauseMillis”(更关注最大停顿时间)或”-XX:GCTimeRatio”(更关注吞吐量)给JVM设置一个优化目标;
(3)、那些具体细节参数的调节就由JVM自适应完成;
这也是Parallel Scavenge收集器与ParNew收集器一个重要区别;
垃圾收集器期望的目标(关注点)
(1)、停顿时间 (垃圾收集器垃圾的时候用户线程的停顿时间)
停顿时间越短就适合需要与用户交互的程序;
良好的响应速度能提升用户体验;
(2)、吞吐量
高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;
主要适合在后台计算而不需要太多交互的任务;
(3)、覆盖区(Footprint)
在达到前面两个目标的情况下,尽量减少堆的内存空间;
可以获得更好的空间局部性;
5.4、、Serial Old收集器
Serial Old是 Serial收集器的老年代版本;
1、特点
针对老年代;
采用”标记-整理”算法(还有压缩,Mark-Sweep-Compact);
单线程收集;
2、应用场景
主要用于Client模式;
而在Server模式有两大用途:
(A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
(B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
5.5、Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;
JDK1.6中才开始提供;
1、特点
针对老年代;
采用”标记-整理”算法;
多线程收集;
2、应用场景
JDK1.6及之后用来代替老年代的Serial Old收集器;
特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的”给力”应用组合;
5.6、CMS收集器(Concurrent Mark Sweep)
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;
在前面ParNew收集器曾简单介绍过其特点;
1、特点
针对老年代;
基于”标记-清除”算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标;
并发收集、低停顿;
需要更多的内存(看后面的缺点);
应用场景
与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用;
过程
(A)、初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要”Stop The World”;
(B)、并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;
(C)、重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
(D)、并发清除(CMS concurrent sweep)
回收所有的垃圾对象;
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;
所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
缺点
1、对CPU资源非常敏感
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
CMS的默认收集线程数量是=(CPU数量+3)/4;
当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
2、无法处理浮动垃圾,可能出现”Concurrent Mode Failure”失败
(1)、浮动垃圾(Floating Garbage)
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;
这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;
也要可以认为CMS所需要的空间比其他垃圾收集器大;
1 | "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间; |
(2)、”Concurrent Mode Failure”失败
如果CMS预留内存空间无法满足程序需要,就会出现一次”Concurrent Mode Failure”失败;
这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。
(C)、产生大量内存碎片
由于CMS基于”标记-清除”算法,清除后不进行压缩操作;
前面《Java虚拟机垃圾回收(二) 垃圾回收算法》”标记-清除”算法介绍时曾说过:
产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
解决方法:
(1)、”-XX:+UseCMSCompactAtFullCollection”
使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
但合并整理过程无法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)、”-XX:+CMSFullGCsBeforeCompaction”
设置执行多少次不压缩的Full GC后,来一次压缩整理;
为减少合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
由于空间不再连续,CMS需要使用可用”空闲列表”内存分配方式,这比简单实用”碰撞指针”分配内存消耗大;