concmarksweepgccompact 会暂停应用吗

博客分类:
JVM堆(Heap)= 新生代(Young) + 旧生代(Tenured)
新生代(Young)= Eden区 + Survivor区
NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2
SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为32
JVM管理的内存叫堆;在32Bit操作系统上有4G的限制,一般来说Windows下为2G,而Linux下为3G;64Bit的就没有这个限 制。
JVM所占用的主要内存都是从堆空间分配的,堆是所有线程共享的,因此在堆上分配内存需要加锁,Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。其大小由JVM根据运行情况计算得到,也可通过参数-XX:TLABWasteTargetPercent来设置TLAB可占用的Eden空间的百分比,默认值为1%。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64但小于1G。
JVM最大分配的内存由-Xmx指定,默认是物理内存的 1/4但小于1G。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指 定。
默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。
服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小,所以上面的两个参数没啥用。
分代/堆模型
分代是Java垃圾收集的一大亮点,根据对象的生命周期长短,把堆分为3个代:Young,Old和Permanent,根据不同代的特点采用不同 的收集算法,可以扬长避短。
分区作用:
新创建的对象通常先将其分配在新生代中,在新生代中经过若干次GC之后仍未释放的对象,再将它移动到旧生代。为了让内存回收更高效(GC会暂停JVM中的应用),Sun JDK在1.2开始对堆采用了分代管理的方式。在分配对象遇到内存不足时,先对新生代进行GC(Young GC);当新生代GC之后仍无法满足内存空间分配需求时, 才会对整个堆空间以及方法区进行GC (Full GC)
Young(Nursery):年轻代
研究表明大部分对象都是朝生暮死,随生随灭的。所以对于年轻代在GC时都采取复制收集算法,具体算法参考下面的描述;
Young的默认值为 4M,随堆内存增大,约为1/15,JVM会根据情况动态管理其大小变化。
Young里面又分为3个区域:
一个Eden,所有新建对象都会存在于 该区
两个Survivor区,用来实施复制算法。
Eden区为对象通常最初分配到的地方,Survivor区分为S0和S1两块大小相等的区域。
JVM进行Minor GC时,将Eden中还存活的对象拷贝到Survivor区中,还会将Survivor区中还存活的对象拷贝到Old区中。在这种GC模式下,JVM为了提升GC效率, 将Survivor区分为S0和S1,这样就可以将对象回收和对象晋升分离开来。
-XX:NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2,但实际上young启动时远低于这个比率?如果信不过JVM,也可以用 -Xmn硬性规定其大小,有文档推荐设为Heap总大小的1/4。
-XX:SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为32。Survivio大了会浪费,小了的话,会使一些年轻对象潜逃到老人区,引起老人区的不安, 但这个参数对性能并不太重要。
Old(Tenured):年老代
年轻代的对象如果能够挺过数次收集,就会进入老人区。
老人区使用标记整理算法。因为老人区的对象都没那么容易死的,采用复制算法就要反复的复制对 象,很不合算,只好采用标记清理算法,但标记清理算法其实也不轻松,每次都要遍历区域内所有对象,所以还是没有免费的午餐啊。
-XX:MaxTenuringThreshold= 设置熬过年轻代多少次收集后移入老人区,CMS中默认为0,熬过第一次GC就转入,可以用-XX:+PrintTenuringDistribution 查看。
Permanent(Perm):持久代
装载Class信息等基础数据,默认64M,如果是类很多很多的服务程序,需要加大其设置-XX:MaxPermSize=,否则它满了之后会引起 fullgc()或Out of Memory。 注意Spring,Hibernate这类喜欢AOP动态生成类的框架需要更多的持久代内存。一般情况下,持久代是不会进行GC的,除非通过 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进行强制设置。
持久代也被成为方法区,方法区是全局共享的,在一定条件下也会被GC。
持久代存放JVM加载时的类型信息:
类型基本信息
ClassLoader引用
Class类引用
当每个代满了之后都会自动促发collection,各收集器触发的条件不一样,当然也可以通过一些参数进行强制设定。主要分为两种类型:
Minor Collection:GC用较高的频率对young进行扫描和回收,采用复制算法。
Major Collection:同时对Young和Old进行内存收集,也叫Full
GC;因为成本关系对Old的检查回收频率要比Young低很多,采用标记清除/标记整理算法。可以通过调用代码System.gc()引发major collection,使用-XX:+DisableExplicitGC禁止它,或设为CMS并发 -XX:+ExplicitGCInvokesConcurrent。
更为具体的阐述如下:
由于年轻代进进出出的人多而频繁,所以年轻代的GC也就频繁一点,但涉及范围也就年轻代这点弹丸之地内的对象,其特点 就是少量,多次,但快速,称之为Minor Collection。当年轻代的内存使用达到一定的阀值时,Minor Collection就被触发,Eden及某一Survior space(from space)之内存活的的对象被移到另一个空的Survior space(to space)中,然后from space和to space角色对调。当一个对象在两个survivor space之间移动过一定次数(达到预设的阀值)时,它就足够old了,够资格呆在年老代了。当然,如果survivor space比较小不足以容下所有live objects时,部分live objects也会直接晋升到年老代。
Survior spaces可以看作是Eden和年老代之间的缓冲,通过该缓冲可以检验一个对象生命周期是否足够的长,因为某些对象虽然逃过了一次Minor Collection,并不能说明其生命周期足够长,说不定在下一次Minor Collection之前就挂了。这样一定程度上确保了进入年老代的对象是货真价实的,减少了年老代空间使用的增长速度,也就降低年老代GC的频率。
当 年老代或者永久代的内存使用达到一定阀值时,一次基于所有代的GC就触发了,其特定是涉及范围广(量大),耗费的时间相对较长(较慢),但是频率比较低 (次数少),称之为Major Collection(Full Collection)。通常,首先使用针对年轻代的GC算法进行年轻代的GC,然后使用针对年老代的GC算法对年老代和永久代进行GC。
最小收集:
较高频率对年轻代进行扫描、回收
年轻代内存使用达到阀值
---&【触发Min GC】 Eden及from space内的存活对象移入to space
|【不足以容纳所有对象时,部分移入老人代】
---& from/to 角色对调 ---&【一个对象移动到一定次数】
移入老人代
最大收集:
同时对年轻代、年老代、永久代进行内存收集
1、年老代、永久代内存使用达到阀值
2、Yong GC后内存仍然不够分配时
GC收集算法
复制 (copying):将堆内分成两个相同空间,从根(ThreadLocal的对象,静态对象)开始访问 每一个关联的活跃对象,将空间A的活跃对象全部复制到空间B,然后一次性回收整个空间A。因为只访问活跃对象,将所有活动对象复制走之后就清空整 个空间,不用去访问死对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。可参考如下的示例图:
标记清除 (mark-sweep):收集器先从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个 内存区域,把所有没有标记活跃的对象进行回收处理。该算法遍历整个空间的成本较大暂停时间随空间大小线性增大,而且整理后堆里的碎片很多。可参考如下的示 例图:
标记整理 (mark-sweep-compact):综合了上述两者的做法和优点,先标记活跃对象,然后将其 合并成较大的内存块。可参考如下的示例图:
GC收集算法
1、复制 (copying)
将堆内分成两个相同空间,将空间A的活跃对象全部复制到空间B,然后一次性回收空间A
只访问活跃对象,所以遍历空间成本小,复制成本大
2、标记清除 (mark-sweep)
遍历第一次访问所有活跃对象并标记
遍历第二次回收所有未标记对象
遍历成本大,碎片多
空间越大暂停时间越多
3、标记整理 (mark-sweep-compact)
compact : 压紧、使紧凑
综合了上述两者的做法和优点,标记清理后合并活跃对象成较大的内存块
成本高,但不产生碎片
并行、并发的区别
并行(Parallel)与并发(Concurrent)仅一字之差,但体现的意思却完全不同,这可能也是很多同学非常困惑的地方,要想深刻体会这 其中的差别,可以多揣摩下上面关于GC收集器的示例图;
并行:指多条垃圾收集线程并行,此时用户线程是没有运行的;
并发:指用户线程与垃圾收集线程并发执行,程序在继续运行,而垃圾收集程序运行于另一个个CPU上。
并发收集一开始会很短暂的停止一次所有线程来开始初始标记根对象,然后标记线程与应用线程一起并发运行,最后又很短的暂停一次,多线程并行的重新标 记之前可能因为并发而漏掉的对象,然后就开始与应用程序并发的清除过程。可见,最长的两个遍历过程都是与应用程序并发执行的,比以前的串行算法改进太多太 多了!!!
串行标记清除是等年老代满了再开始收集的,而并发收集因为要与应用程序一起运行,如果满了才收集,应用程序就无内存可用,所以系统默认 68%满的时候就开始收集。内存已设得较大,吃内存又没有这么快的时候,可以用-XX:CMSInitiatingOccupancyFraction= 恰当增大该比率。
年轻代的痛
由于对年轻代的复制收集,依然必须停止所有应用程序线程,原理如此,只能靠多CPU,多收集线程并发来提高收集速度,但除非你的Server独占整 台服务器,否则如果服务器上本身还有很多其他线程时,切换起来速度就..... 所以,搞到最后,暂停时间的瓶颈就落在了年轻代的复制算法上。
因 此Young的大小设置挺重要的 ,大点就不用频繁GC,而且增大GC的间隔后,可以让多点对象自己死掉而不用复制了。 但Young增大时,GC造成的停顿时间攀升得非常恐怖,据某人的测试结果显示:默认8M的Young,只需要几毫秒的时间,64M就升到90毫秒,而升 到256M时,就要到300毫秒了,峰值还会攀到恐怖的800ms。谁叫复制算法,要等Young满了才开始收集,开始收集就要停止所有线程呢。
====================================================
幸存者0区(Survivor 0 space)和幸存者1区(Survivor1 space):
当伊甸园的空间用完时,程序又需要创建对象;此时JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁工作。同时将伊甸园中的还有其他对象引用的对象移动到幸存者0区。幸存者0区就是用于存放伊甸园垃圾回收时所幸存下来的JAVA对象。
当将伊甸园中的还有其他对象引用的对象移动到幸存者0区时,如果幸存者0区也没有空间来存放这些对象时,JVM的垃圾回收器将对幸存者0区进行垃圾回收处理,将幸存者0区中不在有其他对象引用的JAVA对象进行销毁,将幸存者0区中还有其他对象引用的对象移动到幸存者1区。幸存者1区的作用就是用于存放幸存者0区垃圾回收处理所幸存下来的JAVA对象。
养老区(Tenure (Old) generation space):用于保存从新生区筛选出来的JAVA对象。
垃圾回收描述:
垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
当一个URL被访问时,内存申请过程如下:
A. JVM会试图为相关Java对象在Eden中初始化一块内存区域
B. 当Eden空间足够时,内存申请结束。否则到下一步
C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收);释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区/OLD区
D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”
对象衰老过程:
1. young generation的内存,由一块Eden和两块Survivor Space构成。新创建的对象的内存都分配自eden。两块Survivor Space总有会一块是空闲的,用作copying collection的目标空间。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。所谓survivor,也就是大部分对象在Eden出生后,根本活不过一次GC。对象在young generation里经历了一定次数的minor collection后,年纪大了,就会被移到old generation中,称为tenuring。
2. 剩余内存空间不足会触发GC,如eden空间不够了就要进行minor collection,old generation空间不够要进行major collection,permanent generation空间不足会引发Full GC。
那么如何从jstat的输出来诊断应用程序是否有内存泄露的问题呢?下面列出几个经验总结(其实这才是本文的重点)
如何判断应用程序是否有内存的问题:
1. Full GC的频率,时长和效果: 如果Full GC频率较高,比如数秒一次,那么此时程序可能就已经出问题了,因为jvm在Full GC的时候是不响应外部请求的。
如果Full GC时间较长,比如持续数秒,那么此时程序可能就已经出问题了,因为jvm在Full GC的时候是不响应外部请求的。
如果Full GC之后old 区内存没有显著增加,那么程序很可能有内存泄露问题,并且不久将来可能出现outofmemory异常。
如果young gc和full gc能够正常发生,且都能有效回收内存,常驻内存区变化不明显,则说明java内存释放情况正常,垃圾回收及时,java内存泄露的几率就会大大降低。但也不能说明一定没有内存泄露。
2. GC的频率,时长和效果: 如果JVM进行内存回收的频率非常高,比如几乎每数秒中就有一次,每次回收的时间为数秒钟;并且,通过输出还发现每次回收释放的内存非常有限,大多数对象都无法回收。这种现象很大程度上暗示着内存泄漏。(此时可以用“jmap”来获得当前的一个内存映象,看看哪些对象导致这个问题来找出原因)
如果每次GC时间特别长,比如说数十秒,那这种现象很大程度上暗示着内存泄漏。(内存中对象太多,导致遍历时间太长,有时候不好的缓存机制会造成这样的问题)
3. 常驻内存区(P)的使用率:
常驻内存如果在应用程序稳定运行较长一段时间后还在持续增长,或者在某段某几段时刻有突变,则有可能有内存问题。(当然很大可能是jvm/app server配置问题) 如果P始终停留在某个值,说明常驻内存没有突变,比较正常。
下载次数: 14
下载次数: 12
下载次数: 22
下载次数: 25
浏览: 3958063 次
来自: 一片神奇的土地
[color=red][/color]dfsdsd[color ...
dd发的发给好友
dddddddddddddddd
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'他的最新文章
他的热门文章
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)2415人阅读
Java基础知识梳理(10)
心得:Java中垃圾回收和内存可以实现高度的自动化,栈帧可以由JVM自动分配和回收,局部变量表和操作数栈也可以在编译时就确定好,堆中的内存分配和回收才是JVM关注的重点,JVM实现大多采用可达性分析来标记存活对象,什么时候标记?让用户线程主动跑到那些安全的地方(引用关系不变的时候,SafePoint和Safe Region),再由GC收集器来标记进行处理。
不同的垃圾收集器甚至可以决定堆的内存布局,比如G1的“化增为零”一方面借助Remember Set可以更细粒度的进行并发标记和回收。
分代是GC种重要的思想,对不同特点对象进行各自适合的回收策略,Minor GC一般是新生代采用Copying算法,“空间换时间”,也是因为新生代大部分对象“朝生夕死”;Full GC在老年代一般是Mark and Compact两个步骤,不用额外空间,但停顿长,可谓“用时间省空间”;
CMS和G1更是采用并行+并发的手段,但一个新的问题,就是并发期间的用户线程的内存开销(CMS和G1各有对应策略)和对象引用关系的变化,因此它们都有remark的过程。
总之,不同的场景用不同的技术,“知其然”的同时能够“知其所以然”才能在实际的场景下选择“对的”技术。
垃圾回收是一个复杂的系统问题,本人认识还是十分有限。。。
学习参考资料:
(1)《深入Java虚拟机》(第二版);
一个小栗子(我在知乎的提问)
一个问题Java中的对象到底占多少内存?
JVM规范也不能回答这个问题,因为它是一个公有设计;
The Java Virtual Machine does not mandate any particular internal structure for objects.
看过书的同学应该都知道,对象由对象头+实例数据+padding组成;我利用Instrumentation做了一个小小的实验,基于64位JDK 8的Hotspot:
public static void main(String[] args) {
System.out.println(ClassUtils.sizeOf(new Object()));
System.out.println(ClassUtils.sizeOf(new byte[0]));
System.out.println(ClassUtils.sizeOf(new byte[7]));
System.out.println(ClassUtils.sizeOf(new byte[_1MB]));
System.out.println(ClassUtils.sizeOf(new Integer(1)));
System.out.println(ClassUtils.sizeOf(new Byte("1")));
System.out.println(ClassUtils.sizeOf(new Character('a')));
因为对象的大小一定是8的倍数,可以看到Hotspot很节省的将类型指针和数组长度或者int,byte,char合并保存了,而64位的Hotspot中reference的长度仍然是4个字节,一些博客上有说成8个字节的。
1. 确定回收对象
引用计数和可达性分析(counting和tracing)
前者通过对象的引用计数器来记录被引次数,显然的一个问题是循环引用;Java采用的是可达性分析;
从GC Roots出发,延引用链对对象进行搜索,没有任何引用链和GC Roots相连的对象就被成为不可达的,被判定为可回收的对象;
GC Roots(方法区和栈中):
(1)虚拟机栈(栈帧中的局部变量表);
(2)本地方法栈JNI引用的对象;
(3)方法区中类静态属性;
(4)方法区中常量引用;
就像进程的状态不能由简单的运行和终止描述一样;引用也需要进行一步细分:
强引用:永远不会被回收掉的对象;
软引用:如果一次回收后,内存还是不足,才进行回收,如果再不够,OOM;
弱引用:发生GC时,无论内存是否足够都会被回收;
虚引用:程序不能引用到,但是被回收时可以收到一个通知;
一个很重要的应用就是缓存,在内存中缓存一定要注意防止内存泄漏,在Java Collection Framework中,容器在删除是都执行置空的操作;
另一个注意的是可以使用WeakHashMap作为缓存容器;如果不是WeakHashMap一定要控制数量和及时清除(Integer.valueOf等就控制了数量);
终结(finalize)
从对象的可触及性来说还有2个状态:
可复活和不可触及(两次标记):
在判定为不可达后:
(1)可复活:一次标记,对象分为没有必要执行finalize方法(包括没有覆盖和已经执行两种)和需要执行finalize方法,前者直接就可以被回收;
(2)有必要执行finalize的方法被放在F-Queue队列中,有JVM中一个低优先级的Finalizer线程去触发它们的finalize方法(不会等待方法结束),如果在finalize方法中对象有引用链建立了连接就会被“复活”,否则就Over;
一个对象的finalize方法只能被执行一次,也就是说一个对象甭想自救两次!
方法区中的回收
对于常量来说,没有任何东西引用,那么也是可以被回收的;
类的卸载:条件非常苛刻(JVM规范没有要求在方法区中实现垃圾回收,Hotspot中有但是类还是很难被卸载)
(1)该类所有的实例被回收;
(2)对应的ClassLoader被回收;
(3)对应的Class对象没有引用;
栈的回收是虚拟机静态分配和回收,栈帧的大小可以在编译时确定,JVM通过栈帧的分配和回收很容易(开销很低)就完成;
方法区的回收中类的卸载很棘手,对于大量用反射,动态代理,CGLIB等技术的程序,JVM要能够卸载类;
主要的一个问题就是堆中内存的分配和卸载;
2. 垃圾回收算法
标记-清除算法(Mark-Sweep)
(1)空间碎片;
(2)效率:标记和清除两个过程相对来说不高;
复制算法(Copying)
原理:两块内存来回复制,因为有一块空白的内存可以直接复制,因此不用再分Mark,Sweep或者Compact多个阶段了,空间换时间;
当然我们知道最后的设计是:一个Eden+两个较小的Survivor,这是由于Java中对象98%的新对象都可以被回收的统计数据得来的经验;
标记-整理算法
区别与“标记-清除”,整理指的是不再原来位置直接进行回收,而是存活的对象向一端移动,最后界限之外的部分直接清理掉;
根据不同对象的特点,采用分代的方式垃圾回收;
3. HotSpot的垃圾回收算法实现
什么时候回收垃圾,怎样尽量降低对用户线程的影响不同的业务需求对垃圾回收有什么不同的要求?
枚举根节点
一致性:进行引用链分析显然要基于一个一致性的快照,不能因为分析过程中引用关系变化而导致错误;
Stop the world:一个简单直接的办法,但是显然会产生停顿;
OopMap:为了避免进行全盘扫描,借助与OopMap这样的数据结构保存对象的被引用范围,告诉JVM哪些地方存折对象的引用;
安全点(节省开销,安全性)
定义:the thread’s representation of it’s Java machine state is well described
OopMap可以帮助快速的完成GC Roots枚举,但是显然并不能每条指令都带上OopMap;通过SafePoint的地方保存OopMap,运行到安全点上可以进行GC活动;
如果要触发一次GC,那么JVM里的所有Java线程都必须到达GC safepoint;
哪些地方可以选为SafePoint(不同的JVM实现可能不同):
因此防止“长时间运行”,而导致GC活动等待某个线程迟迟不能进入;
方法调用,循环跳转,异常跳转等:
(1)循环的末尾;
(2)方法返回之前;
(3)调用方法的call之后;
(4)抛出异常的位置;
PS:Java中用到SafePoint的地方
1. Garbage collection pauses;
2. Code deoptimization;
3. Flushing code cache
4. Class redefinition (e.g. hot swap or instrumentation)
5. Biased lock revocation
6. Various debug operation (e.g. deadlock check or stacktrace dump)
主动式中断:
设置标志,让工作线程主动轮询到标志进行挂起;
在JIT执行方式下:JIT编译的时候直接把safepoint的检查代码加入了生成的本地代码,当JVM需要让Java线程进入safepoint的时候,只需要设置一个标志位,让Java线程运行到safepoint的时候主动检查这个标志位,如果标志被设置,那么线程停顿,如果没有被设置,那么继续执行。
例如hotspot在x86中为轮询safepoint会生成一条类似于“test %eax,0x160100”的指令,JVM需要进入gc前,先把0x160100设置为不可读,那所有线程执行到检查0x160100的test指令后都会停顿下来;
在解释器执行方式下:JVM会设置一个2字节的dispatch tables,解释器执行的时候会经常去检查这个dispatch tables,当有SafePoint请求的时候,就会让线程去进行SafePoint检查。
VMThread会一直等待直到VMOperationQueue(消息队列)中有操作请求出现,比如GC请求。而VMThread要开始工作必须要等到所有的Java线程进入到SafePoint。
JVM维护了一个数据结构,记录了所有的线程,所以它可以快速检查所有线程的状态。当有GC请求时,所有进入到SafePoint的Java线程会在一个Thread_Lock锁阻塞,直到当JVM操作完成后,VM释放Thread_Lock(通知),阻塞的Java线程才能继续运行(STW)。
GC stop the world的时候,所有运行Java code的线程被阻塞,如果运行native code线程不去和Java代码交互,那么这些线程不需要阻塞。VM操作相关的线程也不会被阻塞。
PS:输出安全点统计信息
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
安全区域(Safe Region)
GC活动设置状态等待Java线程主动轮询到状态被Thread_Lock阻塞,但是如果处于阻塞状态的线程怎么办,它们已经被阻塞了,这就需要Safe Region;
相对的,Safe Region指在一段代码片段之后,引用关系不会发生变化;线程是BLOCKED那么它的引用关系就不会被修改,JVM可以安全进行标记;
过程:如果线程进入到Safe region时,首先标识自己进入了Safe region,在退出时(比如一个阻塞的线程被唤醒),先检查是否能够离开,如果GC已经完成,那么可以离开,否则等待直到GC完成;
4. 垃圾回收器
从实现框架上来看:
Serial,ParNew,Serial Old,CMS是一个分代式GC框架中的,可以任意的搭配;
Parallel Scavenge和G1有各自的框架,Parallel Scavenge不能和CMS搭配,只能和Serial Old(MSC,MarkSweepCompact),然后有了Parallel Old(标记-整理,PS Mark Sweep);
这个PS MarkSweep默认的实现实际上是一层皮,它底下真正做mark-sweep-compact工作的代码是跟分代式GC框架里的serial old(这个collector名字叫做MarkSweepCompact)是共用同一份代码的。也就是说实际上PS MarkSweep与MarkSweepCompact在HotSpot VM里是同一个collector实现,包了两张不同的皮;一个并行,一个串行。
从串行,并行,并发执行方式看
串行的有:Serial,Serial Old, CMS的Concurrent Mode Fail情况(使用Serial Old);
并行的有:ParNew,Parallel Scavenge,CMS的remark,G1的最终标记,筛选回收(也可并发);
并发的有:CMS的concurrent mark,G1;
从性能特点和适用情景看
Serial:简单,快捷,适合与内存不大(几十上百MB)的client模式;
Parallel Scavenge:从吞吐量的角度控制堆的划分和GC活动;
CMS:目标也是低停顿,在停顿控制上不如G1,但是如果在停顿都接受可以接受的范围内,吞吐量ParNew+CMS的组合可能要比G1更好;
G1:G1的首要目标是为需要大量内存的系统提供一个保证GC低延迟的解决方案,也就是说堆内存在6GB及以上,稳定和可预测的暂停时间小于0.5秒;
如果应用程序具有如下的一个或多个特征,那么将垃圾收集器从CMS或ParallelOldGC切换到G1将会大大提升性能.
(1)Full GC 次数太频繁或者消耗时间太长;
(2)对象分配的频率或代数提升(promotion)显著变化;
(3)受够了太长的垃圾回收或内存整理时间(超过0.5~1秒);
CMS收集器(低停顿,B/S系统,侧重响应速度)
(1)初始标记(initial mark,停顿);
(2)并发标记(concurrent mark);
(3)重新标记(parallel remark,停顿);
(4)并发清除(concurrent sweep);
(5)重置(reset,清理数据结构,为下次并发收集做准备);
(1)对CPU资源敏感,很简单,因为它使用了并发,在CPU核数较少的机器上会对用户线程影响较大;
(2)浮动垃圾,并发标记的过程可能会产生新的垃圾,这一部分垃圾只能在下一次GC进行清理;并且垃圾回收阶段也是并发的,必须为用户线程预留一些内存空间,因此:
JDK 1.5老年代68%,激活CMS,JDK 1.6 默认为92%;
-XX:CMSInitiatingOccupacyFraction设置;
如果预留空间无法满足,造成“Concurrent Mode Failure”,临时使用Serial Old进行回收;
(3)碎片,通过-XX:+UseCMSCompactAtFullCollection开启在要FullGC前进行异常整理;-XX:+CMSFullGCsBeforeCompaction可以设置多少次不压缩的FullGC之后来一次带压缩的FullGC(默认为0);
G1收集器(Garbage First,可预测的低停顿,服务器,化整为零)
将整个Java堆划分成多个大小相等的独立区域(Region),新生代和老年代分布在这些region上(可以不连续);
G1并不是实时垃圾收集器:基于以前收集的各种监控数据,G1会根据用户指定的目标时间来预估能回收多少个heap区。因此,收集器有一个相当精确的heap区耗时计算模型,并根据该模型来确定在给定时间内去回收哪些heap区。
(1)并行与并发;
(2)分代收集:虽然不像其他收集器老年代和新生代是物理隔离的,但是老对象,仍然会采用不同的方式处理;
(3)空间整合:从整体上看是“标记-整理”,从局部上来看是“复制”,不会产生空间碎片;
(4)可预测的停顿:建立了可预测的停顿时间模型(并且后台维护了一个优先列表),可以通过参数控制在M毫秒的时间段内,GC消耗的时间不得超过N毫秒;
G1如何进行垃圾回收的:
后台维护一个优先列表,每次回收先会回收GC价值最大的region;
问题:G1如何保证各个Region在各自回收时和其他region对象之间引用关系被正确处理?
Remembered Set,记录其他region对自己region中对象的引用记录(write barrier+CardTable),以避免扫描全局;
G1的回收步骤:
(1)初始标记(Initial mark);
(2)并发标记(Concurrent mark);
(3)最终标记(final mark,将并发过程对象变化的Rememered Set log合并,将并发标记中的空区域回收,计算所有区域的活跃度(live的程度);
(4)筛选扫描(live data counting and evacuation,拷贝和回收);
G1中的转移失败(Evacuation Failure)):
对Survivor或promoted Objects进行GC时如果JVM的heap区不足就会发生提升失败(promotion failure),堆内存不能继续扩充,因为已经达到了最大值,日志输出to-space overflow;
也就是说GC的效率赶不上空间的消耗导致“碰到天花板”了;
解决(加快GC的速度,增加对内存):
-XX:G1ResrvePercent:保留内存,也就是来个“假天花板”;
-XX:ConcGCThreads=n增加标记线程数量;
5. 对象分配与回收策略
注意启动了本地线程分配,按线程优先分配在TLAB上;
优先在Eden分配
大对象直接分配在老年代
通过-XX:PretenureSizeThreshold控制;
防止提前触发minor GC,减少新生代的复制;
长期存活的对象将进入老年代
根据Age计数器大小,设置-XX:MaxTenuringThreshold设置;
动态对象年龄判断
如果survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold设置的大小;
空间分配担保
在Minor GC前(关键是Old最大可用连续大小):
Created with Rapha?l 2.1.0Start大于New对象总和?HandlePromotionFailure?大于晋升平均大小?Minor GC成功minorGC?EndFull GCMinor GCyesnoyesnoyesnoyesno
问题和总结
1. Minor GC,Major GC和Full GC的区别和联系?
首先这些术语在Java语言规范和JVM规范中都没有正式的定义,但是Hotspot中使用了它们,并在日志中进行输出,在分析问题时也不能够简单的用“Full GC发生次数多少”来作为一个标准,比如一次CMS GC产生了两次STW(initial mark和remark)因此在日志中会输出为2次Full GC,但是CMS的STW时间一般很短。
总之,这些概念只是辅助标识,关键还是监控延迟或者吞吐量,结合GC分析导致问题的原因。
Minor GC是发生在新生代的,基于Serial(DefNew),ParNew,Parallel Scavenge(PS Yong)都是STW的,大多数年轻代中对象都进不了老年代,也就是说能挺到一次Minor GC的对象并不多,这也为什么大部分年轻代GC都使用Copying,一个大的Eden和两个小的Survivor;
如果Minor GC的时间很长,可能是因为新生代存活的对象太多了,都要进行复制,超过了Survivor区的大小的话,要进入老年代,这就有关空间担保分配了。
Major GC和Full GC可以看作是相同的意思,对老年代/永久代进行垃圾回收,因为一些历史原因,这两个概念的定义也挺混乱的,纠结这两个概念并没有什么意义。
Full GC:在Serial GC(UseSerialGC)、Parallel GC(UseParallelGC)中,只有Full GC才会收集老年代(实际收集整个GC堆,包括老年代在内),使用Mark-Compact算法;
Full GC的次数等于老年代GC时STW的次数,时间为STW总时间;
对于CMS收集器来说,Full GC只是一次CMS两个阶段或者在担保失败的情况下用Serial Old来代替了,因此也不能简单Full GC的情况来分析。
Minor GC和Full GC的联系:从一般的编程习惯来看,老的对象引用新创建的对象的情况要多于新对象引用老的对象,因此老年代中的GC一般要从年轻代的引用链开始分析,故而可以设置Full GC进行一次Minor GC,来提高老年代GC的速度。
2. GC和Stop The World(STW):
GC总是会发生停顿,也是“Stop the World”,问题是停顿时间的长短,从这个角度上看,不同GC算法是在努力减少停顿的同时权衡对吞吐量影响或者其他开销。并行是为了利用多核CPU来缩短停顿的时间总量,并发是为了尽可能找出那些可以并行的部分,是进一步利用多核CPU将任务细化,减少STW,G1更是添加了预测模型来控制(尽可能)停顿的时间。
从具体的实现算法来看,Copying和Compact的过程需要移动对象,因此在整理内存阶段需要暂停用户线程。
同步用户线程和GC活动有两种方式,一是read barrier,而是write barrier,前者的开销更大,很少有GC使用read barrier,如果使用write barrier那么在“移动对象”必须要暂停用户线程,从而产生STW。
基于下面的列表,Serial,ParNew,PS,PS old要么是Copying,要么是Mark-Compact,当然它们也都是STW的。
CMS采用了以Mark-Sweep为主的方式,因此可以并发标记和并发重置;
G1从整体上来是Mark-Compact的,局部(region之间)是复制的,但是它是把内存分成一个个region来处理的,可以做到每次compact一部分,而不像Serial等是一口气Compact老年代,因此也可以缩短STW,甚至实现增量式/并发;
Serial:单线程STW,Copying;
Serial Old:单线程STW,Mark&Compact;
ParNew:多线程并行STW,Copying;
Parallel Scavenge:多线程并行,Copying;
PS Old:并行STW,Mark&Compact;
CMS:多线程并发/并行,initial mark,remark是STW,Mark&Sweep/Mark&Compact;
G1:多线程并发/并行,initial mark,remark,Mark&Compact;
3. CMS为什么不采用Mark&Compact而是Mark&Sweep?
CMS在老年代上工作,采用的是Mark&Sweep,不直接使用Mark&Compact, 而是通过-XX:+UseCMSCompactAtFullCollection和-XX:+CMSFullGCsBeforeCompaction两个参数来决定什么时候采用一个压缩,这可以说是一种混合的方式;
使用Mark&Sweep的考虑有:
(1)老年代中一个传统的假设是对象的存活率比较高,我们可以以通过相关的参数控制进入老年代对象的大小和年龄(也就是说进入老年代的对象本来就是经过一次或多次筛选的)。基于这样的场景,使用Copying算法显然是不划算的;
(2)另外一个考虑就是,Mark-Compact和Copying都是要移动对象的,因此还需要修改引用链中的直接引用的地址值,这对于并发重置的CMS来说显然是一个更为复杂的问题,在Mark-Sweep模式下,不需要修改所有指针,因此也不需要暂停用户线程,从而实现并发;
因此,CMS使用这种以Mark-Sweep为主,Mark-Compact为辅的GC方式是一种基于场景的折中方案;}

我要回帖

更多关于 marksweep scavenge 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信