一、Java内存区域§
1.运行时数据区域§
堆(Heap): JVM 启动时按 -Xmx, -Xms 大小创建的内存区域,用于分配对象、数组所需的内存,由GC管理和回收。
方法区(Method Area): 存储被JVM加载的类信息(字段、成员方法的字节码指令等)、运行时常量池(字面量、符号引用等)、JIT编译后的CodeCach等;JDK8之前Hotspot将方法区存储于永久代堆内存,之后参考JRockit废弃了永久代,存储于本地内存的Metaspace区。
直接内存: JDK1.4 引入 NIO 使用 Native/Unsafe 库直接分配系统内存,使用 Buffer,Channel 与其交互,避免在系统内存与 JVM 堆内存之间拷贝的开销。
2.JVM对象§
a.创建对象§
分配堆内存: 类加载完毕后,其对象所需内存大小是确定的;堆内存由多线程共享,若并发创建对象都通过CAS乐观锁争夺内存,会导致效率很低。所以线程创建时会在堆内存为其分配私有的分配缓冲区(TLAB: Thread Local Allocation Buffer)
- 内存模型
- 分配流程
b.内存布局§
对象头
- Mark Word: 记录对象的运行时信息,比如hashCode,GC分代年龄,尾部2bit用于标记锁状态
- Class Pointer: 只想所属的类信息
- 数组长度(可选|对象为数组): 4字节储存其长度
对象数据: 各种字段的值,按宽度分类紧邻存储 对齐填充: 内存对象为1个字长整数倍,减少CPU总线周期 验证: openjdk/jol检查对象内存布局
3.内存溢出§
二、垃圾回收§
GC可分解为三个问题: which、when、how
1.GC条件§
a.引用计数法(Reference Counting)§
每个对象都维护一个引用计数器 rc,当通过赋值、传参等方式引用它时 rc++,当引用变量修改指向、离开函数作用域等方式解除引用时 rc--,递减到0时说明对象无法被使用,可以进入回收了流程。
assign(var, obj):
incr_ref(obj)
# self = self 现增再减,避免引用自身导致内存提前被释放
decr_ref(var)
var = obj
incr(obj):
obj.rc++
decr(obj):
obj.rc--
if obj.rc == 0:
# 断开 obj 与其他对象的引用关系
remove_ref(obj)
# 回收 obj 内存
gc(obj)
-
优点:
- 思路简单,对象无用即回收
- 延迟低,适用于内存较少的场景
-
缺点:
- 此算法中对象是孤立的,无法在全局视角下检查对象的真实有效性,循环引用的双方对象需引入外部机制来检测和回收
b.可达性分析算法(Reachability analysis)§
从肯定不会被回收的对象(GC Roots)出发,向外搜索全局对象图,不可达的对象即无法在被使用,可回收; 常见可作为GC Root的对象有:
-
执行上下文: JVM 栈中参数、局部变量、临时变量等引用的堆对象
-
全剧引用: 方法区中类的静态引用、常量引用所指向的对象
-
优点:
- 无需对象维护 GC 元信息,开销小
- 单次扫描即可批量识别、回收对象,吞吐高
-
缺点:
- 多线程环境下对象间的引用关系随时在变化,为保证 GC Root 标记的准确性,需在不变化的 snapshot 中进行,会产生 Stop The World(以下简称 STW) 卡顿现象
c.引用类型§
| 引用类型 | 类 | 回收时机 |
|---|---|---|
| 强引用 | - | 只要与 GC Root 存在引用链,则不被回收 |
| 软引用 | SoftReference | 只被软引用所引用的对象,当 GC 后内存依然不足,才被回收 |
| 弱引用 | WeakReference | 只被弱引用所引用的对象,无论内存是否足够,都将被回收 |
| 虚引用 | PhantomReference | 被引用的对象无感知,进行正常 GC,仅在回收时通知虚引用(回调) |