JVM ①-类加载 || 内存区域

发布于:2025-02-11 ⋅ 阅读:(8) ⋅ 点赞:(0)

这里是Themberfue 

  • 终于结束了网络层的学习,当然,我们学习的知识也只是冰山一角,想要了解更多的知识,还请大家养成主动探索的习惯~~~
  • 接下来我们将对 JVM 的一些机制进行简单的讲解,对于 Java程序员来说,本身是不需要关心 JVM 底层所封装好的这些机制的,我们只需编写 Java 代码即可,最直观的体会就是:编写 Java程序时不需要过多的关心内存管理相关的事项,这些都可以交给 JVM 进行控制。
  • 但是,由于市场人才的饱和,市场对于 Java程序员的能力也是越来越高,面对面试官的拷打,我们还是需要了解一些最基本的机制的。
  • 了解这些机制其实也不全是应对面试,在日常开发调试中,可能也会应用到相关的知识。例如:一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。只是概率不大~~~

 内存区域划分

  • JVM 想必大家都不陌生,JVM 是 Java 运行环境的核心,.java 文件首先被 javac 编译程序编译成 .class 文件(也就是字节码文件)后,JVM 负责运行 Java 字节码。所以,Java程序最终都是在 JVM 上运行的。
  • 有了 JVM,Java 便实现了 "一次编写,到处运行"(Write Once, Run Anywhere)的特性,使得 Java 代码可以在不同操作系统(Windows、Linux、Mac)上运行,只要安装了 对应的 JVM

  • 操作系统中也对内存区域进行了划分,以便更好的管理。既然是 Java虚拟机,也同理,JVM 进程向操作系统申请了一块空间,随后 JVM 对这块空间进行区域划分。

  • 根据图中我们可以知道,线程私有的区域有:程序计数器、本地方法栈、虚拟机栈线程共享的有:堆、方法区、直接内存(非运行时数据区域的一部分)

程序计数器

  • 程序计数器是一块较小的内存空间,其主要作用是记录当前线程正在执行的字节码指令地址,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环等功能都依靠这个计数器完成。
  • 每个线程都有单独配有一个程序计数器,便于在线程切换后能恢复到正确的执行位置,这些程序计数器独立存在,互相不冲突,是线程私有的。
  • ⚠️ 注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

虚拟机栈

  • 与程序计数器一样,虚拟机栈也是线程私有的,其生命周期随着线程的创建而创建,随着线程的结束而死亡。
  • 虚拟机栈主要存储方法调用的局部变量、操作数栈、返回地址。除了一些 Native 方法调用是通过本地方法栈实现的,大部分 Java 方法调用都是通过虚拟机栈实现的。
  • 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
  • 栈里面存储的是栈帧,每个栈帧都拥有:局部变量表、操作数栈、动态链接、方法返回地址
  • 虚拟机栈的默认空间分配不是很大,一般在 10MB - 70MB 左右,一般情况下是完全够用的。当然,如果函数调用陷入无限循环,也就是死递归后,虚拟机栈中被压入太多栈帧而导致空间超过当前申请的最大空间的话,就会抛出 StackOverFlowError 错误。
  • 实际上,虚拟机栈的大小是可以动态扩展的,如果在动态扩展时无法申请更大的内存空间后,程序就会抛出 OutOfMemoryError 异常。
  • Java 方法返回有两种方式,一种是正常代码执行到 return 语句退出,另一种则是抛出异常,不论是哪种方法返回,虚拟机栈都会认为该方法正常执行完毕,便会将对应的栈帧弹出。

本地方法栈

  • 与虚拟机栈类似,本地方法栈主要用于本地(Native)方法 的执行,调用 C 语言写的 JNI(Java Native Interface)方法 时使用。
  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
  • 方法执行完毕后也会将对应的栈帧弹出,也会出现 StackOverFlowError 错误和 OutOfMemoryError 异常。

  • 用于存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。堆是 JVM 申请到的最大一块内存区域,堆是所有线程共享的,在虚拟机启动时就会创建。
  • 由于是所有线程共享,所以可能会发送并发问题(需要同步管理)。类的成员变量都是存放在堆上的,而局部变量都是存放在栈上的,至于静态成员变量,则是在元数据区(方法区,下方会讲解)。
  • 堆也是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。至于 JVM 的 GC 是怎么进行垃圾回收的,我们下节课再来讲解。
  • 堆这里最容易出现的就是 OutOfMemory 错误,并且出现这种错误之后的表现形式还会有几种,比如:
  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  • java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap size) (上述描述原文

方法区

  • 方法区也可称作 元数据区,当虚拟机需要使用一个类时,它读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
  • JDK 8 之前 方法区位于堆内存,称为 永久代(PermGen)
  • JDK 8 之后 方法区使用 本地内存(Metaspace),避免 OutOfMemoryError

类加载

  • Java 类加载(Class Loading)是 JVM 将 .class 文件(字节码)加载到内存,并解析为可执行 Java 类 的过程。类加载是 Java 运行机制的核心,它支持 Java 的动态特性,比如 反射、动态代理、热部署 等。

  • 类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:

  • 使用(Using)卸载(Unloading)阶段可以忽略,所以主要还是 5 个阶段。其中,验证(Verification)准备(Preparation)解析(Resolution)阶段可以被统称为 连接(Linking)

  • 系统加载 Class 类型的文件主要分三步:加载 => 连接 => 初始化。其中,连接过程又可分为三步:验证 => 准备 => 解析


加载

  • JVM 根据 类 的全限定名(包名 + 类名,形如 Java.lang.String)再通过类加载器(ClassLoader)找到 .class 文件,并读取字节码,将其读取到内存里随后转换为 JVM 内部数据结构。
  • 同时创建 java.lang.Class 对象,代表该类的运行时信息。
  • 类加载的方式有很多种:从本地文件(磁盘 .class 文件)加载从 JAR 包中加载通过网络(远程加载)动态生成(如反射、动态代理、defineClass 方法)热加载(如 Tomcat、Spring 热部署)。
  • 加载阶段连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

验证

  • 确保字节码文件的安全性和正确性,防止 JVM 崩溃或安全漏洞。
  • 验证过程包含 4 个方面
    1. 文件格式验证:检查字节码格式是否符合 JVM 规范。
    2. 元数据验证:检查类是否继承了不存在的类、方法签名是否正确等。
    3. 字节码验证:确保字节码的正确性,比如操作数栈是否匹配,跳转指令是否正确等。
    4. 符号引用验证:确保类、字段、方法等可以正确解析。
  • 为了保证这些信息被当作代码运行后不会危害虚拟机自身的安全,官方对 Class 文件的格式类型进行了规范,根据 Java 虚拟机规范,Class 文件通过 ClassFile 定义,有点类似 C 语言的结构体。我们可以在官方文档查到 ClassFile 的定义:ClassFile
  • 以上是对 ClassFile 的简单描述,想要了解更加深入的同学,可以去官方文档细看。

准备

  • 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 此时不会执行任何静态代码块,只是赋默认值!
    class Demo {
        static int a = 10; // 这里的 a 先被赋默认值 0,后面才赋值 10
    }
    

解析

  • 针对字符串常量进行初始化,从 .class 文件里解析出来的字符串常量放到 内存空间(元数据区、常量池)里。
  • 将类中的符号引用转换为直接引用(如将 java/lang/String 转换为内存地址)。
  • 解析的内容包括:
    1. 类或接口解析(解析父类、接口)
    2. 字段解析(解析 staticinstance 变量)
    3. 方法解析(解析方法调用)
    4. 接口方法解析
  • String s = "Hello"; String 是一个符号引用,解析后 JVM 知道 "Hello" 的具体内存地址。

初始化

  • 执行静态变量赋值和静态代码块(static {}
  • 只有在第一次使用该类时才会触发(比如 newClass.forName())。
  • 针对类对象中的各种属性进行填充。
  • 如果有父类,必须先初始化父类

⭕使用

  • 类被加载后,可以被实例化、调用方法、访问静态变量
  • 类的使用阶段包括实例化对象、方法调用等
    Demo d = new Demo(); // 使用阶段

⭕卸载

  • JVM 在满足以下条件时会卸载类
    1. 该类的所有实例都被回收(无强引用)。
    2. 类加载器被回收(通常是自定义 ClassLoader)。
    3. JVM 确定该类不再被使用

注意:

  • Bootstrap ClassLoader 加载的类不会被卸载
  • 一般只有动态加载的类(如 Tomcat、Spring 热部署)才会被卸载

  • 下节我们将进入 JVM 的另外两个机制~~~
  • 毕竟不知后事如何,且听下回分解 
  • ❤️❤️❤️❤️❤️❤️❤️

 


网站公告

今日签到

点亮在社区的每一天
去签到