温故知新-JVM篇

JVM是每个Java选手面试时几乎绕不过的知识点,但是在我们的工作过程中,用到这些知识的机会并不多,因此很多知识随着时间流逝就慢慢离我们而去了,本文的目的是帮助有一定基础的同学快速串联起JVM相关的一些知识,唤起尘封已久的记忆。

1. JVM是做什么的?

JVM(Java Virtual Machine, Java虚拟机)是我们的Java代码到本地OS的适配层,所有的Java程序都是直接运行在JVM上,之后由JVM间接与OS交互的。 JVM的运行时数据区 既然JVM承接了Java程序运行的任务,那么,在程序运行时,其就一定有一套自己的内存管理策略来调度OS内存,这就是JVM的运行时数据区。

1.1 线程私有

  1. 程序计数器:程序计数器用来保存的是下一条将要执行的指令的地址,这使得Java虚拟机可以在任何时候准确地知道各个线程正在执行的代码位置。在多线程环境或异常处理时,当一个线程被挂起或中断以允许另一个线程运行时,程序计数器会保存该线程的执行位置。
  2. 本地方法栈:本地方法栈(Native Method Stack)是专门为执行本地方法(Native Methods)服务(如C、C++等编写的方法)。
  3. 虚拟机栈:每个线程都有自己独立的虚拟机栈,用于对应线程方法的运行。每个方法对应一个栈帧,栈帧包含了方法的局部变量表、操作数栈、动态链接、方法出口等信息。
  • 操作数栈:用于存储计算过程中的中间结果和操作数。它是JVM执行引擎进行指令操作的主要工作区。
  • 局部变量表:存放了方法参数和方法内部定义的局部变量。
  • 动态链接:在运行期间解析符号引用并将其转换为直接引用的过程,主要用于方法调用和访问字段,指向元空间内的方法模板。
  • 方法返回地址:保存了控制权传递的下一条指令的地址,即方法正常退出或异常退出后,虚拟机应回到哪里继续执行。

1.2 线程公有

  • 堆(Heap):是JVM内存管理的一个核心区域,用于存储几乎所有的Java对象实例以及数组。堆是所有线程共享的一块内存空间,这意味着任何线程都可以访问堆中分配的对象(需要适当同步控制以避免并发问题)。
  • 元空间:元空间使用的是直接的本地内存,而不是JVM堆内存的一部分。这意味着元空间的大小不再受JVM堆大小的直接限制,可以更大程度上利用系统可用内存。
-Xms:设定JVM堆的初始大小。合理的设置可以避免应用程序启动时的内存分配延迟。例如,-Xms1024m表示初始堆大小为1GB。

-Xmx:设定JVM堆的最大大小。这个值限制了JVM堆内存可以扩展到的最大值,防止应用程序因为内存需求超出预期而消耗过多系统资源或导致系统不稳定。例如,-Xmx2048m表示最大堆大小为2GB。

-Xmn:设定年轻代(Young Generation)的大小。年轻代是堆内存中的一部分,专门用于存放新创建的对象。合理设置年轻代大小可以优化垃圾回收性能。例如,-Xmn512m表示年轻代大小为512MB。

-XX:NewRatio:用于控制年轻代与老年代(Old Generation)的比例。默认值是2,意味着老年代是年轻代的两倍。如果希望年轻代更大,可以减小这个值。

-XX:SurvivorRatio:设置年轻代中Eden区与Survivor区的比例。例如,-XX:SurvivorRatio=8表示Eden区与一个Survivor区的大小比例为8:1,另一个Survivor区大小与之一样。

-XX:MaxPermSize(Java 8之前版本):设定永久代(PermGen)的最大大小。在Java 8及之后的版本中,永久代已被元空间(Metaspace)取代,应使用-XX:MaxMetaspaceSize来控制元空间的最大大小。

-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize:分别用于指定元空间的初始大小和最大大小,适用于Java 8及以上版本。

2. 类的加载

Java语言是以类为基本单元组织代码的,代码加载到JVM的过程就是类加载的过程。

2.1 类加载器

Java类加载器主要分为以下几种类型:

  1. 启动类加载器(Bootstrap ClassLoader):
  • 这是最顶层的类加载器,负责加载Java的核心库(如rt.jar、charsets.jar等),这些库位于JRE的安装目录下的lib目录中。它是用C++编写的,并且在Java中是不可见的,因此没有父加载器。
  • 启动类加载器保证了Java平台的基础类能够被正确加载,且不会被篡改或替换。
  1. 扩展类加载器(Extension ClassLoader):
  • 扩展类加载器是Java平台扩展功能的类加载器,它负责加载JRE的lib/ext目录下或者通过系统属性java.ext.dirs指定的路径中的类库。
  1. 应用类加载器(Application ClassLoader):
  • 系统类加载器负责加载用户类路径(ClassPath)上的所有类,是大多数Java程序默认的类加载器。
  • 它会加载环境变量CLASSPATH或Java命令行 -cp 参数所指定的目录或jar文件中的类。
  1. 自定义类加载器(Custom ClassLoader):
  • 用户可以根据需要自定义类加载器,继承自java.lang.ClassLoader类。
  • 自定义类加载器可以用来加载特定目录、网络或其他来源的类,常用于实现代码的热部署、插件系统、隔离加载不同版本的类等高级功能。
  • 自定义类加载器的父加载器通常是系统类加载器,但也可以指定为其他类加载器。

2.2 双亲委派机制

JVM(Java虚拟机)的双亲委派机制是一种类加载过程中的设计策略,它确保了Java程序的稳定性和安全性。这一机制定义了类加载器加载类时的一种委托行为,其核心思想是:当一个类加载器接收到类加载的请求时,并不是自己立即去加载这个类,而是先委托给其父加载器去尝试加载。如果父加载器能够完成加载,那么子加载器就不需要再次加载;如果父加载器无法加载,子加载器才会尝试自己加载这个类。 具体来说,双亲委派机制遵循以下步骤:

  1. 加载请求: 当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是将这个请求委托给其父加载器。
  2. 逐级向上委托: 每个层次的类加载器都是如此,直至委托到最顶层的启动类加载器(Bootstrap ClassLoader)。启动类加载器负责加载Java的核心库(如rt.jar),由于这些类是最基础且被信任的,因此由它来加载最合适。
  3. 加载与返回: 如果父加载器能够成功加载这个类,则直接返回对应的java.lang.Class对象;如果父加载器无法加载(包括它拒绝加载,比如找不到类定义),则子加载器才会尝试自己加载。
  4. 自加载: 如果最终所有的父加载器都没有加载成功,那么最初的类加载器才会自己加载这个类。 双亲委派机制的关键优势在于:
  • 避免类重复加载: 确保了每个类在JVM中只被加载一次,由一个类加载器加载,有助于节省内存并维护类的唯一性。
  • 实现类加载隔离: 不同的应用模块可以通过自定义类加载器加载自己的类,互不影响,这为应用框架提供了灵活的加载策略。
  • 保护程序安全: 防止了恶意代码通过自定义类替换Java核心库中的类,因为这些核心类总是由启动类加载器加载,不会受到其他类加载器的干扰。 这一机制是Java平台能够实现稳定运行和高度模块化的重要原因之一。

3. 垃圾回收

https://juejin.cn/post/7363301791118295081


这是一个从 https://juejin.cn/post/7369038974550163471 下的原始话题分离的讨论话题