Java 垃圾回收与内存分配

Java 垃圾回收与内存分配

graph TB A[垃圾回收] B[探活] C[引用计数] D[可达性分析] E[四类引用] F[GC Roots] G[垃圾收集] H[两次标记] I[方法回收3] J[finalize] K[<b>分代</b>] L[<b>老年代</b><br/>标记-整理<br/>标记-清除] M[<b>新生代</b><br/>复制 118] N[HotSpot<br/>OopMap<br/>见下] A --> B B --> C B --> D D --> E D --> F A --> G G --> H G --> I H --> J G --> K K --> L K --> M G --> N

[TOC]html

对象探活

对象探活的目的在于找到那些须要清理的对象java

对象探活常见方法有引用计数和可达性分析等。使用引用计数法(相似于 C++ 中的智能指针 shared_ptr)实现对象探活相对容易,但没法解决对象之间相互循环引用的问题,例如算法

class A{
	public A element;
	...
}

A a, b;
a.element = b;
b.element = a;

可达性分析 & GC Roots

Java 中可达性分析与 GC Roots 是息息相关的。Java 是面向对象的语言,全部对象之间都使用引用进行关联(底层实现通常是 C/C++ 中的指针)。从软件启动开始,全部的对象都会有一个“父对象”,全部的对象都是由父对象建立的,能够认为 JVM 是始祖对象。Java 启动且未进入main 函数前初始化的对象能够认为其父对象是 JVM;进入 main 函数后建立的全部对象能够认为其父对象是 main 函数所在的对象。因而可知 Java 中全部对象之间的关系可使用多叉树进行表示,这些树的根节点就是垃圾回收扫描的起点(不考虑 JVM),也就是 GC Roots安全

全部对象的父对象均可以追溯到 JVM,但垃圾回收时不将 JVM 当作起点数据结构

GC Roots 通常有如下 4 类:多线程

  1. 虚拟机栈中引用的对象
  2. 方法区静态属性引用的对象
  3. 方法区常量引用的对象
  4. 本地方法栈中 JNI(Java Native Interface,原生方法)引用的对象

引用分类

Java 1.2 后引用被分为如下 4 类,“引用强度”由强到弱分别为:并发

  1. 强引用

强引用就是指在程序代码之中广泛存在的,相似Object obj= new Object()这类的引用,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象框架

  1. 软引用

软引用是用来描述一些还有用但并不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行第二次回收(可使用 SoftReference 函数建立)函数

  1. 弱引用

弱引用也是用来描述非必需对象的,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前(WeakReference)oop

  1. 虚引用

为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知(PhantomReference)

垃圾回收

垃圾回收会涉及到对象探活与内存整理。由于 GC 过程很难保证正在移动的对象没有被其余执行中的线程所引用与修改,因此这个过程通常须要整个 JVM 中止执行工做线程,Sun 将这件事称为 Stop The World。虚拟机暂停执行工做线程会影响业务性能,因此要尽量减小垃圾回收形成的系统停顿时间。使用句柄来访问对象时能够只停顿与将要 GC 的对象相关联的线程,其余线程照样执行,但使用句柄访问对象效率不高,这类方法将 GC 时间分摊到了工做进程执行过程

垃圾回收常见算法

分代收集算法

根据对象存活周期的不一样将内存划分为几块。通常把 Java 堆分为新生代(刚建立没多久的对象)和老年代(已经存在好久的对象,一些比较大的对象也默认是老年代),这样就能够根据各个年代的特色采用最适当的收集算法。例如新生代对象在每次垃圾回收时只有少许对象存活,可使用下面说起的复制算法;对于存活时间长的老年代对象可使用下面说起的“标记-清理”或者“标记-整理”算法

标记-清除

具体过程和下面的两次标记过程相似,缺点是效率低且会出现内存碎片问题

两次标记

若是对象在进行可达性分析后发现没有与 GC Roots 相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过期,虚拟机将对象视为“没有必要执行”,不然是“有必要执行”

若是这个对象被断定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫作 F-Queue 的队列之中,并在稍后由一个由虚拟机自动创建的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行” 是指虚拟机会触发这个方法,但并不承诺会等待它运行结束

finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,若是对象要在 finalize() 中成功拯救本身——只要从新与引用链上的任何一个对象创建关联,那么对象将被移出 F-Queue,并被认为“活着”

对象的 finalize() 方法只会被调用一次,在 Java 中尽可能不要使用 finalize 方法

标记-整理

标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存

复制

基础的复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,代价比较大

从统计上来说,在进行一次垃圾回收前,有 98%新生代对象都是可回收的,因此不必把内存分为大小相等的两份

大部分商用虚拟机使用下述方法回收新生代对象

HotSpot 虚拟机将新生代内存分为三个部分:80% 的 Eden 和两个 10% 的 Survivor,建立对象时优先在 Eden 中建立。每次垃圾回收时由于 98% 的对象均可以删除,因此大部分状况下 10% 的内存便可存储全部存活的对象。垃圾回收前虚拟机只会使用 Eden 和其中一个 Survivor,回收时将 Eden 和被使用的 Survivor 中的对象移到另外一个未被使用的 Survivor 中

当一个 Survivor 保存不了余下的对象时,会触发分配担保(下面有介绍)

方法回收

类(方法,位于静态内存区)须要同时知足三个条件才算可回收类

  1. 该类全部实例均被回收
  2. 加载该类的 ClassLoader 已经被回收
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,即没法在任何地方经过反射访问该类的方法

对方法是否进行回收,虚拟机提供了配置选项。在大量使用反射、动态代理、CGLibByteCode 的框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都须要虚拟机具有类卸载的功能,以保证静态区不会溢出

HotSpot & OopMap 垃圾回收实现

graph LR A[HotSpot&OopMap] B[准确&保守] C[安全点<br/>安全区域] D[抢占&主动] E[回收器实现] F[Serial,ParNew,Parallel<br/>CMS,Serial old,Parallel Old<br/>G1] I[其余] J[GC日志] K[GC 原则] L[分配担保] A --> B B --> C C --> D A --> E E --> F A --> I I --> J I --> K K --> L

保守 & 准确

JVM 的垃圾回收分为保守式 GC 和准确式 GC,这里的保守和准确指的是 JVM 在垃圾回收时是否肯定当前字段为引用类型。下面作详细的介绍,参考

  • 保守式 GC

    保守式 GC, JVM 不记录变量类型信息。每次进行垃圾回收时扫描全部 GC Roots 区域(本地方法栈、JVM 栈、静态方法区等),若是发现一个疑似指针变量(如真实的指针、整数等),JVM 都会检查堆中是否存在对象,若是存在就将对象信息保存在内存分配表中以标识某一段内存已被对象占用,不然就从内存分配表中删除这段内存以供下次分配。若是恰巧一个整数类型和一个指针所指向的地址相同,不管这个对象是否还“存活”,保守式 GC 都会保留这块内存,由于 JVM 不能肯定这个变量不是指针。由于同一个对象可能在不一样的栈帧中被使用且 JVM 又没法区分变量类型,因此 JVM 不能修改栈中“地址”的值(万一一个整型值和指针的值恰好相同),故保守式 GC 不能移动对象,只能使用相似标记-清除的算法法回收内存。若是 JVM 使用句柄(另外一种为直接内存访问)的方式访问对象,则保守式 GC 也能够移动对象,但本该回收的对象依旧存在的现象没法消除

  • 半保守式 GC

    JVM 中栈上的变量通常不包含类型信息,但堆中的对象能够包含类型信息(例如反射等,这个和 C++ 中的虚函数表概念相似),因此垃圾回收时堆对象中的指针类型是能够肯定的,此时就不存在上面整型和指针类型不分的状况,堆上的指针能够实现准确式回收,且能够移动对象

  • 准确式 GC

    准确式 GC 指 JVM 进行内存回收时能够肯定指针的类型与位置,这通常须要辅助的数据结构与存储空间,HotSpot 中这些数据存储在 OopMap 中

OopMap

OopMap 是 HotSpot 实现准确式垃圾回收的基础,HotSpot 中的 GC Roots 通常保存在 OopMap 中,这样 JVM 在 GC 时就不用扫描全部静态区和栈帧,栈帧在运行时动态变化,因此 OopMap 的内容也在不断的变化

在类文件载入和 JIT(Just-In-Time Compiler)编译过程当中 OopMap 都有发生变化的可能

什么时候更新 OopMap ?

何时或者说代码有什么特色时应该更新 OopMap?总不能编译器每执行一条指令就判断一下是否须要更新 OopMap吧,这样效率过低了

安全点

GC 须要在更新 OopMap 后才能进行, GC 时的 OopMap 已经包含了全部 GC Roots,且对象之间的引用关系不会在 GC 过程当中发生变化,因此 JVM 并不能随意在任何位置进行垃圾回收,JVM 进行垃圾回收的位置须要知足必定的条件,这些知足条件的位置被称为安全点。常见的安全点有:方法调用循环跳转异常跳转等,这些点都不会改变对象之间的引用关系

垃圾回收须要全部线程都运行到安全点,通常有两种方式

  • 抢先式中断(少见)

    JVM 中止全部线程,若是一个线程没有运行到安全点就恢复其运行,直至到达安全点

  • 主动式中断(常见)

    JVM 设置一个标志位,当线程到达安全点时检查这个标志并自动中止运行

安全区域

若是线程阻塞了,或者 sleep,线程将没法执行到安全点,则 JVM 没法进行 GC,此时就须要安全区域的概念。所谓的安全区域,是指引用关系不会发生变化的指令段。线程进入安全区域时会给出一个标识,以供 JVM 查询

常见垃圾回收器

从垃圾回收器出现至今并无出现一款通用的、在任何场景下性能都很是出众的实现,因此大部分请求下须要在不一样的场景下选择不一样的垃圾回收器,具体场景具体分析

新生代收集器

Serial

单线程垃圾收集器,收集时须要中止全部工做线程

ParNew

Serial 的多线程版本,能够配合 CMS 使用

Parallel Scavenge

使用复制算法的多线程收集器。其余算法关注如何减小垃圾回收时间,当前算法控制垃圾回收时间和工做线程工做时间的比值。你能够设置一个小的比值,则新生代会占用更多的内存;你能够设置一个大的比值,新生代会占用更少的内存,固然回收时间会变长

老年代收集器

CMS(基于标记清除)

CMS(Concurrent Mark Sweep)是一种追求最短停顿为目标的收集器,特色是并发低停顿收集器,老年代推荐使用

CMS 能够在用户进程运行时进行垃圾回收,此时用户进程产生的垃圾被称为浮动垃圾,CMS 只能等待下次回收时回收这些垃圾

通常在老年代内存被占用超过必定比例时才会触发 CMS 垃圾回收,提升比例能够减小垃圾回收的次数

Serial old(MSC)

Serial 的老年代版本

Parallel Old

Parallel Scavenge 的老年代版本

混合代垃圾回收

G1

将来可能替代 CMS,G1 能够同时用于新生代与老年代

GC 日志

不一样垃圾回收器的日志格式不一样,但有必定的共性

33.125:[ GC[ DefNew: 3324K- > 152K( 3712K), 0. 0025925 secs] 3324K- > 152K( 11904K), 0. 0031680 secs]   
 
100.667:[ Full GC[ Tenured: 0 K- > 210K( 10240K), 0. 0149142secs] 4603K- > 210K( 19456K),[ Perm: 2999K- > 2999K( 21248K)], 0. 0150007 secs][ Times: user= 0. 01 sys= 0. 00, real= 0. 02 secs]

以第一行为例:

  • 33.125 表示 GC 发生事件,从 JVM 启动到当前的秒数
  • DefNew 表示新生代垃圾回收,不一样垃圾回收器使用的关键字不一样
  • 3324k->152k(3712k),表示 GC 前该区域已使用容量,和垃圾回收后该区域所使用容量,括号内为当前区域总容量
  • 0.0025925 secs 表示本次新生代垃圾回收所用时间
  • 3324k -> 152k(11904k),GC 前 JVM 使用的堆容量,和 GC 后 JVM 使用的堆容量,圆括号内为总容量
  • 0.0031680,为本次垃圾回收总耗时

部分 GC 原则

  • 对象优先在 Eden 分配
    • 若是 Eden 空间不足,则发起一次 minor GC
    • 老年代 GC (Major GC/Full GC),速度比 Minor GC 要慢 10 倍以上
  • 大对象直接进入老年代
    • 避免新生代大量内存复制,新生代通常使用复制算法进行垃圾回收
  • 长期存活的对象将直接进入老年代,能够设置一个时间,当对象存活时间超过这个值就扔进老年代
  • 动态对象年龄判断
    • Survivor 空间中先相同年龄全部对象大小总合大于 Survivor 空间的一半时,年龄大于或等于该年龄的对象将进入老年代
  • 空间分配担保
    • 新生代使用复制算法,当一个 Survivor 没法保存全部新生代对象时,须要将部分对象保存到老年代内存中,因此在进行 minor GC 前通常须要查看老年代可用空间是否大于新生代全部对象所用总空间,若是是就能够确保 minor GC 能够成功;不然通常会触发 Full GC 腾出空间或者其余机制,保证 Minor GC 不会出现大问题
    • 具体介绍可参考周志明《深刻理解 Java 虚拟机》
相关文章
相关标签/搜索