JAVA-JVM-GC入门
1.什么是GC?
GC-Garbage collection,意为垃圾回收,在JVM中就是对不需要使用的内存空间进行回收, 这个过程就叫做GC回收.
2.GC的基础
2.1 GC的分类 Minor GC、Major GC和Full GC
1.Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
- 什么时候会触发Minor GC?
当JVM无法为一个新对象分配内存空间时会触发(例如Eden区域满了)
- Minor Gc回收的是哪里的垃圾?
绝大多数的对象都是在Eden出生的,而Eden又属于新生代.
所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。
2.Major Gc/Full Gc:
Major GC 是清理老年代空间,通常Major GC会伴随着至少一次的Minor GC(Major回收效率为Minor的十分之一)
Full GC 是清理整个堆空间—包括年轻代和老年代。
2.2 新生代和老年代
在我们对GC了解之前,先要了解几个概念.
堆区中的空间分布:
对内存空间进行分代,是为了优化GC的性能,将长期存活的放在一边,将不确定存活时期的放在一边,分别称其为新生代和老年代.
1.新生代(young)
- 将新生代又划分为三个区域,eden,survivor 1,survivor 2.
新生代被划分为这三个区域,空间比例为8:1:1
1)Eden:内存占比最大,对象在此生成
2)survivor1与survivor2: 在GC过后每次将Eden与Survivor1中存活的对象,复制到Survivor2中.
- 什么样的对象可以进入新生代:
大多数情况下,对象会在新生代EDEN中进行分配,当Eden中空间不足时,会对对其进行Minor GC操作.
回收效率:新生代回收速度快
2.老年代(old):
老年代的对象存活时间都比较长.
- 什么样的对象可以进入老年代?
1)大对象直接进入老年代:大对象是指需要大量连续内存空间的对象,例如长字符串以及数组.
2)长期存活的对象进入老年代:JVM为了识别新对象与老对象,会为每一个对象设置一个Age的count,如果对象在eden出生并且经过第一次新生代GC(Minor GC)仍然存活,并且能被survivor容纳,就会被移动到survivor中,并且将Age设为1,当年龄增加到一定程度(default:15)就会晋升到老年代.
3)动态年龄判定:如果survivor空间中相同年龄的所有对象大小综合大于survivor空间的一般,大于等于这个年龄的对象可以直接进入老年代,无视第二条的默认年龄设置.
回收效率:老年代回收效率大概是新生代的十分之一.
要对内存进行合理的回收,需要解决三个问题,带着问题往下看,应该会有所理解.
1.哪些内存需要回收?
2.什么时候对其进行回收?
3.怎么进行回收操作.?
3.GC的算法
1.标记-清除算法:
标记-清除算法为最基础的收集算法,算法分为标记和清除两个阶段:
1.执行步骤:
1)首先标记出所有要回收的对象.
2)对标记的对象进行统一回收.
2.缺点:
1)效率不高
2)会产生大量不连续的内存空间,会导致分配大对象的时候出现问题.
2.复制算法:
为了解决上述的效率问题,复制算法随之而生.
1.执行步骤:
1)将可用内存空间划分为相等的A,B两块,每次使用其中一块.
2)当A空间使用完毕,将存活对象复制到B空间,将A空间内存统一清除整理.
2.缺点:
缺点可想而知,我们每次可以使用的内存空间仅为一半,虽然大大增加了GC的效率,但是付出的代价未免太高了一些.
改良:经过IBM公司研究表明,由于新生代对象朝生夕死,存活时间普遍不长,故将其内存空间分为Eden和两块survivor空间,比例为8:1:1, 也就是上述内存空间分配的由来,GC时将survivor复制到to survivor中, 进行复制算法.
分级担保:由于无法保证每次存活的对象都小于等于10%,所以当超过10%的时候需要依赖其他内存(如老年代),也就是额外空间.
3.标记-整理算法:
复制收集算法在对象存活率比较高的时候就会进行较多的赋值操作,效率会降低,如果使用8:1:1的分配比例时,需要额外空间进行担保,故此老年代并不能选用复制算法进行GC.
根据老年代对象存活时间的特点:标记-整理算法出现了.
标记整理算法与标记清除算法思路相同,但是后续步骤不同.
1.执行步骤:
1)标记存活对象
2)将存活对象向同一端移动
3)对端边界外的内存进行统一清理,这样获得的内存空间就是完整连续的.
ps:标记-整理与标记-清除算法的不同点就在于存活对象移动这一点
4.分代收集算法:
根据对象存活的时间进行分类,选择最有效率GC方法进行GC,现在我们都是采用分代收集进行内存回收.
分代收集就是将堆分为新生代和老年代.
新生代:如果发现每次大量对象死亡那么就选择复制算法,
老年代:存活时间比较长,没有额外空间分配担保,必须使用标记-清理或者标记-整理方法.
4.GC收集器类型
算法是GC方法的方法论,而这里GC收集器就是对上述GC方法的实现.
GC收集器分为7种:
1.新生代:
- serial
- ParNew
- Parallel Scavenge
2.老年代
- CMS
- Serial Old(MSC)
- Parallel Old
3.G1:单拿出来说是因为它不分新老,全区域覆盖.
新生代内存收集
1)serial:
特点:
1.单线程
2.效率高
3.缺点:在GC时需要停止所有线程.
使用算法:
新生代:复制算法
老年代:标记-整理算法
执行过程:
1.暂停所有用户线程(stop the world,作者第一次从书上看到也感觉中二病都要发作了..)
2.进行新生代GC
3.进行老年代GC
优势和缺陷:
优势:效率高,能与CMS进行配合
劣势:单线程,在回收较大内存空间的时候Stop the world时间较长,会造成极差的用户体验.
2)ParNew:
ParNew可以说是serial的多线程版本,除了使用多线程进行GC外,其余配置全部与serial一样.
特点:
1.多线程
2.在GC时需要停止所有线程.
执行过程:
1.暂停所有用户线程
2.进行新生代GC
3.进行老年代GC
优势:
1.在多CPU多线程的情况下可以充分利用资源,增加GC效率.
2.能与CMS配合
劣势:
1.在GC时需要停止所有线程.
2.在单线程的情况下效率没有serial高.
并行与并发:
并行:指多条线程同时进行GC,但是用户仍然需要等待.
并发:用户线程与GC同时进行,GC单独占用一个CPU进行GC,或者用户线程与GC线程交替执行.
3)Parallel Scavenge
Parallel Scavenge是并行收集器,同时也是多线程收集器,算法也与上边无异.
特点:
1.这种收集器关注的是吞吐量,其他搜集器关注的是停顿时间(stop the world的时间)
吞吐量:
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
也就是说,吞吐量越高,GC的时间就越短.
乍一看好像与上边两种方法没什么区别,停顿时间短了,吞吐量不就上去了吗?
实际上,说关注的是吞吐量是因为这种搜集器提供了两种参数供我们使用.
控制最大GC时间:MaxGCPauseMills
这里GC的时间是以毫秒为单位,GC时间的缩短是以牺牲吞吐量和新生代空间来换取的.
举个栗子:
若将停顿时间减少,由原来的每10秒搜集一次,停顿100毫秒,改为每5秒搜集一次,停顿时间为70毫秒,但从停顿时间间隔上是减少了.
整体的运行时间却增加了.
直接设置吞吐量大小:GCTimeRatio
这个设置的值就相当于GC占总时间的比率,设定范围为整数的(0,100)
老年代收集器:
1.serial Old
这个收集器是上边serial的老年代版本,同样是单线程收集器,主要意义是给在client模式下的虚拟机使用.
工作原理与serial相同.
特性:
与Parallel scavenge搭配使用
2.Parallel Old
这个收集器是Parallel scavenge的老年代版本,同样使用的是多线程+标记整理算法,JDK1.6后开始提供.
特性:
与Parallel scavenge搭配使用.
3.CMS:
CMS收集器是以获取最短回收停顿时间为目标的收集器,应用在JAVA的网站或者B/S系统的服务端上,重视响应速度,基于标记-清除方法.
执行过程:
1.初始标记:标记一下GC Roots能关联到的对象,速度很快
2.并发标记:GC ROOTS TRACING的过程
3.重新标记:修正在并发期间因程序继续运作导致的标记变动.
4.并发清除
其中1与3步骤仍然需要停止所有用户线程,而2与4是并发的情况,并不需要停止用户线程.
缺点:
1.在并发阶段会导致程序变慢,
默认启动线程数为(CPU数量+3)/4,当CPU数量较少的时候会给予系统比较大的负担.
2.无法处理浮动垃圾.可能会导致并发故障(concurrent mode failure)从而导致另外一次FULL GC.
浮动垃圾:CMS在并发阶段程序还在运行,会不断有新的垃圾产生,本次无法清除的垃圾,就是浮动垃圾.
3.由于使用的是标记-清除算法,会产生不连续的内存空间碎片,从而触发FULL GC
G1收集器
特点: 老幼通吃
1.并行并发
2.分代收集
3.空间整合(不会产生碎片内存空间)
4.可预测停顿(可以预计GC回收时间,不会超过峰值)
新生代老年代不再从物理上进行隔离,把内存划分成若干个大小相等的区域/
运行步骤:
1.初始标记
2.并发标记
3.最终标记
4.筛选回收
新老收集器的组合:
serial和ParNew可以和CMS与Serial Old进行组合
Parallel Scavenge可以与Serial Parallel Old进行组合
G1独成一派.
现在再来看上面的三个问题
1.哪些内存需要回收?
不存活的对象需要被回收,即不能被任何途径所引用的对象.
2.什么时候对其进行回收?
参考上边GC的情况,什么时候会触发MInorGc 什么时候会触发FullGC.
3.怎么进行回收操作.?
通过收集器进行GC.
再细节的东西 对于我来说还无能为力,我只能分享现在已知的,下边看一下GC日志怎么看
GC日志查看
[Full GC (System.gc()) [Tenured: 0K->593K(10944K), 0.0030196 secs] 1265K->593K(15872K), [Metaspace: 209K->209K(4480K)], 0.0030870 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
FULL GC:代表触发了Stop the World 也是GC的类型
Tenured:代表年老代
0.0030196 secs:为用时
0K->593K(10944K):空间变动,括号内为总空间
MetaSpace:为元空间,这里编译器采用的JDK版本为1.8,永久带被移除方法区, GC针对老年代和新生代,不用太过关注.
from Space与To space :就是之前两块survivor空间.
GC信息可以通过在idea中的Edit configuration->vm options处加入-XX:+PrintGCDetails后.
通过System.gc进行输出.
下边是GC的一些设置参数:
堆设置
-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :设置年轻代大小
-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n :设置持久代大小
收集器设置
-XX:+UseSerialGC :设置串行收集器
-XX:+UseParallelGC :设置并行收集器
-XX:+UseParalledlOldGC :设置并行年老代收集器
-XX:+UseConcMarkSweepGC :设置并发收集器
垃圾回收统计信息
-XX:+PrintHeapAtGC GC的heap详情
-XX:+PrintGCDetails GC详情
-XX:+PrintGCTimeStamps 打印GC时间信息
-XX:+PrintTenuringDistribution 打印年龄信息等
-XX:+HandlePromotionFailure 老年代分配担保(true or false)
并行收集器设置
-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间
-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数
Reference:
1.https://blog.csdn.net/bear_lam/article/details/79648701
2.http://www.importnew.com/15820.html
3.https://blog.csdn.net/huzhigenlaohu/article/details/51918854
4.深入理解JVA虚拟机 第二版 P61-P100