深入理解Java虚拟机笔记(2)

1.1 概述

    GC需要考虑的事:哪些内存需要回收,什么时候回收,如何回收

    我们只要在程序出于运行期间的时候才会知道创建哪些对象,这部分内存和回收都是动态的,垃圾回收器所关注的就是这部分内存

1.2 对象已死吗?

    回收的前提就是判断对象是否还存活

1.2.1 引用计数算法

    给一个对象添加一个计数器,若它被引用则计数器+1,当引用失效的时候就-1,当计数器为0的时候,该对象就被认为就是不可能再被使用的

    客观来说,引用计数法,比较简单,判断效率也高,但主要的问题是它难以解决互相循环引用的问题,而且,引用伴随着加减操作,可能会影响性能

1.2.2 可达性分析

    现在的主流虚拟机都是通过可达性分析来判断对象是否存活的,换言之,从root出发没有任何引用连接就证明此对象是不可用的

    GCRoots对象包括这几种:

    虚拟机栈(栈帧中的本地变量表)中引用的对象

    方法区中类静态属性的引用的对象

    方法区中的常量池引用的对象

    本地方法栈中的JNI(一般来说是Native方法)引用的对象

1.2.3 再谈引用

    不论是通过判断引用数量,还是可达性分析,对象的存活都与“引用”有关。引用大致有:强引用,软引用,弱引用,虚引用

    强引用:就是指在程序中普遍存在的,类似于Object object = new Object(),不会被回收

    软引用:用来描述一些还有用但并非必须的对象,内存溢出之前会回收这些对象,如果内存还是不够,则会发生内存溢出

    弱引用:也用来描述一些非必须对象,只生存到一下次回收之前

    虚引用:又称幽灵引用或者幻影引用,最弱的引用。一个对象是否有虚引用,不对其存活时间产生影响

1.2.4 生存还是死亡

    再可达性算法中,存在一种不可达,但未来可能可达的对象,我们称为“缓刑阶段”的对象,筛选条件是它是否重写了finalize()方法,当对象没有finalize()方法时或者这个方法已经被调用一次了,就判断为“没有必要执行”。

    值得注意的是,finalize()方法只会被系统调用一次,而且我们不鼓励大家去用这个方法,因为它能做的事,try-finally都可以去完成,建议大家忘掉这个方法

1.2.5 回收方法区

    永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类,废弃常量比较好判断,比方说"abc"这个常量若没有String对象去引用,那么就认为这个常量是废弃常量

    判定一个废弃类要比判断一个废弃常量苛刻的多,要同时满足下面三个条件:

      -该类的实例已经全被回收,Java堆中不存在任何实例

      -加载该类的ClassLoader已经被回收

      -该类对应的java.lang.Class对象没有再任何对象被引用,确保没办法通过反射去访问这个类

    满足这三个条件仅仅只是说可以回收,并不代表一定回收

1.3 垃圾回收器

1.3.1 标记清除算法

    是现代垃圾回收算法的思想基础。标记清除分两个阶段,标记阶段,从根节点出发可达对象标记,未标记就是未引用的垃圾对象。清除阶段,清除没有标记的对象,但它存在问题:一是,效率问题,标记和清除的效率都不高,二是内碎片问题,它所清除后的空间是不连续的

1.3.2 复制算法

    与标记清除法相比,是一种高效的算法

    不实用于存活对象多的场合,如老年代

    将原有的内存空间分成两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制,清除原所有的空间,交换两个空间

    问题:浪费空间,必须有空间做担保尤其是大对象

    现代大部分虚拟机新生代都是采用这种算法,他们把内存分为一个Eden区和两个suriver区比例8:1

1.3.3 标记整理法

    同标记清除算法思想,标记阶段相同,清除阶段,它会将标记的全部放到一段,清除边界外所有空间。相对于标记清除算法,它最后产生的可以用的空间是连续的

1.3.4 分代收集算法

    一般是把Java堆分为新生代和老年代,这样就可以根据各个时代的特点采用适当的收集算法。在新生代中经常有大批对象死去,所以采用复制算法。老年代因为对象存活率高采用标记清理或者标记整理

1.4 HotSpot的算法实现

1.4.1 枚举根节点

    可达性分析从GCroots节点找引用,那么必然要逐个检查这里面的引用,那么必然会消耗很多时间

    另外,可达性分析对执行时间的敏感还体现在GC停顿上,意思是,在整个分析期间,必须冻结在某个时间点上,不能说还存在着动态变化的情况,不然结果的准确性就无法保证。这件事又被称为“Stop The World”。虚拟机应该要知道,哪些地方存放着对象的引用,在HotSpot中,是使用一组称为OopMap的数据结构来达到目的的

1.4.2 安全点

    在OopMap的帮助下,HotSpot可以快速且准确的完成GCroots枚举,但是如果为每一条指令都生成一个OopMap,一定会浪费许多空间

    实际上,也并没有为每条指令都生成。只有在特定的安全点才会,只有在安全点时才能暂停,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准的。其最明显的特征就是指令序列的复用,比方说:方法调用,循环,异常跳转等等

    还有一个问题需要考虑,就是在发生GC时,如何让所有的线程都“跑”到安全点。这里有两种方式:抢先式中断(基本已经废弃)和主动式中断。主动式中断的思想是设置一个标志,各个线程执行时去轮询。

1.4.3 安区区域

    安全区域值得是在一段代码里,引用关系不会发生变化,在这个区域中的任何地方都是安全的。我们可以把Safe Region看做SafePoint的扩展版

    在线程执行到Safe Region中,首先标识自己已经进入了Safe Region,那样,当JVM发起GC的时候就认为是安全的,当线程离开时,它要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续执行,否则他必须等直到回收可以安全离开时

1.5 垃圾回收器

    如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,至今为止,没有最好的收集器,更加没有万能的,只有适合的

1.5.1 Serial 收集器

    这个收集器是一个单线程的收集器,采用复制算法,这个单线程有两层含义,一是,运行时单线程,而是,运行时“Stop The World”。优点,高效,尤其是单CPU下,缺点,“Stop The World”,体验很差。

1.5.2 ParNew 收集器

    多线程版本的Serial收集器,除了使用多线程进行收集之外,其余行为包括Serial收集器可以用的所以控制参数,大部分情况下首选的新生代收集器。还有一个与性能无关的原因,只有它和Serial能和CMS收集器配合工作
    单线程下,没啥用,性能还不如Serial,因为有线程交换所带来的开销

1.5.3 Parallel 收集器
    一个新生代收集器,也是复制算法,也是多线程。。。它的特点在于它关注的是吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间)),良好的速度能提升用户体验,高吞吐量可以高效利用cpu,但是尴尬的就是两者不可兼得

    另外Parallel收集器有一个需要注意的地方,就是一个参数-XX:+UseAdaptiveSizePolicy,这个称为GC自适应的调节策略。如果不会手工的去优化,那么使用它的时候就可以配合自适应调节。

1.5.4 Serial Old 收集器

    Serial收集器的老年版本,采用标记整理算法,它的意义也是给Client模式下的虚拟机使用

1.5.5 Parallel Old 收集器

    Parallel收集器的老年版,使用多线程和标记整理算法。新生代的Parallel一直处于尴尬的状态,因为新生代选了Parallel,老年代只有Serial,这个组合效率不咋地。老年版出现后“吞吐量优先”终于有了更好的组合(新+旧)

1.5.6 CMS 收集器
    CMS收集器是一种以获取最短的回收停顿时间为目标的收集器,CMS是基于“标记-清除”算法实现的,它主要包括四个步骤:

      - 初始标记:需要“Stop The World”,只是简单标记一下GCRoots能直接关联到的对象,速度很快

      - 并发标记:需要“Stop The World”,就是进行GCRoots Tracing的过程

      - 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象进行标记

      - 清除过程:回收过程和用户线程是一起并发执行的

    CMS收集器对CPU资源十分的敏感,当CPU在4个以上时,并发回收垃圾收集线程不少于25%的资源,并且随着CPU数量下降而下降,当CPU特别少时,CMS对用户的程序影响就非常大

    CMS无法处理浮动垃圾(浮动垃圾:出现在标记过程之后,CMS当次无法集中处理掉他们,只能等待下次去处理),主要还是因为垃圾回收线程和用户线程并发执行。而且CMS需要预留一部分的空间提供并发收集时使用,JDK1.5默认设置下,当老年代使用了68%的时候,CMS就会被激活。这个数字十分保守,可以通过-XX:CMSInitatingOccupancyFraction的值来提高触发百分百

    当然CMS也有一定的缺点,既然是标记-清除算法,那么回收后所产生的空间就不是连续的,存在碎片。为解决这个问题,CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数,用于CMS在Full GC时开启内碎片整理。还有另一个参数-XX:CMSFullGCsBeforeCompaction用于多少次Full GC后去整理碎片

1.5.7 G1 收集器

    特点:并发与并行,能充分利用多CPU,多核环境下的硬件优势,使用多个CPU来缩短Stop The World的时间。与其他CPU一样,也是采用分代的思想。空间整合,G1在整体上采用的是标记-整理算法,但从局部(两个Region之间)上看是基于复制算法的,不论如何G1是不会产生内碎片的。可预测的停顿,G1可以让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒

    G1之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆的价值大小,所获得的空间大小以及回收所需要时间的经验值,G1在后台维护一个优先列表,优先回收价值大的

    G1的过程大概可以分为以下几个步骤:

      - 初始标记:简单标记一下GCRoots能直接关联的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户能在正确的Region中创建对象

      - 并发标记:从GCRoots开始做可达性分析,找出存活对象,这个阶段时间比较长

      - 最终标记:为了修正在标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录

      - 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

1.5.8 理解GC日志

    每一种虚拟机的日志模式都不一样,但是都维持着一些共性,下面举例:

    33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K),0.0031680 secs]

    100.667: [Full GC [Tenured: OK->210K(10240K), 0.0149142 secs] 4630K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times:user=0.01 sys=0.00, real=00.02 secs]

    最前面的33.125 和 100.667 代表了GC发生的时间,这个数字的含义是从虚拟机启动经过的秒数

    GC 和 Full GC是垃圾回收的类型,如果有Full说明发生了STW

    接下来的"[DefNew", "[Tenured", "[Perm" 表示GC发生的区域

    后面方括号内部的3324K->152K(3712K)含义是已经使用的->发生GC后的已使用的(总内存容量),方括号之外的表示的是Java堆的相应情况

    之后的0.0025925 secs是GC所用时间,单位是秒

1.6 内存分配与回收策略

    Java提倡自动内存管理,那么就需要解决两个问题:给对象分配内存和回收已经分配的

对于给对象分配内存,大方向上讲,就是在堆上分配内存,对象主要分配在新生代的Eden区,当然具体分配情况还取决于当前使用的是哪一种垃圾收集器的组合

1.6.1 对象优先在Eden分配

    大多数情况,对象在新生代的Eden区分配。当Eden区没有足够的空间时,虚拟机将发生一次Minor GC

1.6.2 大对象直接进入老年代

    所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对于虚拟机来说是个坏消息,我们应该避免创建“朝生夕灭”的短命大对象

    虚拟机提供了一个参数-XX:PertenureSizeThreshold参数,令大对象大于某个阈值后就直接进入老年代,避免了大量的内存复制

1.6.3 长期存活的对象将进入老年代

    为了做到这一点,需要给对象一个年龄计数器Age,当Survivor区中每熬过一次Minor GC年龄就+1, 当年龄增加到一定程度就会被晋升到老年代(默认15), -XX:MaxTenuringThreshold=xx 可以来设置年龄的阈值

1.6.4 动态对象年龄判定

    如果在Survivor空间中相同年龄所有的对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold要求的年龄

1.6.5 空间分配担保

    在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailue(1.8之后貌似这个参数废了了)设置的值是否允许担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次的平均大小,如果大于,将尝试一次Minor GC,尽管这次GC是有风险的,如果小于或设置不允许冒险就进行一次Full GC