Java中什么是类加载?类加载的过程?

类加载指的是把类加载到 JVM 中。把二进制流存储到内存中,之后经过一番解析、处理转化成可用的 class 类

二进制流可以来源于 class 文件,或通过字节码工具生成的字节码或来自于网络。只要符合格式的二进制流,JVM 来者不拒。

虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。类加载过程包括了加载、连接、初始化三个阶段,其中连接还可以分为验证、准备、解析

加载

将二进制流读入内存中,生成一个 Class 对象

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

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

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

这个阶段既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载

验证

确保Class文件的字节流中包含的信息符合JVM规范,保证在运行后不会危害虚拟机自身的安全。即安全性检查,主要包括四种验证:

  • 文件格式验证: 验证字节流是否符合Class文件格式的规范;例如: 是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

  • 元数据验证:: 对字节码描述的信息进行语义分析(注意: 对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object之外。

  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  • 符号引用验证:确保解析动作能正确执行

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为static 变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段 完成,赋值在初始化阶段完成。也就是说这里给类变量设置初始值,设置的是数据类型默认的零值(如0、0L、null、false等)

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

  • 如果 static 变量是 final 的,但属于引用类型,那么赋值会在初始化阶段完成

解析

将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。

  • 未解析时,常量池中的看到的对象仅是符号,未真正的存在于内存中

  • 解析以后,会将常量池中的符号引用解析为直接引用

初始化

初始化阶段会执行cinit方法来为 类变量static变量 赋上定义的值并执行类中的静态代码块;这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值

  • 使用静态代码块为类变量指定初始值

何时进行类加载?

  • 定义了main的类,启动main方法时该类会被加载

  • 创建类的实例,即new对象的时候

  • 访问类的静态方法

  • 访问类的静态变量

  • 反射 Class.forName()

JVM初始化步骤?

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类

  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类

  • 假如类中有初始化语句,则系统依次执行这些初始化语句

初始化发生的时机?

概括得说,类初始化是【懒惰的】,只有当对类的主动使用的时候才会导致类的初始化

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发父类初始化

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName new 会导致初始化

不会导致类初始化的情况?

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

  • 类对象.class 不会触发初始化

  • 创建该类的数组不会触发初始化

  • 类加载器的 loadClass 方法

  • Class.forName 的参数 2 为 false 时

cinit方法如果执行失败了怎么办,这个类还能用吗?

  • 在Java类加载的过程中,cinit 方法实际上指的是类的静态初始化方法,也就是类的静态代码块或者静态变量的初始化代码。如果类的静态初始化方法执行失败,通常会导致类的初始化失败,这意味着这个类不能被正常使用。会抛出异常,如 ExceptionInInitializerError

  • 在Java中,类的静态初始化方法只会执行一次,无论类被加载多少次,静态初始化方法只会在首次加载类的时候执行。因此,cinit 方法不会多次执行。一旦类的静态初始化方法执行过,后续对同一个类的加载都不会再次触发静态初始化方法的执行。这种机制确保了类的静态初始化只会在需要的时候执行一次,避免了不必要的开销和重复操作。

分配内存

在类加载后,接下来虚拟机将为新⽣对象分配内存。

分配在哪?

主要就是根据JVM的分配机制:对象优先分配Eden

  1. 先TLAB分配
  2. 再通过CAS在Eden区分配
  3. 大对象直接分配到老年代

TLAB:线程本地分配缓冲区,为每⼀个线程预先在 Eden 区分配⼀块⼉私有的缓存区域,JVM 在给线程中的对象分配内存时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时(或者未开启TLAB),再采⽤上述的 CAS 进⾏内存分配。默认情况TLAB仅占每个Eden区域的1%。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。

为什么要CAS分配内存?

多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。

JVM对象分配内存如何保证线程安全

在JVM中,为对象分配内存的过程需要确保线程安全,因为在多线程环境下,多个线程可能会同时尝试创建对象。为了保证内存分配的线程安全性,JVM采用了以下几种机制和技术:

  1. TLAB(Thread Local Allocation Buffer):

    1. 当一个线程需要分配对象时,首先会尝试在TLAB中进行分配。如果TLAB有足够的空间,分配过程就是线程安全的,因为没有其他线程访问这个内存块。
    2. 不足:当TLAB空间不足时,线程需要请求一个新的TLAB或者直接从共享堆中分配,这个过程需要一定的同步机制。
  2. CAS(Compare-And-Swap)机制: 当TLAB耗尽或在涉及到跨线程的堆内存分配时,CAS有效避免了竞争条件。

  3. 分代收集: 虽然不是直接用于线程安全,但分代收集(年轻代、老年代、永久代/元空间)使得内存管理更高效,减少了直接竞争的机会。

结合:TLAB一般对年轻代的内存分配进行优化,更加局部化的内存管理有助于线程安全。

通过运用这些机制,JVM能够在多线程环境下高效而安全地进行内存分配,并最大限度地减少同步操作带来的性能损耗。这样设计不仅提升了性能,也保证了对象内存分配的安全性和一致性。

说说对象分配规则

在Java中,对象分配规则是关于如何为新对象分配内存的一套规则,以确保内存的有效使用和对象的正确初始化。以下是关于对象分配的主要规则:

  1. 内存分配:新对象通常在堆内存中分配内存空间。
  2. 对象头:在为对象分配内存空间后,Java虚拟机会为对象分配一个对象头。对象头包含了一些关于对象的元信息,如对象的哈希码、锁状态、垃圾回收信息等。
  3. 零值初始化:在对象内存分配后,所有的成员变量会被初始化为零值。具体的零值取决于变量的数据类型。例如,整数类型会初始化为0,布尔类型会初始化为false,对象引用会初始化为null。
  4. 构造函数调用:一旦对象内存分配和零值初始化完成,Java虚拟机会调用对象的构造函数。
  5. 对象引用:最后,new 关键字会返回对象的引用,将这个引用分配给一个变量,以便后续可以通过该变量访问对象的属性和方法。
  6. 垃圾回收管理:Java虚拟机会自动管理对象的内存。如果对象不再被引用,它会被标记为垃圾,并在适当的时机由垃圾回收器回收,释放占用的内存。

这些规则确保了对象在创建时的正确初始化和内存管理。对于程序员来说,最重要的是编写好构造函数以确保对象在创建后具有合适的初始状态,并且不忘记在不再需要对象时将引用置为null,以便垃圾回收器能够回收不再使用的对象。

何时进行类卸载?

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

Java虚拟机将结束生命周期的几种情况?(什么情况会导致JVM退出)

  • 正常程序终止: 当程序执行完main方法,包括所有非守护线程都终止时,JVM将正常退出。
  • 调用System.exit(int status): 显式调用System.exit()方法,以指定的状态码终止当前运行的Java虚拟机。
  • 未捕获的异常或错误: 如果某个线程抛出的异常没有被捕获,并且此异常传播到了主线程,JVM可能会终止。
  • Runtime.halt(int)或崩溃:
    • 直接调用Runtime.halt()会立即停止Java进程,类似于突然终止程序而不调用任何钩子。
    • JVM的致命错误(如内存访问违规)也可能导致崩溃并退出。
  • 外部命令强制关闭: 例如通过操作系统的任务管理器或者控制台命令,如kill命令。或者操作系统出现错误而导致Java虚拟机进程终止
相关推荐
小猫咪怎么会有坏心思呢5 分钟前
华为OD机考-生成哈夫曼树-二叉树(JAVA 2025B卷)
java·开发语言·华为od
翱翔的小菜鸟31 分钟前
Java Stream API中peek()方法使用不当引发的生产问题
java·开发语言
xcs1940532 分钟前
java 导入数据和数据验证处理方案
java·linux·python
哪吒编程43 分钟前
我的第一个AI编程助手,IDEA最新插件“飞算JavaAI”,太爽了
java·后端·ai编程
Code季风1 小时前
SQL关键字三分钟入门:WITH —— 公用表表达式让复杂查询更清晰
java·数据库·sql
沿着缘溪奔向大海1 小时前
蓝牙数据通讯,实现内网电脑访问外网电脑
java·爬虫·python·socket·蓝牙
过期动态1 小时前
MySQL中的常见运算符
java·数据库·spring boot·mysql·spring cloud·kafka·tomcat
想用offer打牌2 小时前
一站式了解责任链模式
java·后端·设计模式·责任链模式
专注VB编程开发20年2 小时前
C# .NET多线程异步记录日声,队列LOG
java·开发语言·前端·数据库·c#
京东云开发者2 小时前
由 Mybatis 源码畅谈软件设计(八):从根上理解 Mybatis 二级缓存
java