一个对象从创建到垃圾回收的过程

一、对象创建

  • new指令、类加载

当Java虚拟机遇到一条字节码new指令时,首先检查这个指令的参数是否能在常量池中定位到一个符号引号,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。否则先执行相应的类加载过程。

  • 分配内存

对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存块从Java堆中划分出来。分配内存采用的方式由Java堆是否规整决定:使用Serial、ParNew等带有空间压缩整理功能的垃圾收集器则使用指针碰撞的分配算法;使用CMS这种基于标记-清除算法的收集器则采用空闲列表来分配内存。

  • 分配内存的线程安全

一种是对分配内存空间的动作进行同步处理————实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是每个线程预先在Java堆中分配一小块内存,成为本地线程分配缓冲(TLAB),线程在本地的缓冲区分配内存,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。是否使用TLAB由参数-XX:+/-UseTLAB参数设定

  • 内存空间初始化

将分配到的内存空间都初始化为零值(不包括对象头),这步操作保证了对象的实例字段在Java代码中可以不赋初值就可以使用。还会对对象头进行设置:对象是哪个类的实例、类的元数据信息、对象的哈希码、对象的GC分代年龄等信息

  • 对象初始化

执行Class文件中的()方法,也就是构造函数,按照程序员的意愿对对象进行初始化

  • 逃逸分析

  1. 原理:

分析对象的动态作用域,当一个对象在方法里面被定义后,可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸。甚至可能被外部线程访问到,例如赋值给可以在其他线程中访问的实例变量,这种弄个称为线程逃逸。 如果能证明在别的方法或线程无法通过任何途径访问到这个对象,或者只逃逸出方法不逃逸出线程,则可以为这个对象实例采用不同的优化。

  1. 栈上分配和标量替换

栈上分配:如果确定一个对象不会逃逸出线程之外,可以让这个对象在栈上分配内存,对象所占用的内存空间随着栈帧出栈而销毁。栈上分配可以支持方法逃逸,不支持线程逃逸。

标量替换:把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问。假如能够证明这个对象不会被方法外部访问,并且这个对象可拆散,那么可以不去创建这个对象,而是直接创建它的若干个被这个方法使用的成员变量来代替。对象拆分后,可以让对象的成员变量在栈上分配和读写

同步消除:如果能确定一个变量不会逃逸出线程,那么这个变量的读写就不存在竞争,对这个变量实施的同步措施可以消除。

二、垃圾回收过程(以HotSpot虚拟机为例)

  • 根节点枚举

在JVM中使用可达性分析算法来判断对象是否存活。固定作为GC Roots的节点主要在全局性的引用(常量、类静态属性)与执行上下文(栈帧中的本地变量表)中,而根节点枚举这一步骤必须暂停用户线程(Stop The World)。并不需要检查所有的执行上下文和全局引用,而是在特定的位置使用特殊的数据结构(OopMap) 记录栈和寄存器里哪些位置是引用,以此快速准确地完成根节点枚举。

  • 安全点和安全区域

记录了引用所在位置的特定位置称为安全点。也就是说,用户线程必须执行到安全点之后才能暂停,如何在垃圾收集发生时让所有的线程都运行到最近的安全点再停顿?有抢先式中断和主动式中断两种选择。通常虚拟机会采用后者:当垃圾收集需要中断线程时,简单地设置一个标志位,各个线程执行过程中不断轮询这个标志,一旦发现标志为真时就在最近的安全点上主动中断挂起

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。主要针对处于Blocked/Sleep状态下无法响应中断的线程

  • 跨代引用

记忆集:一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。记忆集有三种实现精度:字长精度、对象精度、卡精度。 卡表:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

  • 查找引用链、增量更新和原始快照

查找引用链需要从根节点开始遍历整个对象图,这个过程需要和用户线程并发运行以减少垃圾回收产生的停顿时间。而为了解决并发标记期间引用关系发生变化带来的问题,有两种解决方案:增量更新和原始快照。 简单来说,前者记录下插入的新引用,并发结束后重新扫描;后者记录下要删除的引用记录,并发结束后重新扫描。

  • 垃圾回收算法

  1. 标记-清除:简单、非移动式、内存碎片化、内存分配复杂

image.png 2. 标记-复制:无内存碎片、浪费空间

image.png 3. 标记-整理:移动式、无空间碎片化、STW、内存回收复杂

image.png

三、垃圾收集器

image.png

可以看到,其中Serial、ParNew、Parallel Scavenge作用于新生代(标记-复制算法),CMS(标记-清除)、Serial Old(标记整理)、Parallel Old(标记整理)作用于老年代

  • Serial系列

image.png 特点: 单线程、使用场景为客户端、单核

  • ParNew

Serial的多线程并行版本,可与CMS收集器配合、适用于多核服务器

  • Parallel系列

特点:达到可控制的吞吐量、适用于注重吞吐量但少交互的场景

  • CMS

特点

以获取最短回收停顿时间为目标,适用于关注响应速度、交互体验的服务器

过程

  1. 初始标记:标记GC Roots直接关联的对象(STW)
  2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图
  3. 重新标记:修正并发期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录(STW)(增量更新)
  4. 并发清除:清除标记阶段判断的已经死亡的对象

缺点

对处理器资源敏感、浮动垃圾、预留空间给用户程序、内存碎片

  • g1

特点:

  1. 基于Region的内存布局:把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region可以作为新生代或老年代
  2. 可预测的停顿时间:后台维护优先列表,每次优先处理回收价值收益最大的Region
  3. 空间整合:整体采用标记-整理,局部采用标记-复制
  4. 分区动态变化:默认新生代在堆中占比5%,可动态调整比例,最多占比不超过60%
  5. Humongous区域:专门存储大对象,大多数情况下作为老年代的一部分看待
  6. 作用区域:不像之前的垃圾收集器要么作用于新生代要么老年代,G1会对新生代和部分老年代进行垃圾回收

过程:

  1. 初始标记:标记GC Roots直接关联的对象,修改TAMS指针(STW)
  2. 并发标记:从GC Root开始对堆中对象进行可达性分析
  3. 最终标记:处理并发阶段结束后遗留的SATB(原始快照)记录
  4. 筛选回收:对各个Region的回收价值和成本进行排序,根据设定的停顿时间选择多个Region构成回收集(CSet),采用标记-复制和标记整理算法清理整个旧Region的空间(STW)

缺点:

内存占用高:每个Region都有一份卡表

执行负载:写屏障操作更为复杂(队列异步处理)、原始快照产生跟踪引用变化带来的额外负担

猜你喜欢

转载自juejin.im/post/7068274353762205733