1.JVM总体框架

image-20230315171157354

  • 方法区和堆是多线程共享的(标为灰色)
  • 其他三个是每个线程都有单独的(标为蓝色)

2.类加载子系统

image-20230315171138943

  • 验证:验证待加载的class文件是否正确
  • 准备:为static变量分配内存并赋零值
  • 解析:将符号引用解析为直接引用

常见类加载器

  • 引导类加载器(BootstrapClassLoader)
  • 自定义类加载器(继承实现了ClassLoader类)
    • ExtClassLoader
    • AppClassLoader
    • WebAppClassLoader

类加载器加载的目录

  • BootstrapClassLoader : jre/lib
  • ExtClassLoader : jre/lib/ext
  • AppClassLoader : classpath

双亲委派机制

双亲委派机制:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,一直向上委托如果不能加载再向下返回

1
2
3
4
5
6
7
8
9
10
11
12
13
//ClassLoader类
protected Class<?> loadClass(String name, boolean resolve){
Class<?> c = findLoadedClass(name);
if (parent != null) {//先让它的父类去加载
c = parent.loadClass(name, false);
} else {//parent=null说明其父类是BootstrapClassLoader,使用BootstrapClassLoader来加载
c = findBootstrapClassOrNull(name);
}
......
if (c == null){//父类也没加载到,只能自己加载了
c = findClass(name);
}
}

那么怎么知道父类是谁呢?在构造Launcher对象时,就给AppClassLoader设置好了parent

image-20230315211619218

image-20230315211728849

在构建AppClassLoader对象时调用了父类的构造方法

image-20230315211840579

image-20230315211858608

image-20230315211914968

image-20230315211930411

image-20230315211943939

一直到ClassLoader类中,设置了parent

双亲委派机制的作用:

  • 避免类被重复加载
  • 防止核心API被篡改

Tomcat自定义类加载器

为什么tomcat要自定义类加载器:

若多个web应用拥有同名的类,AppClassLoader 只能加载一个(JVM判断类是否被加载的逻辑为:类名+对应的类加载器实例)。而WebappClassLoader 会为每个应用生成单独的类加载器实例,实现了类加载的隔离。

3.运行时数据区

image-20230317172408166

程序计数器

  • 它是每个线程私有的,用于存储指向下一条字节码指令的地址
  • 不会发生内存溢出

虚拟机栈

  • 每个线程有一个虚拟机栈
  • 每个方法执行都会创建一个栈帧然后入栈
  • 方法执行完毕会从虚拟机栈中出栈
  • 线程太多的话,创建了太多的虚拟机栈,会导致OutOfMemoryError
  • 方法调用层次太深的话,创建了太多的栈帧,会导致StackOverflowError

虚拟机栈中的每个栈帧包含四个部分:

  • **局部变量表(Local Variable Table)**是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。引用自[此](Java虚拟机—栈帧、操作数栈和局部变量表 - 知乎 (zhihu.com))
  • **操作数栈(Operand Stack)**也常称为操作栈,它是一个后入先出栈(LIFO)。
  • **动态连接(Dynamic Linking)**是将要调用的方法的符号引用转化为其在内存地址中的直接引用。
  • 方法返回地址一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。

举一个例子:

图中为执行指令时操作数栈和局部变量表的变化

image-20230318164324518

本地方法栈

本地方法栈是jvm的一个组成部分,它用于存储本地方法的调用信息。本地方法是用其他语言(如C或C++)编写的方法,通常用于与操作系统交互。本地方法可以通过Java Native Interface (JNI)或Java Native Access (JNA)来调用。每个新线程都会分配一个单独的本地方法栈。不同的jvm实现可能有不同的方式来处理本地方法栈和Java栈,有些可能共享同一区域,有些可能分开。

image-20230318170831273

  • JVM中的堆是用来存储对象数组的内存区域
  • JVM堆的大小可以通过**-Xms-Xmx**参数来设置,其中-Xms表示堆的初始大小,-Xmx表示堆的最大大小
  • JVM会定期执行垃圾回收(GC)操作,来清理不可达和即将回收的对象,并释放内存空间。

4.垃圾回收

目的:防止内存泄露和内存溢出等问题。

回收的对象: 不再被程序使用的对象,也就是没有被引用的对象。

回收过程:

  • 标记垃圾对象:引用计数法,可达性分析法
  • 垃圾回收: 标记清除法,复制算法,标记整理算法……

引用计数法

每个对象都保存一个引用计数器属性,用于记录对象被引用的次数

优点:实现简单,实时性好

缺点:无法处理循环引用 (两个或多个对象互相引用,形成一个环)

可达性分析法

JVM会从一组称为“GC Roots”的对象开始,遍历所有可达对象,并将无法到达的对象标记为垃圾对象。

JVM将下列对象视为GC Roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • Native方法中JNI引用的对象
  • 等等

image-20230404152259487

如图所示,蓝色的为可达对象不会被回收。灰色的为循环引用,不能从GCRoot到达。

标记-清除算法

  • 标记: Collector从GCRoot开始遍历,标记所有被引用的对象。
  • 清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

image-20230404154754697

缺点:

  • 效率不算高

  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差

  • 产生大量内存碎片

复制算法

将内存空间分为A、B两块,每次使用一块,垃圾回收时遍历一次目前使用的内存A块,将所有可达对象复制到B块。然后清除A块所有对象,下一次回收再从B复制到A,交换着来。

image-20230404161855889

优点:

  • 没有标记和清除阶段,通过GC Roots找到可达对象,直接复制,不需要修改对象头,效率高
  • 不会出现内存碎片

缺点:

  • 需要更多的内存,始终有一半的内存空闲
  • 对象复制后,对象存放的内存地址发生了变化,需要额外的时间修改栈帧中记录的引用地址
  • 如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低,所以垃圾对象多的情况下,复制算法比较适合。

标记-整理算法

  • 标记可达对象
  • 移动可到对象到内存的一端
  • 清理边界外的空间

image-20230404165923669

优点:

  • 不会出现内存碎片
  • 也不需要利用额外的内存空间

缺点:

  • 效率要低于标记-清除算法、复制算法
  • 也需要修改栈帧中的引用地址

图片来源:https://www.bilibili.com/video/BV1he4y1e7nW?p=23&vd_source=08ec27c446c7fe3ce7235d101b3cbf17

image-20230404170023948

常见的垃圾回收器

图片来源:https://www.bilibili.com/video/BV1he4y1e7nW?p=23&vd_source=08ec27c446c7fe3ce7235d101b3cbf17

image-20230404170351412