深入理解jvm--垃圾收集器与内存分配策略(三)

提纲

这里写图片描述

判断对象的存活状态

垃圾收集主要区域:Java堆和方法区。因为只有在程序运行时才能知道创建哪些对象,所以这部分的内存分配和回收都是动态的。

1.引用计数算法:

定义:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时候计数器为0的对象是不可能在被使用的。
优点:实现简单,判定效率高。
缺点:很难解决对象之间相互循环引用的问题。

2.可达性分析算法:

定义:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路称为“引用链”,当一个对象到GC Roots没有任何引用链相连,即GCRoots到这个对象不可达时,证明此对象是不可用的
可作为GC Roots的对象:

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

3.引用分类(强>软>弱>虚)

强引用:只在程序代码中普遍存在的,类似于“Object obj = new Object()”这类的引用
软引用:用来描述一些还有用但并非必须的对象。会在内存溢出之前会进行回收。可通过Soft Reference类实现软引用。public class SoftReference<T> extends Reference<T>
弱引用:描述非必需的对象。比软引用更弱,只能存活到下一次垃圾回收之前。可通过WeakReference类实现引用。public class WeakReference<T> extends Reference<T>,例如WeakHashMap的实现:m.put(key, new WeakReference(value))
虚引用:最弱的引用关系,不会堆生存时间构成影响。只是会在回收时收到系统通知。可通过PhantomReference类实现虚引用。public class PhantomReference <T>extends Reference<T>

4.回收对象的过程

在进行可达性分析后发现对象没有与GC Roots相连接的引用链,那么他将被进行第一次标记并且进行筛选,条件是该对象是否有必要执行finalize方法(被执行过一次或者对象没有覆盖重写finalize方法则视为执行过)。
若对象没有必要执行finalize方法,则直接行回收
若对象有必要执行finalize方法,那么这个对象则会被放置在一个F-Queue队列中,并在稍后头一个finalizer线程去执行触发这个方法。
执行之后GC Roots会进行第二次标记,条件是该对象是否重新与引用链上的对象关联。
如果finalize方法中把本对象(this)赋值给某个类对象或者成员变量,它将被移除该队列;
如果finalize方法中没有调用,该对象没有逃脱,则会被回收。
回收过程举例:

public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes, i am alive");
    }
    //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
    //调用后将对象返回还在调用该对象的线程,垃圾收集器最多只调用一次
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次拯救自己
        SAVE_HOOK = null;
        System.gc();
        //由于finalize方法优先级很低,所以可以先休眠0.5秒
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead");
        }

        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead");
        }
    }
}

运行结果:

finalize method executed!
yes, i am alive
no,i am dead

可看到

5.回收方法区(永久代)

判断废弃常量:
字面量无其他地方被引用
判断无用的类:
 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
 加载类的class loader已经被回收。
 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过该类的方法。
参数控制:-Xnoclassgc
查看类的加载和卸载信息:-verbose:class、-XX:TraceClassLoading(product版)、-XX:+TraceClassUnLoading(FastDebug版)

垃圾收集算法

这里写图片描述

1.标记-清除算法

先标记出所有要回收的对象,在标记完成后同意会收要标记的对象。
缺点:效率问题,标记和清除效率不高;
空间问题,会产生大量不连续碎片,导致提前触发垃圾回收动作。

2.复制算法

定义:将可用内存分为大小相等的两块,每次只是用其中的一块,当这块用完,就将还存活的对象复制到另一块上,然后吧已使用过的一次清理掉。
优点:只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
实际上虚拟机会将内存划分为一块较大的Eden空间和两块较小的survivor空间,每次使用Eden和其中的一块survivor空间,回收时会将Eden与survivor中还存活的对象赋值到另一块survivor中,并清理掉Eden和刚才用过的survivor空间。
这里写图片描述

3.标记-整理算法

先标记出所有要回收的对象,让所有要存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4.分代收集算法

一般把java堆分成新生代和老生代,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量对象存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。

hotspot算法实现

1. 枚举根节点

1 逐个检查GCRoots结点会耗费很多时间;
2 可达性分析需要停顿线程(stop the world)来分析对象的引用关系;
3 通过OopMap存放对象引用,帮助hotspo快速准确地完成GCRoots枚举。

2. 建立安全点

安全点的选定:是否具有让程序长时间执行的特征,长时间执行最明显的特征是指令列复用,例如方法调用、循环跳转、异常跳转等。
线程停顿到安全点的方案:抢先式中断和主动式中断
抢先式中断:不需要线程的代码主动配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程让他跑到安全点上。
主动式中断:当GC需要中断线程的时候,不直接对线程进行操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志威震时就自己中断挂起,轮询标志的地方个安全点是重合的,另外再加上创建对象需要分配内存的地方。

3. 安全区域

安全点的缺点:safe point机制保证了程序执行时,在不太长的时间内就会与带可进入的GC的Safepoint。但是程序不执行时,没有分配CPU时间,例如当线程处于Sleep状态或者Blocked状态,这时候无法相应JVM的中断请求。
安全区域:是指在一段代码中,引用关系不会发生变化。在这个区域中的任意地方开始GC
都是安全的。我们也可以把safe region看作是被拓展了的safe point。

垃圾收集器

这里写图片描述

1.Serial收集器

单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作。进行垃圾收集时,必须暂停其它所有工作线程,直到收集结束。用于client模式中的新生代收集器。
缺点:工作线程会因为内存回收而导致停顿
优点:简单而高效,对于单个CPU环境来说,serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

2.ParNew收集器

serial收集器的多线程版本。常用在server模式下的新生代收集器。
限制垃圾回收的线程数:-XX:ParallelGCThreads
指定ParNew收集器:-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC

3.Parallel Scavenge收集器

采用复制算法的新生代多线程吞吐量优先收集器。其目标是达到一个可控制的吞吐量。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
GC自适应策略:根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
控制最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
设置吞吐量大小:-XX:GCTImeRatio
设置自适应调节策略:-XX:+UseAdaptiveSizePolicy
指定新生代的大小:-Xmn
设置Eden与survivor的比例:-XX:SurvivorRatio
晋升老年代对象大小:-XX:PretenureSizeThreshold

4.Serial Old收集器

serial收集器的老年代版本,单线程收集器,使用标记整理算法。
给client模式下的虚拟机使用
server模式下的主要用途:

  1. 在jdk1.5之前与parallel Scavenge收集器搭配使用
  2. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

5.Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

6.CMS收集器

是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,它的运作过程分4个步骤:
1.初始标记:标记GC Roots能直接关联到的对象。
2.并发标记:进行GC Roots Tracing的过程。
3.重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
4.并发清除。
优点:并发收集、低停顿。
缺点:

  1. CMS收集器对CPU资源非常敏感。
  2. CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC产生。
  3. CMS在收集结束时会有大量空间碎片产生。

7.G1收集器

特点:

  1. 并行与并发:使用多个CPU来缩短停顿时间,可以通过并发的方式让Java程序继续运行。
  2. 分代收集:能够采用不同方式处理新对象和多次回收之后存活的旧对象。
  3. 空间整合:从整体来看是基于标记-整理算法,从局部来看基于复制算法。意味着G1运行期间不会产生内存碎片。
  4. 可预测的停顿:建立可预测的停顿时间模型,能让使用者在一个长度为M毫秒的时间片段,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器将java堆划分为多个大小相等的独立区域Region,之所以能建可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪Region中的垃圾堆积的价值大小,在后台维护一个优先列表Remembered Set,根据允许收集时间,优先回收价值最大的Region。这种使用内存空间以及有优先级的区域回收方式,不保证了G1收集器在优先时间内可以获取尽可能高的收集效率。
Remembered Set记录对象引用以及其他收集器中的新生代与老年代之间的对象引用,避免了全盘扫描。
G1的运作步骤:

  1. 初始标记: 标记GC Roots能直接关联到的对象。并且修改TAMS的值,让下一阶段用户程序并发执行时,能在正确可用的Region中创建对象,这阶段需要停顿线程,但耗时很短。
  2. 并发标记:从GC Root开始对堆中对象进行分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  4. 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

内存分配与回收策略

1.对象优先在Eden分配

大多数情况下,对象在新生代Eden区分配。当Eden区没有足够空间进行非配时,虚拟机将发起一次Minor GC。
打印内存回收日志:-XX:PrintGCDetails
MinorGC 示例:

package collection;

public class TestAllocation {

    private static final int _1MB = 1024*1024;
    public static void testAllocation(){
        byte[] allocation1,allocation2,allocation3,allocation4;
        allocation1 = new byte[2*_1MB];
        allocation2 = new byte[2*_1MB];
        allocation3 = new byte[2*_1MB];
        allocation4 = new byte[2*_1MB];
    }

    public static void main(String[] args) {
        testAllocation();
    }

}

jvm参数

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

运行结果:

[GC/*停顿类型:新生代*/ [PSYoungGen/*停顿区域:新生代*/: 6980K->632K(9216K)/*GC前该区域已使用容量->GC后该区域已使用容量(该区域总容量)*/] 6980K->6776K(19456K),
 0.0041511 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 632K->0K(9216K)] [ParOldGen: 6144K->6616K(10
240K)] 6776K->6616K(19456K) [PSPermGen: 2711K->2710K(21504K)], 0.0
102132 secs] [Times: user=0.05 sys=0.02, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 2213K [0x00000000ff600000, 0x00
 00000100000000, 0x0000000100000000)
  eden space 8192K, 27% used [0x00000000ff600000,0x00000000ff8297e0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 6616K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 64% used [0x00000000fec00000,0x00000000ff276018,0x00000000ff600000)
 PSPermGen       total 21504K, used 2717K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 12% used [0x00000000f9a00000,0x00000000f9ca7468,0x00000000faf00000)

2.大对象直接进入老年代

最典型的大对象就是那种很长的字符串以及数组。
大于设置值的对象直接在老年代分配:-XX:PretenureSizeThreshold

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

对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。
设置晋升老年代的年龄阈值:-XX:MaxTenuringThreshold

4.动态对象年龄判定

为了能更好适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到MaxTenuringThreshold才能到老年代。如果在survivor空间中相同年领所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

5.空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间
如果成立,那么Minor GC可以确保是安全的。
如果不成立,则会查看HandlePromotionFailure设置值是否允许担保失败。
如果允许冒险,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
如果大于,将尝试着进行一次Minor GC,尽管有风险;
如果小于,或者HandlePromotionFaliure设置不允许冒险,那这时要改为进行一次Full GC。
冒险:新生代使用复制算法,但为了内存利用率,只是用一个survivor空间来作为轮换备份因此当出现大量对象在minorGC后仍然存活的状况,就需要老年代要进行这样的担保,把survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多的空间

猜你喜欢

转载自blog.csdn.net/yinweicheng/article/details/80611670