0%

Java筑基-:(03)对象的分配及垃圾回收机制

一、 JVM 中对象的创建过程

对象的整体创建过程如下所示:

对象创建过程

我们的常量池里面可能存在符号引用,什么是符号引用?我们引用了一个对象,但是不知道这个对象的真实地址。举个例子:你引用了 com.esun.B ,如果是直接引用的话,那就是真实的地址。 如果你在 A 的常量池里面引用了 B ,但是 B 还没有加载进来,这时候你就只能用一个符号。

在检查加载的过程会去检验是否能正常将符号引用转换为直接引用。

1.1 内存分配

内存分配一般有 2 种方式:

  • 指针碰撞。内存比较规整,前面内存已经分配到 7 了,那么这次分配从 8 开始

  • 空闲列表。不在乎内存碎片,根据维护的空闲列表来分配

分配的时候需要注意多线程安全,主要有 2 种方式:

  • CAS + 失败重试操作

  • 本地线程分配缓冲:给每个线程在 Eden 区默认划分一块空间,线程分配的时候,就在那块空间去操作,由于线程有自己专属的,所以并不会有线程安全问题

1.2 对象内存布局

一个对象会包括对象头、实例数据、以及对齐填充(不一定有)。其中对象头很重要,它主要包括:

  • Markword

  • 类型指针,指向方法区中的 Class ,标明它是哪个类型

  • 记录数组长度(如果是数组对象才有)

二、GC

2.1 判断对象存活

有 2 种主流的方式:

  • 引用计数,不能解决循环引用——Python 就是用这种方案

  • 可达性分析,也说 根可达方法

GCRoots 主要有4种,但是不止4种:

  • 静态变量

  • 线程栈变量

  • 常量池

  • JNI (指针)

  • 如果还纠结,就有其他:Class对象、类加载器、

方法区中的 class 对象是否能被回收?肯定能被回收,有一个很重要的条件是 new 出的所有对象都被回收了。其他的还包括它的 ClassLoader 也被回收了等等。

真正要宣布一个对象可回收需要 2 次标记,首先是不可达,其次就是 finalize ,所以不可达不是非死不可的条件。 finalize 里面可以让引用继续来。 DirectByteBuffer 直接内存分配,它里面也用了虚引用,是否用来协助内存回收??待确认

2.2 对象分配策略

对象的分配原则:

  • 对象优先在 Eden 分配

  • 空间分配担保

  • 大对象直接进入老年代

  • 长期存活的对象进入老年代

  • 动态年龄判定

我们说几乎所有的对象都在堆上分配,为什么是几乎?因为还有栈上分配这种方案:

  • 触发了 JIT (热点数据),逃逸分析的时候发现对象不会逃出当前方法和线程,的时候就可能触发栈上分配

2.3 GC 详情

堆分代:

  • 新生代: Eden区、s0、s1 区,三者大小 8:1:1 ?为什么,因为大多数对象都是朝生夕死的,存活时间很短

  • 老年代,一般老年代 GC ,就会触发 Full GC: 也就是年轻代、老年代、方法区都 GC

新生代一般是用复制算法,老年代是标记整理、标记清除算法

标记整理算法都是怎么做呢? 其实是: 标记——>整理——> 清除 ,为什么要这么做呢?(有些人会以为是:标记——>清除——> 整理 ),这是因为这里的时候,可以覆盖那些垃圾,等整理完了之后,垃圾可能会变少了,清除也快一些。

这个过程会有STW 的,暂停所有线程,这是为什么?因为整理的时候,对象的位置会发生变化啊,整理之后,以前在位置 15 的时候变成 位置 6 了,所以要停止。

三、虚拟机的优化技术

  • 热点数据编译成本地代码

  • 本地线程分配缓冲

最终, JVM 的学习一张图看了:

JVM一张图

谢谢你的鼓励