01 JVM 内存结构
Java 虚拟机的内存空间分为 5 个部分:
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 堆
- 方法区
JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久 代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
1.1 程序计数器(PC 寄存器)
(1)程序计数器的定义 程序计数器是一块较小的内存空间,是当前线程正在执行的那条字节码指令的地 址。若当前线程正在执行的是一个本地方法,那么此时程序计数器为 Undefined。
(2)程序计数器的作用
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
- 在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回 来时,就知道上次线程执行到哪了。
(3)程序计数器的特点 是一块较小的内存空间。
- 线程私有,每条线程都有自己的程序计数器。
- 生命周期:随着线程的创建而创建,随着线程的结束而销毁。
- 是唯一一个不会出现 OutOfMemoryError 的内存区域。
由于文章篇幅问题,部门内容将以图片展示,如有小伙伴需完整文档进行查阅观看点赞+关注之后【点击此处】即可获取!!
1.2 Java 虚拟机栈(Java 栈)
(1)Java 虚拟机栈的定义 Java 虚拟机栈是描述 Java 方法运行过程的内存模型。
Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域, 用于存放该方法运行过程中的一些信息,如:
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口信息
- …
(2)压栈出栈过程 当方法运行过程中需要创建局部变量时,就将局部变量的值存入栈帧中的局部变 量表中。
Java 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的 方法,PC 寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操 作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新 创建的栈帧压入栈顶,变为当前的活动栈帧。
方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一 个操作数。如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有变化。
由于 Java 虚拟机栈是与线程对应的,数据不是线程共享的,因此不用关心数据 一致性问题,也不会存在同步锁的问题。
(3)Java 虚拟机栈的特点
- 局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事 先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
- Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请 求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。
02 HotSpot 虚拟机对象探秘
2.1 对象的内存布局 在 HotSpot 虚拟机中,对象的内存布局分为以下 3 块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
(1)对象头
对象头记录了对象在运行过程中所需要使用的一些数据:
- 哈希码
- GC 分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程 ID
- 偏向时间戳
对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个 数组,那么对象头还会包括数组长度。
(2)实例数据
实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。
(3)对齐填充
用于确保对象的总长度为 8 字节的整数倍。 HotSpot VM 的自动内存管理系统要求对象的大小必须是 8 字节的整数倍。而 对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部 分没有对齐时,就需要通过对齐填充来补全。 对齐填充并不是必然存在,也没有特别的含义,它仅仅起着占位符的作用
03 垃圾收集策略与算法
程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法 的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确 定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束 时,内存自然就跟随着回收了。而对于 Java 堆和方法区,我们只有在程序运行期间才能知道会创建哪些对象, 这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
3.1 判定对象是否存活
若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。
(1)引用计数法
在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用 失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。
引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的 算法。但是主流的 Java 虚拟机里没有选用引用计数算法来管理内存,主要是因 为它很难解决对象之间循环引用的问题。
举个栗子对象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它们互相引用着对方,导致它们的引用计数 都不为 0,于是引用计数算法无法通知 GC 收集器回收它们。
(2)可达性分析法
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关 联的对象就是无效对象。
GC Roots 是指:
- Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
04 HotSpot 垃圾收集器
HotSpot 虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点,虽然我 们要对各个收集器进行比较,但并非为了挑选出一个最好的收集器。我们选择的 只是对具体应用最合适的收集器。
4.1 新生代垃圾收集器
(1)Serial 垃圾收集器(单线程) 只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程 (Stop The World)。
一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾 收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。 因此 Serial 垃圾收集器适合客户端使用。
由于 Serial 收集器只使用一条 GC 线程,避免了线程切换的开销,从而简单高 效。
(2)ParNew 垃圾收集器(多线程)
ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但 清理过程依然需要 Stop The World。
ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收 集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额 外的开销,因此在单 CPU 环境中表现不如 Serial。
05 内存分配与回收策略
06 JVM 性能调优
在高性能硬件上部署程序,目前主要有两种方式:
- 通过 64 位 JDK 来使用大内存;
- 使用若干个 32 位虚拟机建立逻辑集群来利用硬件资源。
07 类文件结构
08 类加载的时机
09 类加载的过程
10 类加载器
10.1 类与类加载器
(1)判断类是否“相等” 任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中 的唯一性,每一个类加载器,都有一个独立的类名称空间。
因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前 提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟 机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。 这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方 法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。