前言:
续更《深入理解jvm》,本来昨晚就可以出来的这篇,偷懒了=。=,keep going!!!
垃圾收集器
垃圾回收器是内存回收的具体实现。
两个收集器间有连线,表明它们可以搭配使用:
- Serial/Serial Old
- Serial/CMS
- ParNew/Serial Old
- ParNew/CMS
- Parallel Scavenge/Serial Old
- Parallel Scavenge/Parallel Old
- G1
其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案
-
新生代:
-
Serial收集器
针对新生代;
采用复制算法;
单线程收集;
进行垃圾收集时,必须暂停所有工作线程,直到完成;
即会"Stop The World";
对于运行在client模式下的虚拟机来说是一个不错的选测
-
ParNew收集器
Serial收集器的多线程版本;
采用复制算法;
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
-
Parallel Scavenge收集器
采用复制算法;
多线程收集;
主要特点是:该收集器的主要目标是达到一个可控制的吞吐量,即用户代码时间/用户代码时间+垃圾收集时间
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互,用此收集器;
提供参数控制最大停顿时间以及吞吐量大小。
提供 自动设置新生代内存结构分布的参数 “-XX:+UseAdptiveSizePolicy”,设置该参数后,JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略,这是它与ParNew的一个重要区别
-
-
老年代
-
Serial Old收集器
是Serial的老年代版本;
单线程;
标记-整理算法;
主要用于搭配Parallel Scavenge收集器,但也会拖累起速度
-
Parallel Old
针对老年代;
采用"标记-整理"算法;
多线程收集;
可以搭配Parallel Scavenge收集器,在Server模式,多CPU的情况下,注重吞吐量以及CPU资源敏感的场景十分有用。
-
CMS收集器
针对老年代;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标;
并发收集、低停顿;
如常见WEB、B/S系统的服务器上的应用;
他的运作流程分为4个步骤:初始标记、并发标记、重新标记、并发清楚。其中初始标记和重新标记需要gc停顿。初始标记姿势标记一下GC Roots能关联到的对象,并发标记就是进行GC Roots 跟踪的过程,重新标记则是为了修改并发标记期间因用户程序继续运行而导致标记产生变动的那部分对象标记记录。耗时最长的还是并发标记和并发清除。
缺点:
1.无法处理浮动垃圾:浮动垃圾(Floating Garbage)在并发清除时,用户线程新产生的垃圾(新的无用常量、无用类)称为浮动垃圾;这使得并发清除时需要预留一定的老年代内存空间,不能像其他收集器在老年代几乎填满再进行收集,即预留空间就开始执行清除,通常68%就会执行老年代回收;也要可以认为CMS所需要的空间比其他垃圾收集器大;Concurrent Mode Failure"失败如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
2.对于cpu资源敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。 CMS的默认收集线程数量是=(CPU数量+3)/4;
3.基于标记-清除算法实现的收集器:产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
解决方法:
(1)、"-XX:+UseCMSCompactAtFullCollection"
使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
但合并整理过程无法并发,停顿时间会变长;
默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)、"-XX:+CMSFullGCsBeforeCompaction"
设置执行多少次不压缩的Full GC后,来一次压缩整理;
为减少合并整理过程的停顿时间;
默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
-
-
整个堆
-
G1收集器
1.并行与并发
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短"Stop The World"停顿时间;
也可以并发让垃圾收集与用户程序同时进行;
2.分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别;
将整个堆划分为多个大小相等的独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
3.结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
-
内存分配与回收策略
对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,少数情况直接分配到老年代中。,如果 (-XX:UseTLAB)启动了本地线程分配缓存,则按照线程优先在TLAB()上分配。
TLAB:是虚拟机在堆内存的eden划分出来的一块专用空间线程专属。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如需要分配内存,在自己的空间上分配,在不存在竞争的情况大大提升分配效率
内存分配策略:
对象优先在Eden分配
大多数情况下,对象在新生代Eden中分配,当Eden没有足够的空间进行分配时,虚拟机就会发起一次Minor GC。
大对象直接进入老年代
需要大量连续空间的java对象,典型的就是很长的字符串和数组,直接在老年代分配。
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄的计数器,放在对象头中,如果对象在Eden出生并且经过了一次Minor GC后仍然存活,且存入Survivot to中,此时如果是第一次,对象的年龄会设置为1,对象在Survivor区每度过一次Minor GC,年龄+1,默认到15,就会升为老年代,存入老年代区域。(Eden中的对象没有年龄,只有Survivor中的对象有年龄)
动态对象年龄判
如果Survivot空间中相同年龄的所有对象大小的和大于Survivor的一半,年龄大于活着等于该年龄的对象就可以直接进入老年代。无需等age到15;
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代中所有对象的总空间,如果这个条件成立Minor GC是安全的,如果不成立,则虚拟机会查看HandlerPromotionFailure设置值是否允许担保失败。如果允许就会检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果大于,那么冒着风险尝试着进行Minor GC,如果小于活HandlerPromotionFailure设置为false就进行一次Full GC。
那么为什么要检查这么多东西呢???
检查老年代最大可用的连续空间是否大于新生代中所有对象的总空间,由于新生代采用复制算法,把Eden和Survivot from两个区域的存活对象转移到Survivor to,如果Survivor to就会触发空间分配担保,把存活对象直接放进老年代,由于无法知道存活的对象有多少,所以如果老年代可用内存大于新生代中所有对象的大小,那么无论如何,发生空间分配担保老年代都可以装下来,
检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小?
这是比较冒风险的一种做法,如果老年代最大可用连续空间大于历代新生代对象晋升老年代的平均值的话,大概率是不会出现老年代空间不够的,所以值得冒险。
如果失败的话就会导致担保失败HandlerPromotionFailure,只好重新发起一次Full GC了。