【JVM】Java虚拟机
1. 内存区域(运行时数据区)
- 线程私有:
程序计数器、Java虚拟机栈、本地方法栈 - 线程共享:
堆、方法区(运行时常量池)、直接内存
1.1 程序计数器
用来记录程序正在执行的字节码指令的地址
1.2 虚拟机栈
- 组成:
每执行一个Java方法都会创建一个栈帧,每个栈帧包含:局部变量表、操作数栈、常量池引用。
操作数栈:计算之前和之后,用于存储数据,入栈出栈 - 异常:
当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
1.3 本地方法栈
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,本地方法栈为本地方法服务。
1.4 堆
垃圾回收的主要区域,也叫GC堆
- 组成:
新生代(Eden、From survivor、To Survivor)、老年代() - 异常:
OutOfMemoryError 异常
1.5 方法区(永久代)
- 组成:
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,还有运行时常量池。 - 异常:
OutOfMemoryError 异常
从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
1.6 常量池
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。
- 字面量
相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等; - 符号引用
属于编译原理方面的概念,包括了如下三种类型的常量:
(1)类和接口的全限定名;
(2)字段名称和描述符;
(3)方法名称和描述符。
1.7 直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。
2. 垃圾收集
主要针对堆(新生代、老年代)和方法区(永久代)
2.1 什么是垃圾
- 引用计数(互相引用,导致永远不能被回收)
- 可达性分析
以GC Roots为起点,进行搜索,可达的不能被回收。可以作为GC Roots的:
(1)虚拟机栈中局部变量表中引用的对象
(2)本地方法栈中JNI引用的对象
(3)方法区中类静态属性引用的对象
(4)方法区中常量引用的对象 - 4种引用:
(1)强引用
(2)软引用(内存不足时才会回收)
(3)弱引用(下一次垃圾回收时会被回收)
(4)虚引用(为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。)
2.2 垃圾回收算法
- 标记 - 清除
- 标记 - 整理
- 复制(堆的新生代)
- 分代收集
新生代:复制
老年代:标记清除或者标记整理
2.3 垃圾收集器
(1)Serial
串行、新生代、客户端(默认客户端新生代的收集器)、标记整理
(2)ParNew
并行、新生代、服务端(Serial的并行版),只能与CMS配合使用、标记整理
(3)Parallel Scavenge
并行、新生代、吞吐量优先(降低了并发数,其他收集器是降低用户线程的停顿时间,提升并发数;减少新生代空间——>垃圾回收频繁——>吞吐量下降,缩短吞吐量牺牲了吞吐量,减少了新生代空间)
(4)Serial Old
串行、老年代、客户端
(5)Parallel Old
并行、老年代、(服务端后台)
(6)CMS(Concurrent Mark Sweep并发标记清除)
并行、老年代、服务端
低停顿
过程:初始标记(需要停顿)、并发标记、重新标记(解决并发标记期间发生变化的标记,需要停顿)、并发清除(期间发生引用变化会导致浮动垃圾,只能下一次GC再清理)
缺点:吞吐量低、无法处理浮动垃圾需要临时使用Serial Old处理、标记清除算法会导致不连续空间的浪费
(7)G1
服务端
引入Region
过程:初识标记、并发标记、最终标记、筛选回收
!2.4 内存分配回收策略
- Minor GC:回收新生代,发生一次Minor GC Eden的对象进入Survivor,年龄增加一岁
- Full GC:回收老年代和新生代
- 内存分配策略:
(1)对象优先分配在Eden
(2)大对象直接进入老年代(-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配)
(3)长期存活的对象进入老年代(-XX:MaxTenuringThreshold 用来定义年龄的阈值)
(4)动态年龄判断(如果Survivor相同年龄的对象大于Survivor空间的一半,大于等于该年龄的直接进入老年代)
(5)空间分配担保
MinorGC之前,老年代最大可用连续空间大于新生代所有对象大小,则安全,否则不安全,出发空间分配担保的检查。
如果允许担保失败,则判断老年代的连续空间大于之前晋升到老年代对象的平均大小,如果大于则进行尝试MinorGC;
否则,担保失败,或者老年代空间较小,都会需要先进行一次FullGC
2.5 GC触发的条件
MinorGC:Eden满
FullGC:
- System.gc()
- 老年代空间不足
- 空间分配担保失败(不允许冒险、或者老年代空间不足担保失败)
- Concurrent Mode Failure(CMS导致的老年代空间不足)
3. 类加载机制
3.1 类加载过程
(1)加载
获取二进制字节流——>放入方法区——>生成Class对象放入内存作为方法区的入口
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
(2)验证
(3)准备
初始化类变量(static修饰的变量),放在方法区的内存。
public static int value1 = 123; //初始化为0
public static final int value2 = 123; //初始化为123
(4)解析
将常量池的符号引用替换为直接引用的过程。
(5)初始化
使用用户设置的值初始化,(上面的代码块中)即将123赋值给value1的过程。
初始化阶段是虚拟机执行类构造器
初始化顺序:
- 静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
- 父类中定义的静态语句块的执行要优先于子类。
- 父接口中的变量,只有在子接口中使用时才会初始化。
3.2 何时出发类初始化
类初始化也会执行前面的4个步骤
- 主动引用:
(1)new、读取设置静态类变量、调用类的静态方法
(2)反射调用类
(3)初始化子类触发父类初始化
(4)JVM执行main方法会触发main所在的类初始化 - 被动引用(不会触发类初始化)
(1)子类引用父类静态字段,不会触发子类初始化
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
(2)数组定义引用类,不会触发类初始化,只会触发数组类的初始化,数组类继承自Object
SuperClass[] sca = new SuperClass[10];
(3)使用常量不会触发类初始化
System.out.println(ConstClass.HELLOWORLD);
4. 对象
4.1 对象的创建
类加载检查——>内存分配——>初始化零值——>设置对象头——>执行init方法
- 类加载检查
根据符号引用检查类是否被加载过,如果没有,加载类。 - 分配内存
在堆中为对象分配空间。分配方法:指针碰撞、空闲列表。
使用哪种方式,根据java堆是否连续决定。堆是否连续与GC算法相关。
指针碰撞:堆内存规整(Serial、ParNew)时,用过的和没用过的以指针分界,分界值指针将指针移动对象大小的内存即可。
空闲列表:堆内存不规整(CMS),虚拟机维护一个内存列表,分配内存,更新列表。
分配内存可能导致线程安全问题,2种方式解决:CAS+失败重试、TLAB(为每个线程在Eden区分配内存) - 初始化零值
对象的实例字段初始化 - 设置对象头
设置对象相关信息:
(1)对象和类的关系
(2)如何找到类的元数据信息
(3)对象的哈希值
(4)对象的GC年龄 - 执行init方法
按照程序猿设置的值初始化。
4.2 对象的内存布局
对象头、实例数据、对齐填充
- 对象头:对象的自身运行时数据(哈希码、GC分代年龄、锁状态等)、类型指针
- 实例数据:各种类型的字段内容
- 对齐填充:仅仅占位,保证整个对象对齐,8字节的整数倍。
4.3 对象的访问
句柄、直接指针
- 句柄:
句柄池(堆):到对象实例数据的指针(指向对象实例数据(堆))、到对象类型数据的指针(指向对象类型数据(方法区))
好处:对象被移动时,只需要改变句柄中的实例数据指针。 - 直接指针(堆):到对象类型数据的指针(指向对象类型数据(方法区))、对象实例数据(堆)
好处:少了一次指针定位的时间开销。