java中垃圾收集器于内存分配策略
java中为我们提供很多种垃圾回收的收集器每种收集都有不同的特点更具业务需求使用不同的收集器会带来服务性能上的大幅度提升.
新生代收集器
Serial
serial收集器应该是最基本的最久的收集器在jdk1.3之前是虚拟机新生代收集器的唯一选择,serial是一个”单线程”收集器,这个”单线程”有两个.一是它只会使用一个cpu和或者一条线程去完成垃圾收集工作,二是在它进行收集的同时会暂停其他所有工作线程知道收集结束这就是垃圾回收最大的挑战问题STW(Stop The World),在用户不可见情况下停掉所有工作线程,这个是个很蛋疼的问题,但是也没办法因为在线程回收期间如果工作线程不停止那么垃圾回收这个机制就永远无法完成,就像是一个人边扫地另一个人在扔垃圾,虽然看上去Serial像是一个老而无用的的比较鸡肋的收集器,但是它也有他的优点,他的优点就是简单高效对于单个cpu环境来说Serial收集器没有线程交互的开销自然可以做到最高单线程收集效率,所以serial收集器对于运行在client模式下的虚拟机来说是一个很好的选择.
Serial收集器运行示意图虚拟机
ParNew
ParNew其实算是Serial的多线程版本,ParNew在Serial出了多线程收集之外并没有大的创新,但他确实许多运行在Server模式下的虚拟机中首选新生代收集器,其中有一个于性能无关的重要原因是应为目前只有它能配合CMS收集器配合工作,在jdk1.5时期虚拟机推虚拟机出一款划时代意义收集器CMS收集器(Concurrent Markk Sweep)它第一次实现了垃圾收集线程和工作线程同时工作能力,但是CMS作为老年代收集器却无法于1.4中的新生代收集器Parallel Scavenge配合工作.
ParaNew在单cpu的环境绝对不会比serial收集效果好,在多线程环境下更适用于ParaNew,它默然开启收集线程数于cpu的数量相同,在cpu非常多情况下可以使用-XX:ParallelGCThreads
参数来限制垃圾收集线程数量.
ParNew收集器运行示意图
Parallel Scavenge(下面简称PS)
PS其实和ParNew看上去都是一样,都是并行的多线程收集器,都是用复制算法,但是它和其他收集最大的不同在于它的关注点不同,CMS,ParNew收集器的关注点在于尽可能的缩短GC导致的工作现场停顿时间,而PS目标则是达到一个可控制的吞吐量(Throughput),借用借用官方解释就是CPU用于运行用户代码时间与CPU总消耗时间比值,即吞吐量=运行用户代码时间/(用户代码时间+GC时间)
,PS提供两个参数用于精确控制吞吐量,分别是最大垃圾收集时间-XX:MaxGCPauseMillis
参数和直接设置吞吐量大小参数-XX:GCTimeRatio
MaxGCPauseMillis允许设置一个大于O的毫秒数,收集器尽可能的保证GC时间不超过此设定值,注意千万不要认为吧这个参数设置的小就能让系统收集速度变快,GC停顿时间缩短是以减少吞吐量的代价和新生带空间的代价来换取的.
GCTimeRatio参数值允许设置为一个大于0小于100的整数,也是GC时间的占比数,例如 -XX:GCTimeRatio=19
GC时间占比=(1/19+1)即5%,默认为99即%1.PS还提供一个参数-XX:+UseAdaptiveSizePolicy
这是一个开关参数,当使用这个参数时就不需要知道新生代的大小-Xmm
,Eden于Survivor区的比例-XX:SurvivorRatio
,晋升老年代对象大小-XX:PretenureSizeThreshold
等细节参数,虚拟机会收集系统运行性能监控信息自动调整最合适的GC时间或者最大吞吐量,这种GC自适应的调节策略(GC Ergonomics)也是和ParNew很大的区别.
老年代收集器
Serial Old
Serial Old 是Serial老年代收集器版本,他同样是一个单线程收集器,使用”标记-整理”算法.它主要有两个用途,在jdk1.5之前主要用于与Parallel Scavenge收集器配合使用,另一个用途就是作为CMS收集器的后背方案在并发收集器发生Concurrent Mode Failure时使用.
Serial Old运行示意图
Paralle Old
Paraller Old 是Parallel Scavenge收集器老年版本,使用多线程”标记-整理算法”,这个收集器在jdk1.6提供,在此之前,Parallel Scavenge 一直处于一个很尴尬的状态,如果新生代选择了Paralle Scavenge,老年代只能选择Serial Old,因为CMS无法于Parallel Scavenge配合工作,由于Serial Old的服务端性能上的欠缺,就算使用Paraller Scavenge在吞吐量上还是没有PraNew+CMS组合高.直到Paraller Old收集器出现后,才有了真正意义上的”吞吐量优先”的组合(Paraller Scavenge + Parallel Old)
Paraller Old 运行示意图
CMS(Concurrent Mark Sweep)
从名字就可以看出来,CMS是基于”标记-清除”算法实现的一个并发型GC,它的整个过程稍微复杂一点可分为4步
- 初始标记 (CMS initial mark)
- 并发标记 (CMS concurrent mark)
- 重新标记 (CMS remark)
- 并发清除 (CMS concurrent sweep)
其中初始标记和重新标记两个步骤仍然会出现”STW”问题,初始标记仅仅只是标记一下GC Roots能直接关联到的对象(可达对象),重新标记标记则是为了修正并发标记期间因为用户程序继续运行而导致的标记产生变动的部分对象,这个阶段时间稍微长一点,但是远比并发标记时间短.
整个过程中耗时最长的是并发标记和并发清除两个过程,而这两个过程都可以与用户线程一起工作,所以CMS收集器的内存回收过程是与用户线程并发进行的.
CMS运行示意图
CMS虽然是一款优秀的收集器但是CMS还远远没有达到完美的程度,它有下面三个明显的缺点:
- 对CPU资源敏感
- 其实并发程序都对CPU资源敏感,在并发阶段虽然不会导致用户线程停顿,但是会因为占用一部分线程资源而导致程序变慢,吞吐量变低,CMS默认启动回收线程数是(CPU数量+3)/4,也就是说当CPU4个以上时,并发回收垃圾时线程资源不少于25%的CPU资源,并伴随着cpu的增加而下降,当cpu数不足4个时候CMS对用户程序影响很大,为了应对这种情况hotspot提供了一种”增量并发收集器”(Incremental Concurrent Mark Sweep /i-CMS)的CMS的变种收集器,原理就是在CPU过低CMS进行并发标记和清理的时候让GC线程和用户线程交替运行,尽量减少GC线程占资源时间,但是实践证明i-CMS 效果很一般,后面i-CMS被标记为 “deprecated”
- CMS无法收集浮动垃圾
- CMS无法收集浮动垃圾可能会出现”Concurrent Model Failure”失败从而导致一次Full GC,所谓浮动垃圾其实就是在CMS并发清理阶段用户线程还在运行,这段时间所出现的新垃圾产生,这段新垃圾没有被标识所以CMS无法收集,只有等下一次处理,也是因为垃圾收集时用户线程还在运行并且制造新的垃圾,那也就必须为这部分新的垃圾预留足够的内存空间给用户线程使用,所以CMS不能像其他老年代收集器一样等到老年代空间被塞满了再进行回收,jdk1.5 默认情况下当老年代使用了68%时候会被激活,根据具体业务,如果老年代增长不是太快可以使用
-XX:CMSInitiatingOccupancyFraction
的值来提除法百分比.jdk1.6中默认已经提高的92%要是运行期间预留的内存无法满足程序需要就会出现一次”CMF”失败,这是后就会启用预备方案:启用Serial Old来进行老年带垃圾收集,这时候停顿时间就比较长了.所以所-XX:CMSInitiatingOccupancyFraction
参数设置占比过高会出现很多”CMF”失败
- CMS无法收集浮动垃圾可能会出现”Concurrent Model Failure”失败从而导致一次Full GC,所谓浮动垃圾其实就是在CMS并发清理阶段用户线程还在运行,这段时间所出现的新垃圾产生,这段新垃圾没有被标识所以CMS无法收集,只有等下一次处理,也是因为垃圾收集时用户线程还在运行并且制造新的垃圾,那也就必须为这部分新的垃圾预留足够的内存空间给用户线程使用,所以CMS不能像其他老年代收集器一样等到老年代空间被塞满了再进行回收,jdk1.5 默认情况下当老年代使用了68%时候会被激活,根据具体业务,如果老年代增长不是太快可以使用
- CMS收集器GC后存在大量空间碎片
- CMS是一款基于”标记-清除”算法实现的GC,这就意味着垃圾收集结束时会有很多内存空间碎片,空间碎片过多对大对象内存分配是一个很大的挑战,经常会出现明明老年代还有很多空间但是却无法找到足够容纳大对象的连续空间来分配当前对象,这样就不得不触发一次Full GC.为了解决这个问题,CMS提供一个参数用于在CMS收集器顶不住的时候进行Full GC时候开启内存碎片合并整理过程
-XX:+UseCMSCompactAtFullCollection
这是一个开关参数,默认是打开的,内存整理过程是无法并发的,空间碎片问题是没了但是停顿时间变长了,CMS还提供一个参数-XX:CMSFullGCsBrforeCompaction
这个参数用于设置执行多少次Full GC后跟着来一次碎片整理过程,默认为0,意思是每次Full GC 都进行整理.
- CMS是一款基于”标记-清除”算法实现的GC,这就意味着垃圾收集结束时会有很多内存空间碎片,空间碎片过多对大对象内存分配是一个很大的挑战,经常会出现明明老年代还有很多空间但是却无法找到足够容纳大对象的连续空间来分配当前对象,这样就不得不触发一次Full GC.为了解决这个问题,CMS提供一个参数用于在CMS收集器顶不住的时候进行Full GC时候开启内存碎片合并整理过程
G1 (-XX:+UseG1GC)
G1收集器可以说是很非常棒了,不仅能够充分利用CPU,多核优势来缩短”STW”停顿时间,还能够自动进行空间整合,这就意味着前面说的CMS空间碎片问题不存在,还能对”STM”停顿时间可预测,G1的各个优势.
- 并发于并行
- G1充分利用多CPU多核环境下的硬件优势使用多个CPU来缩短”STP”停顿时间,部分其他收集器原本需要停顿java线程执行GC动作,G1仍然可以通过并发方式让java继续执行.
- 分代收集
- 于其他收集器一样分代概念在G1中依然保留,但是G1可以不需要其他收集器配合就能独立管理整个GC堆,它能够采用不同方式处理新创建的对象和已经存活一段时间以及熬过多次GC的旧对象已获得更好的手机效果.
- 空间整合
- 于CMS”标记-清除算法不同”G1从整体上来看是基于”标记-整理”算法实现,G1 将整个堆划分为一个个大小相等的小块(每一块称为一个 region),每一块的内存是连续的。和分代算法一样,G1 中每个块也会充当 Eden、Survivor、Old 三种角色,但是它们不是固定的,这使得内存使用更加地灵活。
- 执行垃圾收集时,和 CMS 一样,G1 收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1 也就知道哪些区块基本上是垃圾,存活对象极少,G1 会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间.
- 可预测停顿
- 这是G1不同于其他收集器的另一个优势,降低停顿时间是G1和CMS共同的关注点,但是G1出了追求低停顿外还能建立可预测停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内
-XX:MaxGCPauseMillis=N
,默认是200ms.G1收集器之所以能建立可预测停顿模型,是因为它可以有计划的避免整个Java堆中进行全区域垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据设置的收集时间优先回收价值最大的Region 这也是为什么 G1 被取名为 Garbage-First 的原因.这种使用Region划分内存空间以及有优先级的区域回收方式保证了在有限时间内可以最大化收集效率,将G1理解为内存”化整为零”的思路比较好理解,但是这其中内部时间肯定不是这么简单.
- 这是G1不同于其他收集器的另一个优势,降低停顿时间是G1和CMS共同的关注点,但是G1出了追求低停顿外还能建立可预测停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代于老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的,G1中每个Region都有一个与之对应的RSet,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier 暂时中断写操作,检查Reference引用对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关信息记录到被引用对象所属的Region的Remembered Set之中,当进行回收时在GC根节点中加入Remembered Set 即可保证不对全堆扫描也不会有遗漏.
忽略维护Remembered Set操作 G1收集器大致可以分为4个步骤
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选标记(Live Data Counting andf Eevacuation)
其实G1在很大程度上于CMS很相似,初始标记仅仅只是标记小GC Roots能直接关联的对象,并且修改TAMS(netx Top at Mark Start)的值,让下一阶段用户线程并发运行时能够在正确的Region中创建对象,这阶段需要停顿线程,但耗时很短,并发标记是从GC Root开始对堆中对象进行可达分析,找出存活对象,这段比较耗时,但是可以与用户线程并行执行,最终标记则是为了修正在并发标记期间用户线程运行所导致标记产生变动的那一部分标记(这部分对象产出变化记录在线程Rememberd Set Logs中),最终标记就是将这短时间对象变化记录合并到RSet中,这段时间需要停顿,但是可并行执行,最后筛选回收阶段首先对各个Region的回首价值进行排序,根据用户设置的GC停顿时间来制定最大价值回收计划(从sun公司透漏出的信息看,这个阶段也可以做到与用户线程并发执行的,但是因为只是回首一部分Region,时间也是用户控制的,而且停顿用户线程将大幅度提高收集效率)
G1收集器运行示意图
内存分配策略和回收策略
对象分配其实就是在堆上分配(也有可能经过JIT编译后被拆散为标量类型并间接的栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲将按线程优先在TLAB上分配,少数情况下也可能直接分配在老年代中,分配规则不是百分百确定,细节取决的使用哪种收集器组合和虚拟机于内存相关参数.
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 所谓大对象是指需要大量的连续空间的java对象比如说字符串和数组,大对象对虚拟机来说是个坏消息,而短命的大对象就是更加坏的消息!
- 长期存活对象进入老年代
- 熬过多次Minor GC 的对象会被晋升到老年代
- 动态对象年龄判断
- 空间分配担保
垃圾收集参数总结(这些参数可能不准确,具体默认参数需要看你使用的是虚拟机和jdk版本)
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后使用Serial+Serial Old 的收集器组合 |
UseParNewGC | 打开此开关后使用PraNew+Serial Old 的收集器组合回首 |
UseConcMarkSweepGC | 打开此开关后使用ParNew+CMS+Serial old 的收集器组合进行内存回首,Serial Old 作为CMS出现”CMF”失败后的后背收集器使用 |
UseParallerGC | 打开此开关后使用Parallel + Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收,这是虚拟机运行在Server模式下的默认值. |
UseParallerOldGC | 打开此开关后,使用Paraller Scavenge + Parallel Old的收集器组合进行内存回收 |
UseG1GC | 打开此开关后使用G1收集器进行内存回收 |
SurvivorRatio | 新生代中Eden区域和Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,大于此阈值直接在老年代分配空间 |
MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次MinorGC之后年龄就会+1当超过这个阈值时进入老年代,默认15 注意:这个也不一定就复合这个值详情看这里 |
UseAdaptiveSizePolicy | 打开此开关启用动态调整调整堆中各个区域的大小已经进入老年代的年龄 |
ParallelGCThreds | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占比,默认99,即允许1%GC时间,仅在 Paraller Scavenge收集时生效 |
MaxGCPauseMillis | 设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效 |
CMSInitiatingOccupancyFraciton | 设置CMS收集器在老年代空间被使用多少后触发垃圾收集.默认值更具各个不同jdk版本设置以及各个厂商虚拟机默认设置 |
UseCMSCompactAtFullCollection | 设置CMS 出现Full GC收集后是否需要进行一次内存碎片整理,仅在CMS生效,默认开启 |
CMSFullGCsBeforeCompaction | 设置CMS在多少次Full GC后启动一次内存碎片整理,仅在CMS生效,默认0 每次都开启 |
特别说明
并行(Parallel):指多条垃圾收集线程并行工作,此时用户线程仍然处于等待状态.例如ParNew和Serial 都是属于并行收集器
并发(Concurrent):指工作线程和垃圾收集线程同时运行(不一定是并行,可能是交替运行),用户程序子啊继续运行,而垃圾回收线程在另一个cpu上. 例如CMS. G1是属于并发和并行结合体.
- 新生代GC(Minor GC):指发生在新生代的垃圾手机动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC平率非常高,回首速度也很快.
- 老年代GC(Major GC/Full GC): 指发生在老年代的GC 出现MajorGC,通常会伴随一次Minor GC 但是这并不是绝对的,MajorGC一般会比MinorGC慢10倍左右
GC触发时机可以参考这篇文章传送门这两两个概念一定要搞清楚!
关于java8改动
- Lambda Expressions
- Pipelines and Streams
- Date and Time API
- Default Methods
- Type Annotations
- Nashorn JavaScript Engine
- Concurrent Accumulators
- Parallel operations
- PermGen Space Removed
- TLS SNI
我们主要看 PermGen Space Removed 这个,移除了永久代,其目的是为了Hotspot jvm和JRockit jvm融合设计,java8中将永久带移到heap中,将将字符串常量和类中的静态变量放到内存里面。
参考文章
深入理解jvm虚拟机
https://plumbr.io/handbook/garbage-collection-algorithms-implementations#g1
https://blogs.oracle.com/poonam/understanding-g1-gc-logs
http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html