美文网首页
垃圾收集器和内存分配策略

垃圾收集器和内存分配策略

作者: 官子寒 | 来源:发表于2020-02-05 20:59 被阅读0次

    1. 概述

    • 线程计数器,虚拟机栈,方法栈随着线程自生自灭,内存分配基本确定,因此这几个区域不需要太多考虑
    • Java堆和方法区不一样,比如一个接口中的多个实现类需要的内存可能不一样,这部分的内存分配具有动态性,只有在运行期间才能确定下来

    2. 对象已死?

    如何判断对象是否已经死去?主要使用的方法包括引用计数法根搜索算法

    2.1 引用计数法

    给对象添加一个引用计数器,每当有一个地方引用它时,就加1;当为0时就认为这个对象不再使用

    优点:简单高效
    缺点:不能解决循环引用的问题

    public class ReferenceCountingGC {
        public Object isntance;
    
        private final int _1MB = 1024 * 1024;
    
        private byte[] arr = new byte[10 * _1MB];
    
        public static void main(String[] args) {
            ReferenceCountingGC a = new ReferenceCountingGC();
            ReferenceCountingGC b = new ReferenceCountingGC();
    
            a.isntance = b;
            b.isntance =a;//如果是引用计数法,在这儿会加1
    
            a  = null;
            b = null;
    
    
            System.gc();//那么在这儿不会清除a和b
        }
    }
    
    GC日志

    可以看出Java中不是 使用引用计数法来判断

    2.2 根搜索算法

    通过一系列GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则认为它不可用了

    在Java语言里,可作为GC Roots对象的包括如下几种:
    a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
    b.方法区中的类静态属性引用的对象
    c.方法区中的常量引用的对象
    d.本地方法栈中JNI的引用的对象

    2.3 再谈引用

    Java中将引用分成强引用软引用弱引用虚引用

    • 强引用:只要强引用还在,垃圾收集器就永远不会回收掉被引用的对象
    • 软引用:还有用但并不是必需的引用,系统在内存溢出之前会把这些对象列进回收范围之中进行第二次回收,如果这次还是没有足够的内存才会进行回收
    • 弱引用:只能生存到下次垃圾收集发生之前
    • 虚引用:一个对象是否有虚引用不会对其生存时间造成影响。为一个对象设置虚引用关联的唯一目的就是希望这个对象在回收之前发一个通知

    2.4 生存还是死亡

    根搜索算法
    public class SaveHook {
        public static SaveHook saveHook = null;
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("finalize Executed");
            SaveHook.saveHook = this;
        }
    
        private void isAlive() {
            System.out.println("I am alive");
        }
    
        public static void main(String[] args) throws InterruptedException {
            saveHook = new SaveHook();
            saveHook= null;
    
            System.gc();
    
            Thread.sleep(500); //如果没有等待,则会因为没有救赎而直接死亡
    
            if(saveHook != null){
                saveHook.isAlive();
            } else {
                System.out.println("dead");
            }
    
        }
    }
    

    2.5 回收方法区

    • 方法区的回收性价比低

    永久代的垃圾回收分为两部分:废弃常量无用的类
    1)废弃常量判定方法:是否有引用
    2)无用的类判定方法:需要满足3个条件

    1.该类所有的实例都被回收,也就是Java堆中不存在该类的任何实例
    2.加载该类的ClassLoader已经被回收
    3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    3. 垃圾收集算法

    3.1 标记-清除算法

    • 最基础的垃圾收集算法

    缺点:
    1)效率问题:标记和清除的效率都太低
    2)空间问题:容易产生空间碎片,当程序在接下来运行中需要分配大量连续内存空间时,会再次触发垃圾收集算法

    标记-清除算法

    3.2 复制算法

    • 因为新生代的对象基本上都是朝生夕死的,存活下来的对象约占10%左右,所以需要复制的对象比较少,采用这种算法效率比较高
    • 当进行内存回收时,将eden区和survivor区的还存活的对象一次性地复制到另一个survivor空间上,最后将eden区和刚才使用过的survivor空间清理掉
      赋值算法

    3.3 标记-整理算法

    • 复制算法在对象存活率较高的情况下就要进行较多的对象复制操作,效率将会变低。更关键的是,如果你不需要浪费50%的空间,就需要有额外的空间进行分配担保,用以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种办法。
    标记-整理算法

    3.4 分代收集算法

    • 根据对象的生命周期的不同将内存划分为新生代,老年代
    • 新生代每次垃圾收集有大量对象死去,采用复制算法,只需要付出少量存活对象就可以完成收集
    • 老年代对象存活率高,没有额外的空间对它进行分配担保,必须使用标记-清理或者标记-整理算法

    4. 垃圾收集器

    收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

    4.1 Serial收集器

    • 单线程收集器
    • 在进行垃圾收集时,暂停其他所有的工作线程

    优点:
    1.简单高效,专心单线程收集,没有线程交互开销
    2.适用于Client模式下的虚拟机

    4.2 ParNew收集器

    • 多线程收集器,其他与Serial收集器差不多

    4.3 Parallel Scavenge收集器

    • 关注点是达到一个可控制的吞吐量
      吞吐量 = 运行用户代码时间 / 运行用户代码时间 + 垃圾收集时间

    停顿时间越短,越适合与用户交互的程序
    吞吐量高则适合在后台计算,交互少的程序

    • 自适应调整策略,不需要手动指定新生代的大小等参数

    4.4 Serial Old收集器

    • 使用标记-整理算法,单线程
    • 主要用途:搭配Parallel Scavenge;CMS的后备预案,在发生Concurrent Mode Failure的时候使用

    4.5 Parallel Old收集器

    • 标记-整理算法
    • 可以与Parallel Scavenge组合,在注重吞吐量及CPU资源敏感的场合比较适用

    4.6 CMS收集器

    • 以获取最短回收停顿时间为目标的收集器
    • 基于标记-清除算法
    • 运作过程包括:
      初始标记:Stop the World;标记GC Roots直接关联的对象
      并发标记:Stop the World;GC Roots Tracing;时间较长
      重复标记:修正并发标记期间,因为程序继续运行而产生的关联
      并发清除:时间较长
    • 优点:
      停顿时间短
      并发收集
    • 缺点:
      1.对CPU资源非常敏感,由于占用了一部分CPU资源而导致应用程序变得缓慢
      2.无法处理浮动垃圾,可能出现Concurrent Mode Failure而导致另一次Full GC的出现

    浮动垃圾:垃圾收集阶段程序还需要运行而产生的垃圾

    1. 标记-清除算法可能会导致大量空间碎片

    4.7 G1收集器

    与CMS收集器相比的改进:

    1. 采用标记-整理算法,不会产生时间碎片
    2. 可以非常精确地控制停顿,达到实时Java

    4.8 参数总结

    JVM - 垃圾收集器参数总结

    5. 内存分配和回收策略

    5.1 对象优先在Eden分配

    public class TestAloocation {
        private final static int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
            byte[] allocation1, allocation2, allocation3, allocation4;
            allocation1 = new byte[4 * _1MB];
            allocation2 = new byte[4 * _1MB];
            allocation3 = new byte[4 * _1MB];
            allocation4 = new byte[2 * _1MB];
    
    
        }
    }
    
    优先给Eden分配

    5.2 大对象直接进入老年代

    • 大对象是指需要大量连续空间的对象

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

    • 对象年龄计数器:Eden区域经过MinorGC进入Survivor,年龄为1;而后Survivor每经过一轮MinorGC就年龄+1

    5.4 动态对象年龄判定

    • 如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,则将它们送入老年代,无需等待ThreshHold

    5.5 空间分配担保

    在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的平均大小是否大于剩余空间大小,如果大于,则改为直接进行一次Full GC;如果小于,则查看HandlerPromotionFailure是否开启,如果开启则进行Minor GC;没有开启,则会进行Full GC

    • 当大量对象在Minor GC之后依然存活,Survivor空间进不去,这时需要老年代空间分配担保,让它们直接进入老年代
    • 取之前每一次回收晋升到老年代容量的平均大小值作为经验值,与老年代的剩余空间作对比,决定是否进行Full GC让老年代有更多的空间来进行担保;可能会出现Handler Promotion Failure

    相关文章

      网友评论

          本文标题:垃圾收集器和内存分配策略

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