美文网首页
深入理解Java虚拟机-垃圾收集器及内存分配策略

深入理解Java虚拟机-垃圾收集器及内存分配策略

作者: 那些年未曾努力过 | 来源:发表于2017-09-30 13:02 被阅读0次
    • 哪些内存需要回收
    • 什么时候回收
    • 如何回收

    如何判断对象是否存活

    可达性分析算法(Reachability Analysis): 通过一系列"GC Roots"对象作为起始点,从这些节点开始往下搜索,搜索过的路径称为引用链(Reference Chain),当任意一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

    GC Roots对象包括以下几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态变量属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(本地Native方法)引用的对象

    引用包括:

    • 强引用:Java代码中类似Object obj = new Object()这类的引用。只要有强引用在,垃圾收集器永远不会回收掉被引用的对象
    • 软引用:用来描述一些还有用但并非必要的对象。对于软引用关联着的对象,在系统发生内存溢出之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 弱引用:比软引用更弱一些,只能存活到垃圾收集活动发生之前。
    • 虚引用:它是最弱的一种引用关系,它存在的目的就是能在这个对象被收集器回收时收到系统通知。

    对象的死亡

    在可达性分析算法中不可达的对象,至少要经历两次标记的过程,才确定是否被回收:

    1、如果对象在可达性分析后发现没有与GC Roots相连的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法时,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

    2、如果对象有必要执行finalize()方法,那么这个对象将会被放在一个F—Queue队列之中,虚拟机的finalizer线程会执行它

    3、finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue队列中的对象进行第二次小规模的标记,如果对象成功在finalize()中拯救自己(建立与引用链的联系),那在第二次标记时它将被移除出“即将回收”的集合;否则它就要被回收。

    方法区的回收(永久代)

    方法区垃圾收集的效率是比较低的,收集的内容主要有两部分:废弃的常量和无用的类

    无用的类需要符合三个条件:

    • 该类的所有实例都已经被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    垃圾收集算法

    标记-清除算法

    标记-清除算法分两阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象

    标记-清除算法的不足:第一两个过程的效率都不高;第二会产生内存碎片

    复制算法

    复制算法是把内存分成相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块内存上,然后将当前这块内存一次清理掉。

    复制算法的好处就是实现简单,效率高,不足之处就是浪费内存空间。

    如果对象的存活率较高,复制操作的效率会比较低,所以复制算法适合对象存活率较低的情况。

    现在商业虚拟机及HotSpot虚拟机都采用这种算法来回收新生代。新生代的内存空间会分为Eden、From Survivor和To Survivor三块,每次使用Eden空间和From Survivor空间。当进行回收时,将Eden空间及From Survivor空间存活的对象复制到To Survivor,然后清理Eden和From Survivor空间。

    标记-整理算法

    标记-整理算法分两阶段:首先标记需要回收的对象,然后让所有存活对象往一端移动,然后直接清理端边界以外的内存。

    标记-整理算法适合对象存活率较高的情况,并且不会产生内存碎片

    分代收集算法(Generational Collection)

    一般把Java堆分成新生代和老年代:

    • 新生代的对象存活率较低,使用复制算法

    • 老年代的对象存活率较高,使用“标记-清除”或“标记-整理”算法

    HotSpot的算法实现

    枚举根节点

    必须确保可达性分析执行的效率:HotSpot虚拟机中使用一组OopMap(Ordinary Object Pointer)普通对象指针存放对象的引用。在类加载完成的时候,HotSpot就把对象内什么偏移量是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

    必须确保可达性分析的执行时在“一致性”的快照中进行:在整个分析期间整个执行系统看起来像被冻结在某个时间点上(Stop The World)。

    安全点(Safepoint)

    安全点是HotSpot生成OopMap的位置,也是程序停下来GC的地方。安全点的选定基本是以程序“是否具有让程序长时间执行的特征”为标准进行选定,“长时间执行”的最明显特征就是指令序列复用,例如方法的调用、循环跳转、异常跳转等。

    如果在GC时让所有的线程跑到安全点再停顿:

    • 抢先式中断:首先让所有线程中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点。目前基本没有虚拟机才用这种方式
    • 主动式中断:当GC需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动轮询这个标志,发现中断标识为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

    安全区域(Safe Region)

    安全区域是安全点的扩展,解决了程序在“不执行”时候的GC安全问题:如果程序处在Sleep或Blocked的状态,这个时候线程无法响应JVM的中断请求,无法走到安全点。

    在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region。这样,当这段时间内发生GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成根节点枚举(整个GC过程),如果完成,线程就继续执行将,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

    垃圾收集器

    • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待情况
    • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定并行,可能会交替执行),用户程序继续,而垃圾收集程序运行于另外一个CPU上

    Serail、ParNew、Parallel Scavenge : 复制算法

    CMS:标记-清除算法

    Parallel Old、Serial Old、G1:标记-整理算法

    Serial 收集器

    Seral 是一个单线程的收集器。它只会使用一个CPU或者一条收集线程去完成垃圾收集的工作,在它进行垃圾收集时,必须暂停其他所有的工作线程。

    多CPU的情况下,效率比较低。但对于单CPU的情况下,因为是单线程,减少了线程之间的交互,专注垃圾收集的工作,效率会比较高,适用于Client模式下的虚拟机。

    ParNew 收集器

    ParNew是Serial的多线程版。除了使用多条线程进行垃圾收集之外,其余行为都和Serial一样。它默认开启的收集线程和CPU的数量相同,在CPU非常多的情况下,可以通过参数-XX:ParallelGCThreads限制垃圾收集的线程数。ParNew是目前唯一能和CMS配合工作的收集器。

    在单CPU的情况下,因为存在线程交互的开销,ParNew的性能不一定比Serial收集器的好。不过在当前CPU动辄4核的情况下,它是许多运行在Server模式下的虚拟机中首选的新生代收集器。

    使用-XX:UserConcMarkSweepGC或者-XX:UserParNewGC选项来指定

    Parallel Scavenge 收集器

    Parallel Scavenge 是一个并行的多线程收集器,它的目标是达到一个可控的吞吐量(Throughput),因此也被称为“吞吐量优先”收集器。

    吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

    停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验,而高吞吐量则可以高效地利用CPU时间,尽快地完成程序的运算任务,主要适合后台运算而不需要太多交互的任务。

    Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

    • -XX:MaxGCPauseMillis:最大的GC停顿时间。允许一个大于0的毫秒数,收集器尽可能保证内存回收花费的时间不超过设定值。但这并不意味这个值越小越好,因为GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。因为收集300M的新生代肯定比收集500M的新生代快,原来10秒收集一次,每次停顿100毫秒,现在5秒收集一次,每次停顿70毫米,停顿时间是下降了,但吞吐量也降低了。
    • -XX:GCTimeRatio:垃圾收集时间战总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占5%(1/(1+19)),默认值是99,就是允许最大1%(1/(1+99))的垃圾收集时间。

    GC自适应调节策略(GC Ergonomics):-XX:UseAdaptiveSizePolicy 当这个参数打开,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

    Serial Old 收集器

    Serial Old 是Serail收集器的老年代版本,适合于给Client模式下的虚拟机使用。如果在Server模式下:一种用途是在JDK1.5及之前的版本和Parallel Scavenge搭配使用;另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

    Parallel Old 收集器

    Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,在JDK1.6中才开始提供。

    CMS(Concurrent Mark Sweep) 收集器

    CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。目前大部分应用在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统的停顿时间最短,以给用户带来较好的体验。

    CMS收集器基于“标记-清除”算法实现,整个过程分为4步骤:

    • 初始标记(CMS inital mark) 需要“Stop The World”,仅仅就只是标记一下GC Roots能直接关联到的对象
    • 并发标记(CMS Concurrent mark) GC Roots枚举阶段
    • 重新标记(CMS remark) 需要“Stop The World”,该阶段是修正并发标记期间因为用户线程继续运作导致标记产生变动的那部分对象的标记记录
    • 并发清除(CMS concurrent sweep)

    CMS收集器提供了并发收集、低停顿等优点,但也有很明显的缺点:

    1、CMS收集器对CPU资源敏感:CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程占不少于25%的CPU资源,并且随着CPU数量的增加而降低。但如果CPU少于4个时(比如2个时),CMS对用户程序的影响就会比较大。

    2、CMS收集器无法处理浮动(Float Garbage)垃圾,可能出现“Concurrent Mode Failure”失败导致发生另外一次Full GC。

    • CMS在并发清理阶段用户线性还在运行,伴随用户线程的运行自然就会有新的垃圾不断产生,CMS无法在当次处理这些垃圾,只能在下次GC时处理,这些垃圾称为“浮动垃圾”。

    • 由于用户线程在GC的时候还在运行,所以必须预留足够的内存空间给用户线程,当预留的内存空间无法满足用户线程时,就会出现“Concurrent Mode Failure”失败,CMS启动后备方案:临时启动Serial Old进行老年代的垃圾收集,此时,停顿时间就会比较长。CMS收集器通过参数-XX:CMSInitiatingOccupancyFraction 来激活自己,JDK1.5默认设置当老年代使用68%触发CMS收集器,JDK1.6中,此值为92%。

    3、CMS收集器采用“标记-清除”算法,会产生大量的空间碎片。会导致老年代还有很多空间,因为找不到足够大的连续空间不得不进行Full GC。

    • -XX:UseCMSCompactAtFullCollection:开关参数,默认是开启,用于在CMS收集器顶不住需要进行Full GC是开启内存碎片整理,这样会导致停顿时间变长
    • -XX:CMSFullGCsBeforeCompaction:这个参数用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的。默认为0,表示每次进入Full GC时都进行内存碎片整理

    G1 收集器

    G1是一款面向服务端应用的垃圾收集器,它有以下几个特点:

    • 并行与并发:G1重复利用硬件资源优势,使用多个CPU核心来缩短Stop-The-World停顿时间
    • 分代收集:分代的概念依然在G1中保留
    • 空间整合:G1从整体上看采用了“标记-整理”的算法,局部(连个Region)来看是基于复制算法,这样保证了G1运行期间不会产生内存碎片
    • 可预测停顿:G1除了最求低停顿外,还能建立可预测的停顿模型。能让使用明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

    G1收集器Java堆的布局:将整Java堆划分多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。

    G1创建可预测的停顿时间模型:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这也是Garbage-First名称的来由。

    G1如何避免全堆扫描:使用Remembered Set来避免全堆扫描。G1中的每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂停中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是是否有老年代的对象引用了新生代的对象)。如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围假如Remembered Set即可。

    G1收集器的回收步骤可分为以下步骤:

    • 初始标记(Initial Marking):仅仅标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时,能在正确的Region中创建新对象,这个阶段需要停顿用户线程。
    • 并发标记(Concurrent Marking):从堆的GC RootS开始对堆的对象进行可达性分析,找出存活的对象,这个阶段用户线程可并发执行
    • 最终标记(Final Marking):修正由于并发阶段用户线程执行运行导致标志产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。这阶段需要停顿用户线程,当标记线程可以并行执行。
    • 筛选回收(Live Data Counting and Evacuating ):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。

    内存的分配与回收策略

    对象优先在Eden分配

    • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多数具备朝生夕灭的特征,所以Minor GC非常频繁,一般回收速度也很快。
    • 老年代(Major GC/Full GC): 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(Parallel Scavenge收集器例外),Major GC 一般比Minor GC慢10倍以上。

    大对象直接进入老年代

    大对象:需要大量连续空间的Java对象,比如:长的字符串和数组。对大对象,我们可以通过参数设置,让它直接在老年代分配,从而避免频繁的Minor GC。

    -XX:PretenureSizeThreshold:可以让对象直接在老年代分配

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

    对象年龄技术器:虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间,并且对象的年龄设定为1。对象在Survivor区每熬过一次Minor GC,年龄就增加1。当它的年龄增加到一定程度(默认为15),就将会晋升到老年代。对象晋升到老年代的阀值,可以通过参数-XX:MaxTenuringThreshold设置。

    动态对象年龄判断

    虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。

    空间分配担保

    在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果大于,将尝试进行一次Minor GC;如果小于,或者HandlePromotionFailure设置不允许,需要进行一次Full GC。

    但是,在JDK1.6 Update24之后,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

    理解GC日志

    打印GC日志:-XX:PrintGCDetails

    GC日志 待补充……

    相关文章

      网友评论

          本文标题:深入理解Java虚拟机-垃圾收集器及内存分配策略

          本文链接:https://www.haomeiwen.com/subject/iybgextx.html