《深入理解Java虚拟机》读书笔记二

第三章 垃圾收集器与内存分配策略

1、判断对象是否已死

引用计数法:

  • 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,每当引用失效时,计数器值就减1。
  • 任何时刻计数器为0的对象就是不可能再被使用的。
  • 引用计数器实现简单,判定效率高,但是无法解决对象相互循环引用。

可达性分析:

  • 通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜素,搜素所走过的路称为引用链。
  • 当一个对象到GC Roots没有任何引用链相连,则证明对象不可用。
  • GC Roots的对象包括,虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象。

再谈引用:

  • 强引用,类似 Object obj = new Object()这类引用,只要强引用还存在垃圾收集器永远不会收集掉被引用的对象。
  • 软引用,在系统将要发生内存溢出异常之前将会把这些对象列入回收范围之中进行第二次回收。JDK1.2之后提供了SoftReference类来实现软引用。
  • 弱引用,被弱引用关联的对象只能生存到下一次垃圾收集之前,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
  • 虚引用,不会对对象的生存时间造成影响,也无法通过虚引用来取得对象的实例,虚引用的作用是当对象被回收的时候收到一个系统通知。

生存还是死亡:

  • 对象至少要经历两次标记才可以被回收,如果对象在进行可达性分析的时候发现没有GC Roots相连接的引用,那它将会被第一次标记并且进行一次筛选。
  • 筛选条件是此对象是否有必要执行finalize()方法。如果对象没有覆盖finalize方法,或者finalize方法已经被执行调用过,虚拟机将这种情况视为没有必要执行。
  • 如果这个对象被判定为有必要执行finalize()方法,那么这个对象会将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的,优先级低的Finalizer线程去执行他。
  • 如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么第二次标记他将被移出即将回收的集合。如果对象这个时候还没有逃脱那么基本上他就真的被回收了。

回收方法区:

  • 方法区也需要被回收,主要回收的是废弃的常量和无用的类。
  • 无用的类的判定条件,类所有的实例被回收,类加载器被回收,对应的class没有任何引用,无法通过反射获得该类的方法。eu
  • 废弃的常量,没有l任何对象引用常量池中的这个常量,也没有其他方法引用了这个字面量。

2、垃圾收集算法

标记清理法:

  • 首先对标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
  • 标记和清除效率都比较低,还有就是会产生大量的空间碎片。

复制算法:

  •  将可用内存划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完后,就将还在着的对象复制到另外一块上,然后再把已使用的过的内存空间一次清理掉。
  • 实现简单,运行高效,但是内存缩小了一半。常用于存活对象少的新生代。

标记整理算法:

  • 首先对需要回收的对象进行标记,对存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:

  • 根据对象存活周期的不同将内存划分为几块,一般将Java堆分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。
  • 新生代每次垃圾收集有大量的对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代中因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收。

3、HotSpot算法分析

枚举根节点:

  • 在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到直接获取那些地方存在着对象引用。
  • 在OopMap的帮助下可以准确的完成GC Roots枚举。

安全点:

  • HotSpot在特定的位置记录了这些信息,生成了对弈的OopMap,这些位置称为安全点。
  • 让所有的线程到达安全点的方式有抢占式中断和主动式中断。
  • 抢占式中断在GC发生时中断所有线程,如果线程不再安全点上就恢复线程直到到达安全点。
  • 主动式中断,对线程设置一个标志,各个线程主动去轮询这个标志,轮询标志和这个安全点重合,发现中断标志就中断挂起。

安全区域:

  • 如果程序没有分配CPU时间,线程处于睡眠或者阻塞状态,这时候线程无法响应JVM的中断请求,这种情况就需要安全区域爱解决。安全区域是指一段代码之中引用不会发生变化。
  • 在线程执行到安全区域的时候首先标识自己已进入安全区域,在GC时无需管标识自己为安全区域的线程,在线程要离开的时候要检查是否已经完成根节点枚举,如果完成就继续执行,否则必须受到可以离开的信号才可以离开。

4、垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。如果收集器中存在连线则说明收集器可以搭配使用。

Serial收集器:

  • 单线程收集器,采用复制算法,收集时必须暂停其他工作线程然后使用一个CPU或者一条收集线程去完成垃圾收集工作。
  • 简单而高效,专心做垃圾回收效率高。 依然是HotSpot在Client模式下默认的新生代收集器。

ParNew收集器:

  • Serial收集器的多线程版本,除了多线程外,其余的行为、特点和Serial收集器一样。
  • 新生代收集器,也采用复制算法。可以和CMS收集器配合使用。
  • Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器:

  • 多线程收集器,吞吐量优先的收集器,可以设置吞吐量和停顿时间。
  • -XX:MaxGCPauseMillis控制最大垃圾收集停顿时间。
  • CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量。

Serial Old收集器:

  • Serial收集器的老年代版本,单线程收集器,采用标记整理算法。
  • 与Parallel Scavenge搭配使用,作为CMS收集器的备选方案。

Parallel Old收集器:

  • Parallel Scavenge收集器老年代版本,吞吐量优先的收集器,使用标记整理法。

CMS收集器:

  • 并行低停顿的收集器,一种以获取最短回收停顿时间为目标的收集器。
  • 分为四个步骤初始标记、并发标记、重新标记和并发清除。
  • 初始标记:标记一下GC Roots能直接关联到的对象。
  • 并发标记:进行GC Tracing的过程。
  • 重新标记:修正并发标记期间因用户程序继续运行导致标记产生变动的那一部分对象。
  • 采用标记清除算法,容易产生内存碎片。
  • CMS收集器对CPU敏感,并发阶段占用了一部分CPU资源,导致应用程序执行缓慢。
  • 无法清理浮动垃圾,由于并发阶段,用户线程还存在,自然会有新的垃圾产生,而这些垃圾只能留到下一次GC中清理,这一部分垃圾称为浮动垃圾。
  • 应用:与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度; 以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用;

G1收集器的特点:

  •  并行与并发,G1收集器可以通过并发的方式让Java线程继续执行。
  • 分代收集,能独立管理整个堆,不需要与其他收集器一起使用,能够采用不同方式处理不同时期的对象。
  • 堆内部布局改变,将整个堆划分为若干个大小相等的区域,新生代和老年代不再物理隔离,它们都是一部分Region(不需要连续)的集合。
  • 空间整合,从整体来看是基于标记整理,从局部两个Region之间是基于复制算法,这两种算法都不会产生空间碎片。
  • 可预测停顿,低停顿的同时实现高吞吐量,G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;

可预测停顿的原因:

  •  可以有计划地避免在Java堆的进行全区域的垃圾收集;
  • G1跟踪各个Region获得其收集价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表;
  • 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
  • 这种使用Region划分空间以及优先级的区域回收方式保证了在有限的时间内可以获取尽可能高的收集效率;

一个对象被不同区域引用的问题:

  • 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
  • 每个Region都有一个对应的Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
  • 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set; 就可以保证不进行全局扫描,也不会有遗漏。

G1收集器的运行步骤:

  • 初始化标记,标记GC Roots能直接关联的对象
  • 并发标记,从GC Root开始对堆中对象进行可达性分析找出存活的对象。
  • 最终标记,修正并发标记阶段因用户线程继续执行而导致的标记产生变化,这部分变化会被记录在Remembered Set Log中,最后需要合并到Remembered Set中。
  • 筛选回收,首先排序各个Region的回收价值和成本,然后根据用户期望的停顿时间来制定回收计划,最后按照计划回收一些价值高的Region中垃圾对象; 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。

5、内存分配与回收策略

理解GC日志:

[GC (Allocation Failure) [PSYoungGen: 6150K->696K(9216K)] 6150K->4800K(19456K), 0.0027801 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
  • [GC、[FullGC,说明了这次垃圾收集的停顿类型而不是用来区分新生代GC还是老年代GC的。如果Full说明GC发生了Stop-The-World的。
  • [PSYoungGen,表示GC发生的区域与收集器的类型密切相关,PSYoungGen代表是ParallelScavenge收集器。
  • 6150K ->696K(9216K),含义是GC前该内存区域已使用容量->GC后该内存区域已使用的容量(该内存区域总容量)。
  • 6150k->4800k(19456k),表示GC前Java堆已使用容量 ->GC后Java堆已使用容量(Java堆总容量)。
  • 0.0027801 secs,表示该GC所占用的时间,单位是秒。
  • [Times: user=0.00 sys=0.00, real=0.00 secs],分别代表用户态消耗的CPU时间,内核消耗的CPU时间和操作从开始到结束锁经过的墙钟时间。墙钟时间包括各种非运算的等待耗时,如等待磁盘I/O、等待线程阻塞,但是CPU时间不包括这些。

对象优先在Eden区分配:

  • 大多数情况下,对象在新生代Eden区中分配,如果启动了本地线程分配的缓冲将按线程优先级在TLAB上分配,少数情况将直接分配到老年代中,分配规则取决于所选收集器和内存相关参数的设置。
    package com.ecut.gc;
    
    /**
     * -verbose:ge -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     */
    public class TestAllocation {
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
    
            byte[] allocation1, allocation2, allocation3, allocation4;
    
            allocation1 = new byte[2 * _1MB];
            allocation2 = new byte[2 * _1MB];
            allocation3 = new byte[2 * _1MB];
            //出现一次Minor GC
            allocation4 = new byte[4 * _1MB];
    
        }
    }

    -Xms设置JVM最大可用内存,-Xmx促使内存设置为和Xms一样可以避免每次垃圾回收后JVM重新分配内存,-Xmn设置年轻代的大小,-XX:PrintGCDetails打印内存回收日志,-XX:SurvivorRation设置Eden区和Survivor区的空间比例是8:1。

    运行结果如下:

    [GC (Allocation Failure) [PSYoungGen: 6316K->760K(9216K)] 6316K->4864K(19456K), 0.0046261 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
    Heap
     PSYoungGen      total 9216K, used 7141K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3b610,0x00000000ffe00000)
      from space 1024K, 74% used [0x00000000ffe00000,0x00000000ffebe030,0x00000000fff00000)
      to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
     ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
     Metaspace       used 3495K, capacity 4498K, committed 4864K, reserved 1056768K
      class space    used 387K, capacity 390K, committed 512K, reserved 1048576K
  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以MionrGC非常频繁,一般回收速度比较快。
  • 老年代GC(Major GC/ Full GC):指发生在老年代的GC,出现Major GC ,经常会伴随至少一次Minor GC,但非绝对的,在Parallel Scavenge收集器的收集策略里就进行Major GC 的策略选择过程,Major GC的速度一般会比Minor GC慢10倍以上。

大对象直接进入老年代:

  • 大对象指的是大量连续内存空间的Java对象,最典型的的是很长的字符串以及数组。
  • -XX:PretenureSizeThreshold参数来设置大于这个值的对象直接进入老年代分配内存。
  • 这个参数只有Serial和ParNew两款收集器才有用。
    package com.ecut.gc;
    
    /**
     * -verbose:ge -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
     */
    public class TestPretenureSizeThreshold {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
    
            byte[]  allocation = new byte[4 * _1MB];
    
        }
    }

    运行结果如下:

    Heap
     def new generation   total 9216K, used 2384K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  29% used [0x00000000fec00000, 0x00000000fee54198, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
     Metaspace       used 3497K, capacity 4498K, committed 4864K, reserved 1056768K
      class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

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

  • 虚拟机给每个特定的对象定义了一个age计数器,如果这个对象Eden出生并经历了一次MinorGC后仍然存活,并且被survivor区所容纳,将被移入survivor区中并且对对象的年龄加1,在survivor区中每经历一次MinorGC年龄就加1,当年龄达到一定的阈值就会晋升到老年代。
  • 可以通过-XX:MaxTenuringThreshold来设置多少岁的对象将进入老年代。
    package com.ecut.gc;
    
    /**
     *  -verbose:ge -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
     */
    public class TestTenuringThreshold {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
    
            byte[] allocation1, allocation2, allocation3;
    
            allocation1 = new byte[_1MB/4];
            allocation2 = new byte[4 * _1MB];
            allocation3 = new byte[4 * _1MB];
            allocation3= null;
            allocation3 = new byte[4 * _1MB];
        }
    }

动态对象年龄判定:

  • 虚拟机不是永远都要求对象的年龄必须达到MaxTenuringThreshold才可以进入老年代。如果survivor区中的相同年龄的对象大小总和已经大于survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入老年代。
    package com.ecut.gc;
    
    /**
     *  -verbose:ge -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
     */
    public class TenuringThresholdTest {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
    
            byte[] allocation1, allocation2, allocation3, allocation4;
    
            allocation1 = new byte[_1MB/4];
            allocation2 = new byte[_1MB/4];
            allocation3 = new byte[4 * _1MB];
            allocation4 = new byte[4 * _1MB];
            allocation4= null;
            allocation4 = new byte[4 * _1MB];
        }
    }

空间分配担保:

  • 在发生MinorGC之前,虚拟机会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果条件成立,那MinorGC可以确保安全发生,如果不成立,则虚拟机需要查看HandlePromotionFailure设置值是否允许。
  • 如果允许担保失败会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试一次MinorGC,如果MinorGC失败会进行一次FullGC。如果老年代最大可用连续空间小于历次晋升到老年代对象的平均大小,将进行一次FullGC。
  • jdk6之后HandlePromotionFailure参数不再影响虚拟机内存分配担保策略,只要老年代连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则将进行FullGC.

源码地址:

https://github.com/SaberZheng/jvm-test

转载请于明显处标明出处:

https://www.cnblogs.com/AmyZheng/p/10509394.html

posted @ 2019-03-13 10:36  AmyZheng  阅读(678)  评论(0编辑  收藏  举报