垃圾收集器和内存分配

  • 判断对象是否还存活

    • 引用计数法(未被采用,有缺陷):

      每当有引用时,计数器就加一,当引用失效时,计数器就减一

      通过代码来证明有缺陷:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class GcDemo {
      public static void main(String[] args) {
      //分为6个步骤
      GcObject obj1 = new GcObject(); //Step 1
      GcObject obj2 = new GcObject(); //Step 2

      obj1.instance = obj2; //Step 3
      obj2.instance = obj1; //Step 4

      obj1 = null; //Step 5
      obj2 = null; //Step 6
      }
      }

      class GcObject{
      public Object instance = null;
      }
      • Step1:GcObject实例1的引用计数加“1,实例1的引用计数=1;

      • Step2:GcObject实例2的引用计数加1,实例2的引用计数=1;

      • Step3:GcObject实例2的引用计数再加1,实例2的引用计数=2;

      • Step4:GcObject实例1的引用计数再加1,实例1的引用计数=2;

      • Step5:栈帧中obj1不再指向Java堆,GcObject实例1的引用计数减1,结果为1;

      • Step6:栈帧中obj2不再指向Java堆,GcObject实例2的引用计数减1,结果为1。

      这样便得不到释放,从而导致内存泄露

    • 可达性分析算法:

      通过一系列的成为GC Root的对象作为起始点,从这些节点开始向下搜索,搜索过的路径叫做引用链,当一个对象到GC Root没有任何引用链链接,就可以证明此对象是不可用的了

      在Java语言中,可作为GC Root的对象包括以下几种:

      • 虚拟机栈中引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象
      • 本地方法栈中JNI(即一般说的Native方法)引用的对象
    • 引用分类:

      • 强引用:Dog dog = new Dog();这种只要强引用还在,垃圾处理器永远不会回收掉被引用的对象

      • 软引用:有用但非必须的对象,这种对象,在系统要发生内存溢出异常之前,会把这些对象进行二次回收,要是还是没有足够内存才抛出异常,可以用来实现高速缓存

        picture 8

        • 弱引用:用来描述非必需的对象的,比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集前,只要垃圾回收工作就会把这部分给回收掉 dog = null

        • 虚拟引用:对生存时间没有任何影响,他的作用是能在对象被回收时,收到一个系统通知

      • 判断生存还是死亡过程

        真正判断一个对象死亡,需要经历两次标记过程,如果通过可达性分析后发现与GC Root没有关联了,则会被第一次标记,并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法,当此对象没有覆盖此方法或者已经被虚拟机调用过了,就没必要再执行了

      • 回收方法区:

        方法区主要回收两部分内容:废弃常量和无用类

        判断废弃常量:没有任何对象引用常量池中的字面量

        判断无用类:

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

    • 标记-清除算法

      1. 简介:就是标记出要回收的对象,标记完成后同意回收所有被标记的对象

      2. 优缺点:

        优点:简单;

        缺点:效率,标记和清除这两个效率都不高;空间问题:标记清除后可能会产生大量的内存碎片

    • 复制算法

      1. 简介:讲可用内存分为大小相等的两块,每次使用其中的一块,当这一块用完后,就将继续还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收的

      2. 优缺点:

        优点:每次都是对整个半区进行内存回收的,不用考虑内存碎片等复杂情况,只要一动堆顶指针,按顺序分配内存即可,实现简单,运行高效

        缺点:代价是将内存缩小为原来的一半,代价太高

      3. 使用:现在商业虚拟机使用这种算法来回收新生代,但并不是1:1划分空间,新生代对象98%是朝生夕死,将内存分为一块较大的Eden空间和两块较小的survivor空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性的复制到另外一个Survivor空间上,最后清理掉Eden和刚才那个Survivor,HotSpot默认比例是8:1,当Survivor空间不足时,需要依赖老年代进行分配担保。新生代一般选用这种算法

    • 标记-整理法

      1. 简介:先标记,把所有存活的对象移向一端,直接清理掉边界以外的内存
      • 分代收集算法

      现在虚拟机的垃圾收集都采用这种算法,不同的特点用不同的算法,比如新生代用复制算法,老年代存活率高就用标记-清理或标记-整理算法

  • 垃圾处理器

    根据年轻代还是老年代来使用不同的垃圾处理器

picture 9

  • 年轻代:

    1. Serial收集器(-XX:+UseSerialGC,复制算法)clien模式下年轻代默认的

      • 单线程收集,进行垃圾回收时,必须暂停所有工作线程
      • 简单高效,Client模式下默认的年轻代处理器picture 10
    2. ParNew收集器(-XX:+UseParNewGC,复制算法)Server模式下年轻代首选收集器

      • 多线程收集,其他和Serial收集器一样,除Serial外只有它能与CMS收集器配合工作
      • 单核执行效率不如Serial,在多核下执行才有优势
      • picture 11
    3. Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)并行

      • 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
      • 比起关注用户线程的停顿时间,更关注系统的吞吐量(适合在后台,而不是太多交互)
      • 在多核下执行才有优势,Server模式下默认的年轻代处理器
      • 处于一个比较尴尬的状态新生代选择了Parallel Scavenge 后老年代除了SerialOld收集器外无其他选择
      • picture 12
  • 老年代:

    1. Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)

      • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
      • 简单高效,Client模式下默认的老年代收集器
      • 在server模式下:1.5之前版本与Parallel Scavenge收集器搭配使用,另一个用途是作为CMS收集器的后备方案
    2. ParallelOld收集器(-XX:+UswPallelOld,标记-整理)

      • 多线程,吞吐量优先
      • 是ParallelScavenge收集器的老年代版本
    3. CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)

      垃圾回收线程几乎可以做到与用户线程同时工作,是一种获取最短回收时间为目标的收集器

      有更多存活时间的对象

      步骤:

      ​ 初始标记-并发标记(并发追溯标记,程序不会停顿)-并发预处理(查找执行并发标记阶段从年轻代晋升到老年的的对象)-重新标记(暂停虚拟机,扫描CMS堆中的剩余对象)-并发清除(清理垃圾对象,程序不会停顿)-并发重置(重置cms收集器的数据结构)

      picture 13

    4. G1收集器(-XX:+UseG1GC,复制+标记-整理算法)

      • 特点:
        1. 并发和并行 2. 分代收集 3. 空间整合 4. 可预测的停顿
      • 简介:
        • 将整个Java堆内存分为多个大小相等的Region
        • 年轻代和老年代不再隔离
  • 内存分配与回收策略

    1. 对象优先在Eden分配

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

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

      大对象指需要大量连续内存空间的Java对象,最典型的大对象是那种很长的字符串以及数组

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

      虚拟机给每个对象设定年龄,,如果对象在Eden出生并且经过第一次Minor GC后仍然存活,并且能被survivor容纳的话,就被移动到Survivor中,并且年龄设为1,每熬过一次MinorGC年龄就会加1,达到要求就可以进入老年代了

    4. 动态对象年龄判定

    如果相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,不用达到设置年龄

    1. 空间分配担保

    在Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立,则操作是安全的,不成立看下是否允许担保失败,如果允许则检查老年代剩余空间是否大于历次的老年代对象的平均水平,如果大于则尝试进行MinorGC,如果小于则,要进行一次Full GC

  • 常见面试题总结

    1. finalize是否与c++中的析构函数相同

      • 不同,析构函数调用时确定的,而他是不确定的

      • 当垃圾回收器宣告一个对象死亡时,至少要经过两次的标记过程:没有与GCRoot连接,第一次标记,并且判断是否执行finalize方法,如果重写了finalize方法且未被引用,这个对象就会被放置在F-Queue队列中,并由虚拟机去执行finalize方法;

      • 优先级比较低,触发该方法后可能随时被终止

      • 给予对象最后一次重生机会

      例子:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public class Finalization {
      public static Finalization finalization;
      @Override
      protected void finalize(){
      System.out.println("Finalized");
      finalization = this;
      }

      public static void main(String[] args) {
      Finalization f = new Finalization();
      System.out.println("First print: " + f);
      f = null;
      System.gc();
      try {// 休息一段时间,让上面的垃圾回收线程执行完成
      Thread.currentThread().sleep(1000);
      } catch (InterruptedException e){
      e.printStackTrace();
      }
      System.out.println("Second print: " + f);
      System.out.println(f.finalization);
      }
      }
    2. Java中的强引用,软引用,弱引用,虚引用有什么用

参考:https://blog.csdn.net/LAMP_zy/article/details/53212909