JVM
# 运行时数据区
# 运行时数据区包括什么
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
# 程序计数器
# 程序计数器是干什么的
程序计数器(Program Counter Register)
是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
# 为什么需要程序计数器
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
# 程序计数器为什么是私有的
当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令,从而在线程切换后能恢复到正确的执行位置。各条线程间的计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存
# Java虚拟机栈(栈)
# 内存中的堆和栈的区别
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
- 堆解决的是数据存储的问题,即数据怎么放,放哪里。
# Java虚拟机栈基本内容
定义
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。
生命周期
生命周期和线程一致。
作用
每个 Java 方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储 局部变量表、操作数栈、常量池引用 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
存储是什么
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都有各自对应一个栈帧(Stack Frame)。
栈运行原理
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
# 栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)主要存放了编译期可知的各种数据类型
- 操作数栈(operand Stack)(或表达式栈):数据计算的中转站
- 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
# 本地方法栈
本地方法
简单地讲,一个Native Method是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。
本地方法栈
本地方法栈(Native Method Stack)
与虚拟机栈的作用相似。
二者的区别在于:虚拟机栈为 Java 方法服务;本地方法栈为 Native 方法服务。本地方法并不是用 Java 实现的,而是由 C 语言实现的。
# 堆
Java 堆(Java Heap)
的作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存。
Java 堆是垃圾收集的主要区域(因此也被叫做"GC 堆")。现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法。
# 方法区
方法区(Method Area)也被称为永久代。方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
# 运行时常量池
运行时常量池(Runtime Constant Pool)
是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容会在类加载后被放入这个区域。
- 字面量 - 文本字符串、声明为
final
的常量值等。 - 符号引用 - 类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
# 字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
# 重点:垃圾回收
# 垃圾回收-标记阶段(死亡对象判断)
# 引用计数算法
在对象中添加一个引用计算器,每当一个地方引用它时,计算器值就加一;当引用失效时,计算器值就减一;任何时刻计算器为0的对象就是不可能再被使用的。
优点
- 实现简单,垃圾对象便于辨识
- 判定效率高,回收没有延迟性。
缺点
它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
循环应用问题
public class RefCountGC {
// 这个成员属性的唯一作用就是占用一点内存
private byte[] bigSize = new byte[5*1024*1024];
// 引用
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
// 显示的执行垃圾收集行为
// 这里发生GC,obj1和obj2是否被回收?
System.gc();
}
}
// 运行结果
PSYoungGen: 15490K->808K(76288K)] 15490K->816K(251392K)
在这里,两个对象除了彼此引用之外,再无任何引用,实际上两个对象已经不可能再被访问,但是因为它们相互引用对方,导致它们的引用计数器不为0,引用技术算法就无法回收他们。
# 可达性分析算法
基本思路
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
# 三色标记法
三色标记法是一种垃圾回收法,它可以让JVM不发生,或者短时间发生STW(Stop The World),就能达到清除JVM内存垃圾的目的。JVM中的CMS、G1垃圾回收器所使用垃圾回收算法才是三色标记法,这也是为什么说CMS是第一个真正意义上的可以和用户程序并行处理垃圾回收的垃圾收集器,
三色标记算法思想三色标记法将对象的颜色分为了黑、灰、白,三种颜色
- 白色,该对象尚未被垃圾收集器访问过,在可达性分析开始的阶段,所有的对象都是白色
- 灰色,该对象已经被垃圾收集器访问过,但是这个对象直接引用的对象还没有被完全访问(至少存在一个引用未被扫描过)
- 黑色,该对象已经被垃圾收集器访问过,并且该对象的所有引用对象都已经完全访问,所有的引用都被扫描过
标记流程
- 初始标记:初始的时候所有对象都是白色,将GCRoots直接引用的节点,将它们标记为灰色,这个阶段需要STW
- 并发标记:并发标记阶段,从上一步GCRoots的灰色节点开始,去扫描整个引用链,然后将它们标记为黑色,这个阶段不需要STW
- 如果该节点没有子节点,直接标记为黑色
- 如果该节点有子节点,则把当前节点标为黑色,它的子节点设置成灰色,表示该节点完全扫描,但是子节点还没有被完全访问
- 重复这个过程,进行多次标记过程,直到没有任何灰色的对象,才结束
三色标记算法的缺陷
三色标记算法也存在缺陷,在并发标记阶段的时候,因为用户线程与 GC 线程同时运行,有可能会产生多标或者漏标
漏标问题
漏标的两个充要条件:
- 有至少一个黑色对象在自己被标记之后指向了这个白色对象
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
解决方案:
增量更新:增量更新破坏的是第一个条件,我们在这个黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描。
可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰。这样有一个缺点,就是会重新扫描这个黑色对象的所有引用,比较浪费时间。
原始快照:原始快照破坏的是第二个条件,我们在这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向白色对象为根,对它的引用进行扫描
可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰。这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是变灰了,就会导致它和它的引用,本来就应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾。
多标问题
- 多标产生原因:
- 并发标记过程中,由于用户线程运行导致部分局部变量(GC root)被销毁,导致该局部变量所引用的对象(被扫描过,黑色)成为浮动垃圾。
- 并发标记或并发清理,由于用户线程运行产生的新对象,通常被直接全部标记为黑色,
# 如何判断一个常量是废弃常量
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。
# 如何判断一个类是无用的类
类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
# 垃圾回收-清除阶段
目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法(Mark-Sweep)
、复制算法(copying)
、标记-压缩算法(Mark-Compact)
# 标记-清除算法
算法分为标记
和清除
两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
缺点
- 执行效率不稳定,如果有大部分需要被回收的对象,这时需要进行大量标记和清除动作,导致标记和清除两个过程的执行效率都会随着对象数量增长而降低。
- 内存空间的碎片化问题,这种方式清理出来的空闲内存是不连续的,如果需要分配较大对象无法找到足够多的连续内存,就需要额外一次垃圾收集动作。
# 标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
优点
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
# 标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
优点
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点
从效率上来说,标记-整理算法要低于复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
# 分代收集算法
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
# 引用深入
- 强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“
Object obj = new Object()
”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。
- 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
- 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
# 类加载过程
类加载子系统中有三个大阶段:加载阶段
、链接阶段
、初始化阶段
。这三个大阶段主要包括七个小阶段,如图所示:
# 加载阶段
加载阶段主要完成三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
- 在内存中生成一个代表这个类的
Class
对象,作为方法区这个类的各种数据的访问入口。
# 链接阶段
1、验证阶段
验证的目标是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段会完成四项验证:
- 文件格式验证 - 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证 - 对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
- 字节码验证 - 通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
- 符号引用验证 - 发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
2、准备阶段
为类变量分配内存并且设置该类变量的默认初始值,即零值(如
0
、0L
、null
、false
等)。public static int value = 123;
上面的变量value被初始化为0,而不是123.
类型 默认初始值 byte (byte)0 short (short)0 int 0 long 0L float 0.0f double 0.0 char \u0000 boolean false reference null 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
public static final int value = 123;
上面的变量value就直接被初始化为123
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
3、解析阶段
解析阶段目标是将常量池的符号引用
替换为直接引用
的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
- 符号引用(Symbolic References) - 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct Reference) - 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
# 初始化阶段
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。
1、类初始化方式
- 声明类变量时指定初始值
- 使用静态代码块为类变量指定初始值
2、类初始化步骤
- 如果类还没有被加载和链接,开始加载该类。
- 如果该类的直接父类还没有被初始化,先初始化其父类。
- 如果该类有初始化语句,则依次执行这些初始化语句。
# 类加载器
# 类加载器的分类
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
# 双亲委派机制
Java虚拟机对class文件采用的是按需加载
的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式
,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改