JVM垃圾收集器与内存分配策略

判断对象是否存活
在进行GC之前,首先要确定的就是在java堆中那些对象已经“死去”那些对象还“活着”
引用记数法(Referencecounting)
了解即可,虚拟机并不是通过该算法来判断对象是否存活。
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
存在的问题:很难处理对象的循环引用。根对象已经不可达,垃圾对象的相互引用造成垃圾对象的引用计数都不为1,所以就不会被回收。
可达性分析算法(ReachabilityAnalysis)
主流虚拟机通过该算法来判断对象是否存活。
GCRoots(一组必须活跃的引用对象)的对象为起点,如果从GCroots到这个对象不可达,就说明该对象是不可用的,被判定为可回收的对象
JVM垃圾收集器与内存分配策略
可作为GCRoots的对象:
虚拟机栈中应用的对象,方法区中类静态属性应用的对象,方法区中常量引用的对象,Native方法应用的对象。
对象的自救
在可达性分析算法中“不可达”的对象,并不是“非死不可的”。暂时处于“缓刑”。
一个对象如果被标记为“不可达”,接下来将会进行筛选,即判断该对象是否有必要执行fianlize方法,如果对象中没有覆盖finalize方法,或者finalize方法已经被JVM调用过了(任何一个对象的finalize方法只会被系统自动调用一次)。则判定没有必要执行finalize方法。判定对象死亡。
finalize方法是对象逃脱死亡的最后一次机会。
如果对象被判定为有必要执行finalize方法,只要在finalize方法中重新与引用链上任意一个对象建立关联(比如把自己(this关键字)赋值给一个变量)。对象就会自救成功。
JVM垃圾收集器与内存分配策略 
垃圾收集算法
标记—清除算法(Mark-Sweep)
标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
JVM垃圾收集器与内存分配策略
该算法有两个不足:
一是效率,标记和清除两个阶段效率都不高。
二是空间问题,标记清除后会产生大量不连续的内存碎片,这样以后在位大对象分配空间时,由于找不到足够的连续内存会提前出发下一次GC
标记—整理算法(MarkCompact)
标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。
和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象移动到内存的一端。之后,清理边界外所有的空间。
JVM垃圾收集器与内存分配策略
复制算法(Copying)
与标记-清除算法相比,复制算法是一种相对高效的回收方法。
不适用于存活对象较多的场合,如老年代。
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
JVM垃圾收集器与内存分配策略
 
JavaGC、新生代、老年代
Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为
三个区域:EdenFrom SurvivorTo Survivor
这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。
JVM垃圾收集器与内存分配策略
 
从图中可以看出:堆大小=新生代+老年代。其中,堆的大小可以通过参数–Xms-Xmx来指定。
默认的,新生代(Young)与老年代(Old)的比例的值为1:2(该值可以通过参数–XX:NewRatio来指定,即:新生代(Young)=1/3的堆空间大小。老年代(Old)=2/3的堆空间大小。其中,新生代(Young)被细分为Eden和两个Survivor区域,这两个Survivor区域分别被命名为fromto,以示区分。
默认的,Eden:from:to=8:1:1(可以通过参数–XX:SurvivorRatio来设定),即:Eden=8/10的新生代空间大小,from=to=1/10的新生代空间大小。
JVM每次只会使用Eden和其中的一块Survivor区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为9/10(90%)的新生代空间。
JVM垃圾收集器与内存分配策略
 
分代收集算法(Generational Colletion)
依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。
根据不同代的特点,选取合适的收集算法。
少量对象存活,适合复制算法
大量对象存活,适合标记清理或者标记整理算法。
内存分配及回收策略
对象优先在Eden(伊甸园)分配:大多数情况下对戏在新生代Eden中分配,当Eden区中没有足够的空间时,虚拟机将发起一次Minor GC
大对象直接进入老年代:所谓大对象就是指那些需要大量连续内存空间的Java对象,最典型的大对象就是那些很长的字符串以及数组。
长期存活的进入老年代:虚拟机给每个对象都定义了一个年龄计数器。对象在Eden出生,经过一次Minor GC还存活。对象的年龄就加一,当它的年龄增加到一定程度时(默认是15),就会被晋升到老年代。
 
新生代GC(Minor GC):指发生在新生代的GC动作,因为大多数Java对象都具有朝生夕灭的特点,所以Minor GC发生较为频繁,回收速度页比较快。
 
老年代GC(Major GCFull GC):指发生在老年代的GC,出现了Major GC,至少会伴随一次Minor GCMajor GC的速度比Minor GC慢10倍以上。
 
内存溢出和内存泄漏
内存溢出out of memory(memory overflow),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory.
泄漏,什么是泄漏?我举个简单的例子,不知道是不是这个意思,就比如说有人跟你关系不错,找你借了点钱,但是后来他搬家了,新地址你不知道,你想找他要钱回来,但是就是找不到他在什么地方。专业点的话就是说你向系统申请到了你想要的内存空间,但是使用完了之后却不归还,结果你申请到的内存空间你自己也访问不到(也许你把地址搞丢了),系统也无法分配该空间给其他的程序。这就是一次泄漏.
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露的堆积会造成内存溢出。
Java内存泄露根本原因是什么呢长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景
 
造成的原因有:1、静态集合类引起内存泄漏:2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。3、监听器。4、各种连接。5、内部类和外部模块的引用。
垃圾收集器
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除”算法实现,它的运作过程如下:
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
初始标记、重新标记这两个步骤仍然需要stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,
并发标记阶段就是进行GC Roots Tracing的过程
而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。
在整个过程中耗时最长的的并发标记和并发清除过程收集器线程都可以和用户线程一起工作,所以从总体上说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
JVM垃圾收集器与内存分配策略
 
CMS是一款优秀的收集器,主要优点:并发收集、低停顿
缺点:
1CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致另一次Full GC产生。
浮动垃圾(Floating Garbage:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。
3CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC
G1收集器
G1(Garbage-First)收集器是当今集线器技术发展的最前沿成果之一。G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:
1、并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPUCPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
 
G1收集器的运行步骤
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)最终标记(Final Marking)
4)筛选回收(Live Data Counting and Evacuation)
上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
JVM垃圾收集器与内存分配策略