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
先 TLAB 分配
再通过 CAS 在 Eden 区分配
大对象直接分配到老年代
TLAB:线程本地分配缓冲区,为每⼀个线程预先在 Eden 区分配⼀块⼉私有的缓存区域,JVM 在给线程中的对象分配内存时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时(或者未开启 TLAB),再采⽤上述的 CAS 进⾏内存分配。默认情况 TLAB 仅占每个 Eden 区域的 1%。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。
为什么要 CAS 分配内存?
多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。
JVM 对象分配内存如何保证线程安全
在 JVM 中,为对象分配内存的过程需要确保线程安全,因为在多线程环境下,多个线程可能会同时尝试创建对象。为了保证内存分配的线程安全性,JVM 采用了以下几种机制和技术:
TLAB(Thread Local Allocation Buffer):
当一个线程需要分配对象时,首先会尝试在 TLAB 中进行分配。如果 TLAB 有足够的空间,分配过程就是线程安全的,因为没有其他线程访问这个内存块。
不足:当 TLAB 空间不足时,线程需要请求一个新的 TLAB 或者直接从共享堆中分配,这个过程需要一定的同步机制。
CAS(Compare-And-Swap)机制: 当 TLAB 耗尽或在涉及到跨线程的堆内存分配时,CAS 有效避免了竞争条件。
分代收集: 虽然不是直接用于线程安全,但分代收集(年轻代、老年代、永久代/元空间)使得内存管理更高效,减少了直接竞争的机会。
结合:TLAB 一般对年轻代的内存分配进行优化,更加局部化的内存管理有助于线程安全。 通过运用这些机制,JVM 能够在多线程环境下高效而安全地进行内存分配,并最大限度地减少同步操作带来的性能损耗。这样设计不仅提升了性能,也保证了对象内存分配的安全性和一致性。
说说对象分配规则
在 Java 中,对象分配规则是关于如何为新对象分配内存的一套规则,以确保内存的有效使用和对象的正确初始化。以下是关于对象分配的主要规则:
内存分配:新对象通常在堆内存中分配内存空间。
对象头:在为对象分配内存空间后,Java 虚拟机会为对象分配一个对象头。对象头包含了一些关于对象的元信息,如对象的哈希码、锁状态、垃圾回收信息等。
零值初始化:在对象内存分配后,所有的成员变量会被初始化为零值。具体的零值取决于变量的数据类型。例如,整数类型会初始化为 0,布尔类型会初始化为 false,对象引用会初始化为 null。
构造函数调用:一旦对象内存分配和零值初始化完成,Java 虚拟机会调用对象的构造函数。
对象引用:最后,new 关键字会返回对象的引用,将这个引用分配给一个变量,以便后续可以通过该变量访问对象的属性和方法。
垃圾回收管理: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 虚拟机进程终止
行业拓展
分享一个面向研发人群使用的前后端分离的低代码软件——JNPF。
基于 Java Boot/.Net Core 双引擎,它适配国产化,支持主流数据库和操作系统,提供五十几种高频预制组件,内置了常用的后台管理系统使用场景和实用模版,通过简单的拖拉拽操作,开发者能够高效完成软件开发,提高开发效率,减少代码编写工作。
JNPF 基于 SpringBoot+Vue.js,提供了一个适合所有水平用户的低代码学习平台,无论是有经验的开发者还是编程新手,都可以在这里找到适合自己的学习路径。
此外,JNPF 支持全源码交付,完全支持根据公司、项目需求、业务需求进行二次改造开发或内网部署,具备多角色门户、登录认证、组织管理、角色授权、表单设计、流程设计、页面配置、报表设计、门户配置、代码生成工具等开箱即用的在线服务。
评论