【JVM系列】JVM对象创建与垃圾回收

JVM一直是面试中经常问到的知识点,除此之外,在项目性能调优上,也需要对其十分了解。本系列会从内存模型,再到整个对象的创建流程以及垃圾回收等,将JVM的知识以及面试常见题型全部讲解一遍。

【JVM系列】JVM内存模型详解

JVM一直是面试中经常问到的知识点,除此之外,在项目性能调优上,也需要对其十分了解。本系列会从内存模型,再到整个对象的创建流程以及垃圾回收等,将JVM的知识

在上篇的最后,展示了一张JVM从对象创建到GC的流程图,并且还提到了垃圾回收相关的知识,本篇会进行详细的介绍。

首先,我们把这张图再扩展一下,从对象创建的完整流程开始说起,那可以得到下面的完整流程图。

JVM对象创建

先看到最左边,介绍一下对象创建的整体流程。

  1. 类加载检查:虚拟机遇到一条new指令时,首先将去检查这个 指令的参数 是否能在常量池中 定位到 这个类的符号引用,并且通过检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须现执行相应的类加载过程。
  2. 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可以确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。分配方式根据 Java堆内存是否绝对规整 分为两种方式:“指针碰撞”和“空闲列表”。
  3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,保证对象的实例字段在java代码中可以不赋初始值就直接使用。
  4. 设置对象头:初始化零值之后,虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例等信息。这些信息存放在对象头中。
  5. 执行init方法:在上面的工作都完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但是从java程序的角度来看,还需要进行初始化工作。

其中,在分配内存时,根据Java堆内存是否绝对规整,分为两种方式:指针碰撞、空闲列表。

规整:已使用的内存在一边,未使用内存在另一边;不规整:已使用的内存和未使用内存相互交错。如下图所示。

指针碰撞

已使用内存在一边,未使用内存在另一边,中间放一个作为分界点的指示器。那么,分配对象内存 = 把指针向 未使用内存 移动一段 与对象大小相等的距离。如下图所示。

空闲列表

虚拟机维护着一个 记录可用内存块 的列表,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

 

左边介绍完毕,再来看看右边。不论是在执行Minor GC还是Full GC,首先都要判断对象是否可以回收。所以涉及到了存活判断的问题。

GC存活判断

有两种方法:

引用计数法

在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。但如果发生循环引用,对象的引用计数永远不会为0,结果这些对象就永远不会被释放。所以主流的Java虚拟机不采取这种方法。

可达性分析

从GC Roots 为起点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的,即不可达对象。这类对象是会被回收掉的。

GC Roots 是指:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

Java中的引用

上面有提到“引用”这个概念,在Java中对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用,这4种引用强度依次逐渐减弱。

这样子设计的原因主要是为了描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。也就是说,对不同的引用类型,JVM 在进行GC 时会有着不同的执行策略。所以我们也需要去了解一下。

强引用

例如:

Map<String, Object> map = new HashMap<>(1);

只要强引用存在,垃圾收集器永远不会回收被引用的对象,只有当引用被设为null的时候,对象才会被回收。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。

软引用

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。

软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

虚引用

是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

垃圾回收算法

好了,前面铺垫了一堆内容,终于进入重点了。不论是Minor GC还是Full GC,都会进行内存的回收。而回收算法,有以下三种。

标记-清除算法

首先标记所有需要回收的对象,然后统一回收被标记的对象。

【优点】:利用率100%。
【缺点】:对比复制算法,标记和清除效率都不高,会产生大量不连续的内存碎片。

复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。现在的商业虚拟机都采用这种收集算法来回收新生代,上一篇文章最后我们也提到过新生代和老年代的划分,以及工作过程。

【优点】:简单高效,不会出现内存碎片。
【缺点】:内存利用率低,存活对象较多时效率明显降低。

标记-整理算法

首先标记出所有需要回收的对象,在标记完成后,让所有存活的对象都向一端移动,然后直接清理掉端,边界以外的内存。这种算法就非常适合老年代对象存活率都比较高的情况。

【优点】:利用率100%,没有内存碎片。
【缺点】对比前两者,标记和清除效率都不高。

分代收集算法

在上面的介绍中引入了新生代、老年代的概念,所以准确来说,是先有了分代收集算法的这种思想,才会将Java堆分为新生代和老年代。

这个算法很简单,就是根据对象存活周期的不同,将内存分块。在Java 堆中,内存区域被分为了新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在这,就解释了为什么JVM内存结构要如此设计。

就如我们在介绍上面的算法时描述的,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清理” 或者 “标记—整理” 算法 来进行回收。

STW

STW是 stop the word 的简称。即整个Java虚拟机用户线程暂停工作。

不论是在执行Minor GC还是Full GC,都需要去判断对象是否存活,那如果我在判断的时候,又有新对象产生怎么办呢?所以,就需要STW了,暂停所有用户线程,先把垃圾处理完,再继续用户线程。

Minor GC相比而言是非常快的,其造成的STW,可以忽略不计。因为Minor GC只针对新生代,并且采用的是复制算法,很明显的是空间换时间,而Full GC是时间换空间。

 

参考

图文并茂,万字详解,带你掌握 JVM 垃圾回收!

人已赞赏
Java基础编程语言

【JVM系列】JVM内存模型详解

2020-6-7 15:10:11

Java基础编程语言

【JVM系列】JVM启动参数与性能调优

2020-6-7 19:19:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索