🔥个人主页: 中草药

🔥专栏:【Java】登神长阶 史诗般的Java成神之路


📔 一.概念

        Java虚拟机(JVM, Java Virtual Machine)是Java平台的核心组件,它使得Java程序可以在任何安装了JVM的平台上运行,而不需要关心底层的操作系统和硬件架构。JVM的主要职责包括加载、验证、准备、解析和执行Java字节码,以及自动管理内存。

虚拟机是指通过软件模拟的具有完整硬件功能的、运行在⼀个完全隔离的环境中的完整计算机系统。

📕二.内存区域的划分

        Java虚拟机(JVM)的内存区域划分是理解Java应用程序如何管理和使用内存的关键。JVM将运行时数据区分为几个不同的部分,每个部分都有其特定的功能和生命周期。以下是JVM内存区域的主要划分及其详细说明:

1. 程序计数器 (Program Counter Register)

  • 功能:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码指令的行号指示器。它保存了下一条要执行的指令的地址(Java字节码)。
  • 特点
    • 每个线程都有一个独立的程序计数器。
    • 如果线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
    • 如果线程正在执行的是Native方法,则程序计数器的值为undefined
    • 这是唯一一个不会抛出OutOfMemoryError的区域。

2. Java虚拟机栈 (Java Virtual Machine Stacks)

  • 功能:Java虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储函数中的局部变量,函数的形参,函数之间的调用关系。(JVM之上运行的java代码的方法调用关系)
  • 特点
    • 每个线程都有一个私有的栈,它的生命周期与线程相同。
    • 栈中的数据都是线程私有的,不会被其他线程访问。
    • 如果线程请求的栈深度大于虚拟机允许的最大深度,会抛出StackOverflowError
    • 如果无法申请到足够的内存来支持新的栈帧,会抛出OutOfMemoryError

3. 本地方法栈 (Native Method Stacks)

  • 功能:与Java虚拟机栈类似,但服务于本地方法(用C/C++等语言编写的方法)。(JVM里面,C++代码的函数调用关系)
  • 特点
    • 本地方法栈也是线程私有的。
    • 在HotSpot VM中,本地方法栈和Java虚拟机栈是一起实现的。
    • 如果无法申请到足够的内存来支持新的栈帧,也会抛出OutOfMemoryError

4. 堆 (Heap)

  • 功能:堆是所有线程共享的一块内存区域,JVM中最大的空间,几乎所有的对象实例都在这里分配内存。(new 出来的对象都在堆上)
  • 特点
    • 堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。
    • 堆可以细分为新生代(Young Generation)和老年代(Old Generation)。
    • 新生代又可以进一步划分为Eden区和两个Survivor区(From Survivor和To Survivor)。
    • 如果堆中的内存不足以完成一次分配请求,并且也没有办法再扩展,或者不允许扩展(由-Xmx设置最大堆大小),则会抛出OutOfMemoryError

5. 元数据区(方法区 (Method Area))

  • 功能:方法区也是所有线程共享的内存区域,用于存储已被虚拟机加载的信息、常量、静态变量、即时编译器编译后的代码等数据。(保存代码中涉及到类的相关信息)
  • 特点
    • 方法区有一个别名叫做“非堆”(Non-Heap),目的是与Java堆区分开来。
    • 方法区的大小可以通过-XX:MaxPermSize(对于较旧的JVM版本)或-XX:MaxMetaspaceSize(对于较新的JVM版本)参数来控制。
    • 如果方法区无法满足内存分配需求,也会抛出OutOfMemoryError

6. 运行时常量池 (Runtime Constant Pool)

  • 位置:运行时常量池是方法区的一部分。
  • 功能:用于存放编译期生成的各种字面量和符号引用,在类加载后进入方法区的运行时常量池中存放。
  • 特点
    • 具有动态性,Java语言并不要求常量一定只能在编译期产生,运行期间也可能将新的常量放入池中。
    • 运行时常量池相对于Class文件常量池的一个重要特征是具备动态性。

7. 直接内存 (Direct Memory)

  • 功能:直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
  • 特点
    • 通过java.nio.ByteBuffer.allocateDirect()等API可以直接访问系统内存,这部分内存不受Java堆大小限制,但是受本机总内存(包括RAM以及SWAP区或者分页文件)大小及处理器寻址空间的限制。
    • 如果本机直接内存余量不足,可能会导致OutOfMemoryError

总结

  • 线程私有:程序计数器、Java虚拟机栈、本地方法栈。
  • 线程共享:堆、方法区、运行时常量池。
  • 非标准区域:直接内存。

在一个java进程中,元数据和堆都只有一份(同一个进程中的多个线程都是共用同一份数据的)

一个变量处于那个内存区域,和变量的“内置类型”无关,而和变量的类型有关

局部变量->栈

全局变量->堆

静态成员变量->元数据区

        理解这些内存区域及其特性对于优化Java应用程序的性能、调试内存问题以及设计高效的垃圾回收策略非常重要。例如,合理配置堆大小、选择合适的垃圾收集器、避免内存泄漏等问题都需要对这些内存区域有深入的理解。

📙三.类加载

 1.加载

在加载阶段,JVM需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流

    • 这可以通过从文件系统读取类文件、从网络下载类文件、从数据库中读取类信息等多种方式实现。
    • 类加载器(ClassLoader)负责执行这个任务。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    • 在方法区内创建类的元数据,包括类的名称、父类信息、接口信息、字段和方法信息等。
  • 在内存中生成一个代表这个类的java.lang.Class对象

    • 作为方法区这个类的各种数据的访问入口。

2.验证

目的:验证被加载的.class文件是否正确,是否合法

内容: 

 文件格式验证:验证字节码是否符合Class文件格式规范。

元数据验证:检查类中的元数据信息是否有错误,如是否存在不合法的继承关系。

字节码验证:通过数据流分析和控制流分析,确保程序语义正确,不会出现非法访问等问题。

符号引用验证:确保类中引用的其他类、字段和方法确实存在,并且可以被当前类访问。 

3.准备 分配内存空间

根据刚刚所读取到的内容,确定出类对象所需要的内存空间,申请内存空间,并都进行初始化

例如,int类型的静态变量会被初始化为0,boolean类型的静态变量会被初始化为false,引用类型的静态变量会被初始化为null

注意,这里只是分配内存并设置默认值,并不执行用户自定义的初始化逻辑(这将在初始化阶段进行)。

4.解析 主要针对类的字符串常量进行处理

目的:将常量池内的符号引用替换为直接引用。

内容

符号引用是在编译时确定的(可以理解为于一个相对位置,通过偏移量的形式去描述),而直接引用是在运行时根据符号引用找到实际的内存地址。

包括类或接口、字段、类方法、接口方法等的解析。

解析操作可能会触发其他类的加载。

5.初始化 针对类对象做最终的初始化操作

执行静态成员的赋值语句

📘四.双亲委派模型

        双亲委派模型(Parent Delegation Model)是Java类加载器(Class Loader)的一种工作机制。它实际上是类加载过程中第一个步骤里面的一个环节

        这种机制确保了类加载过程中的安全性、稳定性和唯一性,防止了核心API被篡改,并且保证了不同类加载器加载的相同名称的类是同一个类。

工作原理

类加载器层次结构

Java中的类加载器通常有以下几种:

  • 启动类加载器 (Bootstrap Class Loader)

    • 是最顶层的类加载器。
    • 负责加载JDK内部的类,通常是$JAVA_HOME/jre/lib目录下的核心库文件。
    • 由本地代码实现,通常是C/C++编写,而不是Java。
  • 扩展类加载器 (Extension Class Loader)

    • 是启动类加载器的子类加载器。
    • 负责加载JDK扩展目录中的类,通常是$JAVA_HOME/jre/lib/ext目录下的jar包。
  • 应用程序类加载器 (Application Class Loader)

    • 是扩展类加载器的子类加载器。
    • 负责加载应用程序类路径(classpath)下的类,包括用户自定义的类和第三方库。
    • 也被称为系统类加载器(System Class Loader)。
  • 自定义类加载器 (Custom Class Loader)

    • 开发者可以继承java.lang.ClassLoader来创建自定义的类加载器。
    • 用于实现特定需求,例如从网络加载类、加密类等。

总结 JVM默认有三个类加载器(自定义加载器和应用程序加载器通常看为一体)

BootstrapClassLoader 负责加载标准库中的类

ExtensionClassLoader 负责加载拓展类

ApplicationClassLoader 负责加载 第三方库/自己代码写的类

        在双亲委派模型中,当一个类加载器接收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一层的类加载器都是如此,因此所有的类加载请求最终都会传到顶层的启动类加载器(Bootstrap Class Loader)。只有当父类加载器无法完成这个加载请求(即在它的搜索范围里没有找到所需的类)时,子类加载器才会尝试自己去加载。

具体步骤如下:

  1. 接收请求:某个类加载器接收到类加载的请求。
  2. 向上委派:该类加载器将请求委派给其父类加载器。
  3. 递归委派:父类加载器继续向上委派,直到达到最顶层的启动类加载器。
  4. 查找类:从上至下,每个类加载器在其负责的路径下查找并加载类。
  5. 加载失败:如果父类加载器未能找到该类,则将控制权交回给子类加载器。
  6. 尝试加载:子类加载器尝试在其自己的路径下查找并加载类。
  7. 加载成功或抛出异常:如果找到了类,则加载;如果没有找到,则抛出ClassNotFoundException

优点

安全:防止核心类(标准库中的类)被用户自定义的同名类所替换,保证标准库中的类优先级是最高的,保证了了Java平台的安全性。

稳定:避免了类的重复加载,确保了类的唯一性。

性能优化:通过缓存机制,已经加载过的类不需要再次加载,提高了性能。

📗五.垃圾回收机制(GC)

为什么要有垃圾回收?

        在编程语言中,尤其是像Java这样的高级语言中,垃圾回收机制的引入主要是为了自动化管理内存。它帮助开发者避免了手动分配和释放内存所带来的复杂性和潜在错误,如内存泄漏和野指针问题。通过自动回收不再使用的对象所占用的内存空间,垃圾回收机制提高了程序的健壮性和开发效率。

GC是有代价的,在JVM引入额外的逻辑,影响开发效率,运行效率

(1)消耗不少的CPU开销,去进行垃圾的扫描和释放

(2)进行GC的时候可能会触发STW问题,导致程序卡顿(STW对于性能要求高的场景会有很大影响)

垃圾回收主要回收哪个内存区域?

        垃圾回收主要针对的是Java堆(Heap)内存。堆是所有线程共享的一块内存区域,用于存放对象实例。此外,方法区(Method Area)中的某些数据,例如废弃的类信息,在特定条件下也会被回收。但通常我们讨论的垃圾回收更多是指堆内存的回收。

        垃圾回收,是以对象为纬度进行回收的,整个对象都不在使用才整个回收。

具体回收过程

标记的过程

标记阶段是垃圾回收过程的第一步,说白了就是找出垃圾其目的是识别出哪些对象还在使用,哪些可以被回收。

1.引用计数

        引用计数是一种垃圾回收技术,它通过为每个对象分配一个计数器来跟踪该对象被引用的次数。每当有新的引用指向这个对象时,计数器就加一;每当有一个引用不再指向这个对象时,计数器就减一。当计数器变为零时,说明没有引用再指向该对象,这时可以安全地回收该对象所占用的内存。

引用计数方案在JVM中没有被采纳,主要有两个主要原因

        1.会消耗额外的空间

        2.引用计数可能导致“循环引用”

        循环引用 如果两个或多个对象之间存在循环引用(即互相引用),即使这些对象实际上已经不可达,它们的引用计数也不会变为零,导致内存泄漏。

A -> B
^    |
|    v
B -> A

        假设有一个简单的系统,其中有两个对象A和B,并且A持有B的一个引用,同时B也持有A的一个引用。在这种情况下,即使A和B都不可达了,它们的引用计数都不会变成零,因为它们彼此之间还保持着引用。这就是典型的循环引用问题。

循环引用的问题也是有解决方案的,需要引入更多的机制(环路检测),代价更大了

2.可达性分析

        可达性分析(Reachability Analysis)是垃圾回收中用于确定哪些对象仍然在使用,哪些对象可以被安全回收的一种方法(周期性)。与引用计数不同,可达性分析通过从一组称为“GC Roots”的对象开始,沿着这些根对象的引用链来追踪所有可达的对象。如果一个对象无法通过任何路径从GC Roots到达,那么它就被认为是不可达的,可以被垃圾回收器回收。(可以形象类比于二叉树)

基本概念

  • GC Roots:这是进行可达性分析的起点。常见的GC Roots包括:

    • 虚拟机栈中的局部变量
    • 方法区中的静态属性
    • 方法区中的常量引用
    • 正在执行的方法内的局部变量
    • JNI引用
    • 活跃线程
    • JVM内部使用的某些特殊对象
  • 引用链:从GC Roots出发,通过对象间的引用关系形成的一条或多条路径。如果一个对象能通过这条路径被访问到,那么它就是可达的。

可达性分析实际上是比较消耗时间的

回收的过程

1.标记清除 算法

直接针对内存中被标记可回收的对象进行释放

这样的做法会引入“内存碎片问题”,很有可能要释放的对象是所对应的内存并不是连续的,长期使用会导致大量不连续的小内存区域,这会使得大对象难以分配到足够的连续内存空间。尽量避免内存碎片,是释放内存的关键性问题

2.复制 算法

它通过将存活的对象从一个区域复制到另一个区域来实现内存的清理

  1. 标记存活对象:确定哪些对象是存活的。
  2. 复制存活对象:将存活对象从一个区域复制到另一个区域。
  3. 清空旧区域:清除不再使用的区域,为下一次分配做准备。

缺点也很明显

        1.内存空间利用率低

        2.如果存活下来的对象比较多,复制的成本也比较大

3.标记-整理 算法

它结合了标记-清除(Mark and Sweep)和复制(Copying)算法的优点。这种算法旨在解决标记-清除算法中的内存碎片问题,同时避免复制算法中需要额外空间的问题。

  1. 移动存活对象:将所有存活对象向一端移动,以消除内存碎片。这个过程通常是从堆的一端开始,将存活对象紧凑地排列在一起。
  2. 更新引用:修改所有指向这些存活对象的引用,使其指向新的位置。这一步是必要的,因为对象的位置发生了变化。
  3. 清理空闲空间:在移动完成后,所有存活对象之后的空间都可以视为连续的空闲空间,可以用来分配新的对象。

搬运的开销也是比较大的

JVM真实的解决方案,是把上述几个方案综合一下-分代回收

在分代回收中,堆内存被划分为不同的区域或“代”,每个代具有不同的回收频率和算法。根据对象的“年龄”(可达性分析是周期性的,每次经过一轮扫描,对象仍然存活,年龄+1)把对象进行区分

  1. 新生代(Young Generation)

    • 包含新创建的对象。
    • 通常分为 伊甸(Eden) 区和两个幸存(Survivor) 区(S0 和 S1)。
    • 使用复制算法进行垃圾回收,因为大部分新对象很快就会死亡。
  2. 老年代(Old Generation)

    • 包含经过多次垃圾回收后仍然存活的对象。
    • 通常使用标记-整理或标记-清除算法,因为这些对象存活时间较长,不适合频繁移动。

具体回收过程如下 

1. 新生代垃圾回收(Minor GC)

  • 触发条件:当Eden区满时触发。

  • 过程:

    • 标记存活对象:从GC Roots开始,标记所有可达的对象。
    • 复制存活对象:将存活对象从Eden区和一个Survivor区复制到另一个Survivor区。
    • 清空旧区域:清空Eden区和当前使用的Survivor区。
    • 交换角色:两个Survivor区交换角色,以便下一次垃圾回收时使用。
  • 晋升:如果对象在Survivor区中存活了一定次数(通常是年龄阈值),则会被晋升到老年代。

根据经验规律,大部分的新对象活不过第一轮GC

2. 老年代垃圾回收(Major GC 或 Full GC)

  • 触发条件:当老年代满时触发。

  • 过程:

    • 标记阶段:从GC Roots开始,标记所有可达的对象。
    • 整理阶段:将存活对象向一端移动,消除内存碎片。
    • 清理阶段:释放不可达对象占用的空间。
  • Full GC:有时也称为 Major GC,但更准确地说,Full GC 是指整个堆(包括新生代和老年代)的垃圾回收。

📒六.垃圾回收器

        垃圾回收器(Garbage Collector, GC)是自动管理内存的系统,它负责识别和回收不再使用的对象所占用的内存。在Java虚拟机(JVM)中,有多种垃圾回收器可供选择,每种都有其特定的行为模式、性能特点以及适用场景。下面我将介绍几种常见的垃圾回收器及其特点。

垃圾回收算法是垃圾回收的方法论,垃圾收集器是算法的落地实现。

比如:

        新生代的收集器:serial,ParNew,Parallel Scaveng,都使用的是复制算法;

        老年代的收集器:SerialOld,Parallel Old,使用的是标记整理算法,CMS是基于标记清除算法实现的。

 CMS (Concurrent Mark Sweep) GC

CMS的设计理念,是把整个GC过程拆分成多个阶段,能和业务线程并发运行的,就尽量并发,尽可能减少STW的时间

  • 类型:并发
  • 适用场景:需要低延迟的应用
  • 特点:
    • 主要针对老年代,采用并发标记-清除算法。
    • 目标是减少垃圾回收造成的停顿时间。
    • 由于是并发执行,因此应用程序可以在垃圾回收期间继续运行。
    • 可能会产生较多的内存碎片。
    • 启用参数:-XX:+UseConcMarkSweepGC

4. G1 (Garbage-First) GC

        G1 是把整个内存,分成很多块,不同的颜色(字母)表示这一小块是新生代,老年代,进行 GC 的时候,不要求一个周期就把所有的内存都回收一遍,而是一轮 GC 只回收其中的一部分就好。

通过限制你一轮 GC 花的时间/工作量使 STW 的时间在可控范围之内 

  • 类型:并发
  • 适用场景:大堆内存,需要低延迟的应用
  • 特点:
    • 将堆分成多个大小相等的区域(Region),每个区域可以扮演 Eden、Survivor 或 Old 代的角色。
    • 采用标记-整理算法,并结合了局部收集(Partial GC)的思想。
    • 旨在提供可预测的停顿时间和高吞吐量。
    • 自动调整年轻代和老年代的大小。
    • 适用于大型堆(如数GB或更多)。
    • 启用参数:-XX:+UseG1GC

梦想一旦被付诸行动,就会变得神圣。——阿.安.普罗克特

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐