JVM的基础知识整理
1 内存布局
1.1 JVM 内存
- 线程私有
- 程序计数器
- 本地方法栈
- 虚拟机栈
- 线程共享
- 堆
- 方法区
- 直接内存
1.2 对象内存布局
- 对象头
- mark word
- 类型指针
- 数组长度标识
- 实例数据
- 对齐
1.3 JVM 运行时内存
- 新生代
- 一个伊甸区
- 两个幸存区
- 老年代
- 永久代:1.8移除
- 元空间:1.8顶替永久代,不在虚拟机中,使用本地内存
1.4 分配内存方法
-
指针碰撞(规整时):空闲内存和使用内存间存放一个指针,每次分配内存就移动该指针
-
空闲列表(不规整时):维护一个列表,上面记录空闲内存。每次分配内存时,都会更新表上的记录
2 对象创建到死亡
2.1 对象创建过程
- 先尝试在栈上分配
- 栈上无法分配且对象很大,则直接分配到老年代
- 若对象不是很大,先尝试在 TLAB 上分配(TLAB 线程分配本地缓冲区,在伊甸区上线程私有的一块内存)
- TLAB 上无法分配,则分配在伊甸区
- 伊甸区经过一次 YGC,到幸存区
- 幸存区经过多次 YGC,年龄达到(默认值为15),进入老年代
2.2 动态年龄
- 幸存区相同年龄的对象大于幸存区一半时
- 年龄大于或等于上述年龄的对象直接进入老年代
2.3 分配担保
在一次 YGC 时,幸存区空间不够,直接进入老年代
2.4 对象死亡过程
两次标记
- 第一次标记:根不可达
- 放置在 F-Queue 队列中;覆盖
finalize()
方法,且没有调用finalize()
方法,暂时不死 - 第二次标记:对F-Queue队列中仍未被引用的对象标记
- 被二次标记后,死亡
3 垃圾回收
3.1 如何确定垃圾
- 引用计数法
- 在对象中添加一个引用计数器
- 每当有一次引用时+1,引用失效时-1
- 当计数器为0时对象没有被使用。
- 存在问题:无法解决循环引用
- 可达性分析(Java虚拟机使用)
- 对象到 GCRoots 间没有任何引用链相连,表明该对象没有被使用
- 根对象:线程栈变量 静态变量 常量池 JNI指针
3.2 垃圾回收算法
- 标记清除,内存碎片
- 标记整理,整理耗时
- 拷贝算法,空间利用率低
- 分代收集
- 新生代:朝生夕灭 -> 标记整理 -> Appel 式回收
- 老年代:老不死 -> 标记清除或标记整理
3.3 四种引用类型
- 强引用:对象赋值给引用变量
Object o = new Object()
- 软引用:SoftReference 类实现,内存不足时才会被回收
- 弱引用:WeakReference 类实现,只要垃圾回收器触发就会被回收
- 虚引用:PhantomReference 类实现,不能单独使用,主要作用是跟踪对象被垃圾回收的状态
3.4 分区收集算法
- 将整个堆空间划分为连续的不同小区间,
- 每个小区间独立使用、独立回收
- 合理回收区间,从而减少一次GC造成的STW时间
3.5 常见垃圾回收器
- Serial:单线程、复制算法
- ParNew:多线程、复制算法
- Parallel Scavenge:多线程、复制算法,注重 吞吐量
- Serial Old:单线程、标记整理算法
- Parallel Old:多线程、标记整理算法
- CMS:多线程、标记清除算法
- G1之后不使用分代收集理论,使用 分区收集算法
4 类加载
4.1 JVM 类加载过程
- 加载:这个阶段会在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的入口
- 验证:确保符合虚拟机要求
- 准备:类变量(静态变量)分配内存和赋初始值
- 解析:将常量池中的符号引用替换为直接引用的过程
- 初始化:执行类构造器
4.2 符号引用与直接引用
- 符号引用:符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中
- 直接引用:指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,引用目标一定在内存中
4.3 不会执行类初始化
- 子类引用父类静态字段,只会初始化父类
- 定义对象数组
- 常量
- 通过类名获取 Class 对象
- 通过Class.forName 加载指定类时,指定参数 initialize 为 false 时
- 通过 ClassLoader 默认的 loadClass 方法
4.4 类加载器
- 启动类加载器: JAVA_HOME\lib
- 扩展类加载器:JAVA_HOME\lib\ext
- 应用程序类加载器:用户路径(classpath)上的类库
- 自定义类加载器:继承 ClassLoader,重写 findClass 方法
4.5 双亲委派机制
- 自底向上检查该类是否已被加载
- 自顶向下进行实际的查找与加载
- 总而言之,委托父类加载器加载,除非父类加载器无法加载,才会被子类加载器加载
- 好处,保证使用不同的类加载器,都能获得相同的对象