书成

再这样堕落下去就给我去死啊你这混蛋!!!

0%

Java垃圾收集机制

判断对象是否可回收

 判断一个对象是否可以被回收,即还有没有其它指针指向它,如果没有了,说明这个对象已经不可以再被使用到了,就可以回收掉它的内存空间。主要有两种方式可以判断:

 - 引用计数法:每当有一个引用引用到对象,就给对象的引用计数加一,这样当某个对象的引用计数为0时,就说明该对象已经可以被回收了,但是该方法有循环引用的问题。

 - 可达性分析算法:以GC Roots对象为起点开始搜索引用链,凡是能搜索到的对象都是存活的对象,未能搜索到的对象都是可回收的对象。

 Java虚拟机中多使用可达性分析算法,其中GC Roots对象有4类:

  • 方法区中的常量引用的对象。
  • 方法区中静态变量引用的对象。
  • 栈中局部变量引用的对象。
  • 本地方法栈中引用的对象。

 Java中对象有四种引用:强引用,一定不会被垃圾收集;软引用,空间不足时会被收集;弱引用,下一次垃圾收集就被收集;虚引用,不会对对象的生命周期产生任何影响。

对类的回收

 虚拟机对类的回收十分严苛,需要满足三个条件,并且满足了也不一定会回收:

  1. 类的所有实例已经被回收。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的Class对象没有被引用,不能通过反射获得该类。

垃圾收集算法

  1. 标记-清除算法:虚拟机标记可达对象后,将不可达的对象统一清除。这种算法效率比较低,而且容易造成空间碎片。
  2. 标记-整理算法:虚拟机将可达的对象全部移动到内存的一侧,将边界以外的空间一起释放。好处是无内存碎片,缺点是效率低。
  3. 复制算法:将内存分两块,只使用一块,在垃圾收集时将存活对象移动到另一块即可。好处是效率高,但是内存只使用到一半。虚拟机内实现一般分为一块Eden与两块Survivor,比例8:1:1,每次使用Eden与一块Survivor,留一块Survivor做垃圾收集时的复制空间,这样可以提高空间利用率。

Stop the world

    由于不能出现可达性分析过程中对象引用关系还在不断变化的情况,因此GC时必须停顿所有的Java执行线程,会影响系统响应以及用户体验,在HotSpot实现中,使用一组称为OopMap的数据结构来存储哪些地方存放着对象引用,这样可以减少GC时扫描全局引用的时间。
    HotSpot并没有为每条指令都生成OopMap,只在“特定的位置”记录,这些位置称为安全点,因此程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能。但还有一个问题需要考虑,如何在GC时让所有线程都跑到最近的安全点上停顿,有两种方式:

  • 抢先示中中断,不需要线程的执行代码配合,在GC时,中断所有线程,如果发现有线程中断的地方不在安全点上,就让它跑到安全点上,现在几乎没有虚拟机采用这种方法。
  • 主动式中断,当GC需要中断线程时,设置一个标志,各个线程主动轮询这个标志,发现为真时自己中断挂起。

    当线程处于挂起状态时,无法响应中断请求,这种时候需要安全区域来解决。安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方开始GC都是安全的,安全区域可以看作是扩展的安全区域。

垃圾收集器

 很多垃圾收集器采用分代收集的方式,将堆中的内存分为新生代和老年代,新生代的对象通常存活时间较短,回收比较频繁,采用效率高的复制算法,老年代的对象存活时间很长,回收不频繁,采用标记-清除或标记-整理算法。

  • Serial 与 Serial Old 收集器:单线程收集器,垃圾收集时需要暂停其它工作线程,直到收集结束。主要用在Client模式下的虚拟机。
  • ParNew 收集器: Serial收集器的多线程版本,主要用在Server模式的虚拟机上,能与CMS收集器配合工作。
  • Parallel Scavenge 与 Parallel Old 收集器,多线程吞吐量优先收集器。

CMS收集器

 目标是获得最短的回收停顿时间,基于标记清除算法实现,在Server端使用比较多。运作过程包括初始标记,并发标记,重新标记与并发清除。初始标记只标记GC Roots能直接关联到的对象,速度很快;并发标记就是进行可达性分析的过程;重新标记修正并发标记期间引用关系发生变化的部分;并发清除则是清除不可达对象,回收空间。

 整个过程中,并发标记与并发清楚可以和用户线程一起工作,因此停顿时间只有初始标记和重新标记的过程,但时间并不长。

 CMS收集器的缺点是有空间碎片产生,无法处理浮动垃圾以及对CPU资源十分敏感。

G1收集器

 G1收集器主要应用在Server端。它将对划分为多个大小相等的独立区域,新生代和老年代不再物理隔离。它会跟踪每个区域垃圾堆积的大小,维护一个优先列表,回收时回收价值最大的区域。运作工程包括初始标记,并发标记,最终标记,筛选回收。由于可以控制回收多少块区域,因此可以得到一个可预测的停顿时间

对象内存分配

内存分配策略

  • 对象优先在新生代的Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代,可以设定年龄阈值或者动态年龄判断

分配内存

 在堆上为对象分配内存是,如果内存是绝对规整的,可以使用指针碰撞,直接让指针偏移出对象内存大小即可。如果内存不规则,需要维护一个空闲列表,从列表中选择能满足对象内存需求的内存块分配给对象。

 对象在分配时,也需要保证线程的安全,通常本地线程会预先分到一块内存空间作为本地线程缓冲,该线程创建的对象都在这块空间内分配,当缓冲用完后,需要申请新的空间时才需要同步锁定。

Minor GC 与 Full GC

 Minor GC 主要回收新生代的空间,Major GC 主要回收老年代空间,Full GC 回收老年代与新生代与元空间。

 触发full gc 的条件:

  • 调用System.gc() 方法,但是不一定会执行
  • 老年代空间不足
  • 空间分配担保失败
  • 永久代( jdk 1.7 之前) 或 元空间不足
  • CMS GC 发生错误

finalize()方法

 对象被回收时,如果对象覆盖了Object类的finalize()方法,虚拟机会在回收前执行对象的finalize()方法,但是不保证什么时候开始执行,也并不保证一定会执行完这个方法。

元空间

在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。