JVM学习笔记归档

25 阅读8分钟

JVM如何运行

JVM是 Java 程序的运行环境,它负责将 Java 字节码翻译成机器代码并执行。也就是说 Java 代码之所以能够运行,主要是依靠 JVM 来实现的

流程:通过类加载器将字节码文件加载到内存中运行时数据区,然后JVM 的执行引擎会将字节码翻译成底层系统指令再交由 CPU 去执行, 在执行的过程中也可能调用本地库接口

JVM的内存结构

程序计数器

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。

Java虚拟机栈

每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M:

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

堆内存

所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

java -Xms1M -Xmx2M HackTheJava

方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

类加载机制

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

三个步骤:加载、链接、初始化

链接包括:验证、准备、解析

下面逐个来讲:

加载:简单来说就是将我们写的类Class文件Load到内存中,JVM通过类加载器(ClassLoader)读取类的.class文件,然后将类的静态结构存储在方法区

链接验证:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

链接准备:为类的静态变量分配内存,并设置默认值

链接解析:就将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定

初始化:在这个阶段,JVM执行类的()方法。这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。初始化只执行一次

类加载器分成哪几类

启动类加载器:是Java虚拟机内置的加载器,负责加载Java平台核心类库 扩展类加载器:扩展类加载器负责加载Java平台的扩展库,位于<JAVA_HOME>/lib/ext目录下的JAR文件和类

应用类加载器:应用程序类加载器也被称为系统类加载器,负责加载应用程序类路径(Classpath)上指定的类和用户自定义类

自定义加载器:继承java.lang.ClassLoader类,重写findClass方法,以实现自定义的类加载逻辑,通常用于动态加载类,实现类隔离或实现特定的类加载需求

双亲委派机制

用于控制类的加载和隔离,保证核心内库的安全性

  • 当一个类加载器(称为子加载器)需要加载一个类时,它首先不会自己去加载,而是委托给父加载器进行加载。
  • 父加载器也可以选择继续委派给其父加载器,直到达到最顶层的启动类加载器(Bootstrap Class Loader)。
  • 如果父加载器无法加载该类,子加载器才会尝试自己加载

总结:向上委托,向下加载

四种引用类型

  • 强引用:默认的引用类型,new一个对象时,拿到的引用就是强引用
  • 软引用:一般可以实现对象缓存,垃圾回收且内存不足时,会对其进行回收
  • 弱引用:弱引用对象会在没有强引用指向它时被垃圾回收器回收
  • 虚引用:通常用于对象生命周期的跟踪,无法通过虚引用直接访问对象

垃圾回收 算法

  • 引用计数法:给对象添加一个计数器,当有地方引用该对象的时候计数器+1,为0则回收
  • 可达性分析法:从根对象来遍历对象之间的引用关系,一个对象没有任何链路可到达就是不可达的
  • 标记清除法:清除后会产生不连续的内存碎片,可能导致分配大对象时内存不足
  • 复制算法:将内存划分为两个相等的区域,每次只使用其中一个区域,满了之后复制到另一个区域
  • 三色标记法:将对象分为三种不同的状态,来进行垃圾的标记和回收;白(未访问)、灰(访问了、但其引用的没有访问)、黑(都访问了,是可达的)

类加载

类加载阶段:加载、链接、初始化

首先加载class文件到内存中,链接:验证、准备、解析(将常量池中的符号引用转变为地址引用),初始化

为什么要将 常量池 中的符号引用转变为地址引用?

  • 支持多态:为了实现多态,需要将符号引用转变为实际内存地址,允许JVM在运行时动态地链接和解析类的成员
  • 解耦合:符号引用提供了一种解耦合的机制,使得类之间的引用不依赖于具体的内存布局

FullGC时机

概念:指对整个Java堆(包括年轻代和老年代)进行一次完整的垃圾回收操作

老年代空间不足,内存泄漏,手动调用

直接 内存

概念:在堆之外分配和管理内存的方式,不受JVM的影响,直接与操作系统交互的

JVM调优

如果合理的使用JVM的参数,大部分场景不需要调优

但是还是存在少数场景需要调优,我们可以对一些JVM核心指标配置监控告警,当出现波动时人为介入评估

讲一下GC垃圾回收算法?

引用计数法、复制算法、标记整理、标记清除、分代回收算法

讲一下JAVA的内存模型?

分为主存和工作内存

主内存是所有线程共享的内存区域,用于存储共享变量。在Java中,当一个线程需要读取或修改共享变量时,它必须首先从主内存中获取该变量的副本。

每个线程都有自己的工作内存,用于存储该线程使用的变量的副本。线程对共享变量的所有操作(读取、赋值、同步操作)都是在工作内存中进行的。

讲一下对象分配过程?

在对象分配之前,JVM会检查对象所属的类是否已经被加载、链接和初始化。如果没有,JVM会执行类加载过程。

内存分配:JVM需要在堆内存中为新对象分配足够的内存空间

确定对象大小、对象初始化、执行构造方法