【JVM】:类加载机制,jvm内存布局,垃圾回收,String 不可变性源码分析

一、 类加载机制

类加载器

  1. 启动类加载器 (Bootstrap ClassLoader) :C++ 实现,负责加载 Java 核心库(如 java.lang.*,java.util.*, java.io.*),它是大 Boss,最顶层。

  2. 扩展类加载器 (Extension ClassLoader):负责加载 Java 的扩展库,官方提供的"插件包。

  3. 应用程序类加载器 (Application ClassLoader):负责加载第三方库(非 Java 官方提供)或用户路径下的类,即你自己写的代码。

双亲委派模型

当一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是

  • 向上委派:把这个请求委派给父类加载器去完成。每一层都是如此,直到最顶层的启动类加载器。

  • 向下加载:只有当父加载器反馈自己无法完成这个加载请求(在它的搜索范围中没找到)时,子加载器才会尝试自己去加载。

  • 为什么需要双亲委派 :①保证同一个类只被加载一次②防止恶意代码替换 Java 核心类,如果你自己写了一个 java.lang.String 类试图窃取用户密码。如果没有双亲委派,JVM 可能会加载你这个假 String

五个阶段

  • 加载 (Loading) :根据包名,类名找到二进制.class文件,读到内存中,在元数据区 创建 Class 对象。

  • 验证 (Verification):确保.class字节码符合 JVM 规范,没有安全漏洞(比如不是乱改的伪造文件),并把内容转为结构化数据。

  • 准备 (Preparation) :为类对象,静态变量 分配内存,并设置默认初始值 (如 int 设为 0,而不是代码里的初始值)。

  • 解析 (Resolution):将常量池内的符号引用(名字)替换为直接引用(内存地址)。

  • 初始化 (Initialization) :执行静态代码块和静态变量的赋值动作(这时 static int a = 10 才真正变成 10)。类对象初始化--对类对象属性填充--若有父类加载父类

二、 JVM 内存划分区域

一个运行中的 Java 虚拟机(JVM)实例就是一个操作系统进程:如果你同时开了两个终端窗口,分别运行两个不同的 Java 程序,那么你的任务管理器里就会出现两个 java.exe 进程

一个进程内可以有多个线程 :你写的 Java 文件中可能包含 main 方法,它是主线程。你还可以在代码里启动成百上千个新线程,但它们都"住"在同一个进程里,共享这个进程的堆内存、元空间等资源

对Java 开发者来说,**JVM(java虚拟机)**不仅仅是一个运行环境,它是 Java 能够实现"一次编写,到处运行的核心功臣

我们可以把 JVM 理解为一个翻译官:它将跨平台的字节码(.class 文件)翻译成当前操作系统(Windows、Linux、macOS)能听懂的机器指令

0. 对象引用 vs 对象实例

  • 对象引用(Reference) :本身不是对象,而是一个存储了内存地址的变量。存放在栈(Stack) 中(如果是局部变量)或者堆中(如果是另一个对象的属性)

  • 对象实例(Instance/Object) :它是根据类图纸(Class)在堆中实际开辟出来的内存空间,包含了对象的所有属性数据,永远存放在 堆(Heap)

1. 程序计数器

  • 存放内容:记录当前线程执行到哪个指令了

  • 特性: 线程私有,每个线程有自己独立的程序计数器

2. 元数据区

  • 存放内容:类的元数据信息(比如某类有哪些方法、哪些字段)、静态变量、常量池。

  • 特性 :线程共享,使用本地内存,受系统可用内存限制。

3. 栈 (Stack)

  • 虚拟机栈 :存放局部变量、基本数据类型的值(int, double 等)、对象的引用地址

  • 本地方法栈:存放c++写的native方法

  • 特性 :线程私有,生命周期随线程同步,通过"栈帧"管理方法调用,不涉及 GC,方法执行完,栈帧随之释放。

4. 堆 (Heap)

  • 存放内容 :几乎所有的对象实例(new出来的)以及数组(如 int[] arr)。

  • 特性 :线程共享,是垃圾回收器(GC)的主要战场。

  • 划分 :为了优化回收效率,分为新生代 (分为Eden, S0, S1)和老年代----新老比1:2,新内部比8:1:1

三、垃圾回收

当一个对象不再被任何"根"指向时,它就变成了垃圾

1. 寻找垃圾的方法

  • 引用计数法:每new一个对象搭配一小块内存,保存一个证书表示当前对象有几个引用;简单但内存消耗大,无法解决"循环引用"问题(A 引用 B,B 引用 A,但没人用 A 和 B)。

  • 可达性分析算法(JVM 采用) :从 GC Roots(如栈上的局部变量、常量池引用指向的对象,静态属性等)作为起点向下搜索,做尽可能的遍历,可以访问到的标记为可达。搜索不到的对象即判定为可回收。

2. 回收垃圾的方法

复制算法:这是新生代(Eden 和 Survivor 区)最常用的招式

  • 原理:将内存容量划分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉

标记-整理算法:老年代常用,老年代存活率高,复制算法在这里会由于搬运对象太多而导致效率低下,且没有额外的空间进行担保

  • 原理:先扫描整个区域,标记出所有存活的对象;让所有存活的对象都向内存的一端移动,然后直接清理掉端边界以外的内存

分代收集 :是一种**管理策略,**它根据对象存活周期的不同将内存划分为几块

  • 原理:先扫描整个区域,标记出所有存活的对象;让所有存活的对象都向内存的一端移动,然后直接清理掉端边界以外的内存

  • 新生代1 :分为【Eden 8:Survivor1:Survivor1】对象出生快,死得也快,新生代占用空间小,采用复制算法效率高

  • 老年代2 :对象生命力顽强,空间大,采用标记-清除标记-整理算法。因为没有额外空间做担保,且对象移动频率低

  • 要点:

    很大的对象直接放入老年代,因为大对象复制成本高

    GC年龄:每经过一次GC没有被释放,年龄+1

    新生代比老年代回收频率高

四、String 的存储艺术:

1. 不可变性

java 复制代码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    private int hash; // Default to 0

//创建一个String对象时,hash属性的默认值是0,第一次调用 hashCode() 方法时才计算

}
  • 类的final :表示类不可继承;防止通过继承来破坏其行为,没人能通过写一个 MyString 来改变其核心逻辑

  • 属性的 private:表示 value[]只可在当前类使用,外部拿不到str地址,就修改不了它的值

  • **属性的 final:**不允许重定向,一旦指向某个地址,就不可指向别的地址

  • hash:

  • 不提供 Setter 方法 :源码中所有看似修改字符串的方法(如 replace(), substring(), toUpperCase()),本质上都是在方法内部 new 了一个全新的 String 对象并返回,原字符串纹丝不动

  • hash: 属于懒加载:如将 String 作为 Key 放入 HashMap 或 HashSet【map.put("key", value) 或 set.add("key")】需要计算 Key 的哈希值,以决定将其放入哈希表(数组)的哪个桶;两个字符串进行 equals() 比较之前,会先对比两个对象的 hashCode,如果 hash1 != hash2,那么这两个对象一定不相等,直接返回 false,从而避免了 O(n)的逐个字符对比

2. 为什么这样设计

  • 字符串常量池的需要:只有不可变,多个变量才能安全地共享池中的同一个对象。

  • 缓存 Hash 值 :由于内容不变,hash 只需要计算一次并缓存。这让 String 在作为 HashMap 的 Key 时性能极高。

  • 安全性:网络连接、文件路径、数据库连接通常使用 String 传递。如果 String 可变,可能会被恶意篡改导致安全漏洞。

  • 线程安全:多个线程同时访问同一个 String 对象,永远不需要加锁

3. 存储位置的差异

  • 直接赋值 (如 String s = "abc"):对象存放在字符串常量池 中。它底层是一个 StringTable(哈希表结构),能实现相同字面量的复用,如果发现常量池已经有abc就直接指向而不创建新的。

  • new 构造 (如 new String("abc")):会在堆内存中额外创建一个独立的对象,即使内容相同,地址也不相等。

4.String -StringBuffer - StringBuilder

核心区别对比

|-----------|----------------|----------------------------------|---------------|
| 特性 | String | StringBuffer | StringBuilder |
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全性 | 线程安全(天然安全) | 线程安全 (方法加了 synchronized 锁) | 线程不安全 |
| 执行性能 | 最低(频繁创建新对象) | 中等 | 最高 |

底层实现
  • String :不可变字符数组(char[]),每次修改都会生成新对象。

  • StringBuffer / StringBuilder :都继承自 AbstractStringBuilder,底层是可变字符数组 。它们在原有的数组上进行修改,如果空间不够,会自动进行扩容

如何选择
  1. 少量字符串操作 :直接用 String 即可,代码简洁易读。

  2. 单线程、大量拼接操作 :首选 StringBuilder。它是现代 Java 开发中最常用的工具,性能最强。

  3. 多线程、共享变量修改 :选择 StringBuffer。虽然性能稍低,但它通过内部加锁确保了并发情况下的数据正确性(虽然现在更推荐用 String 或手动加锁,但在遗留代码中常见)

相关推荐
2303_821287382 小时前
CSS中如何实现绝对定位元素的等比缩放_利用宽高百分比
jvm·数据库·python
JAVA面经实录9172 小时前
Java核心底层原理全集(终版无遗漏·生产级PDF)
java·开发语言·学习
java修仙传2 小时前
实习日志:完成算法调用总接口并修复联调问题
java·开发语言·数据库
铅笔小新z2 小时前
【Linux】进程间通信(IPC)
java·linux·运维
2303_821287382 小时前
如何用 Object.defineProperty 为现有对象添加拦截器
jvm·数据库·python
Rabitebla2 小时前
深入理解 C++ STL:stack 和 queue 的底层原理与实现
c语言·开发语言·数据结构·c++·算法
weixin_459753942 小时前
PHP源码运行需要独立显卡吗_显卡对PHP执行有无影响【解答】
jvm·数据库·python
极客先躯2 小时前
高级java每日一道面试题-2025年12月11日-实战篇[Docker]-如何配置 Docker 的资源限制(CPU、内存、磁盘)?
java·docker·如何配置docker的资源限制·资源限制的底层支柱·linux cgroups·cpu 限制·从逻辑到策略
CLX05052 小时前
如何自动同步SQL异构表数据_利用触发器实现实时数据复制
jvm·数据库·python