一、 类加载机制
类加载器
启动类加载器 (Bootstrap ClassLoader) :C++ 实现,负责加载 Java 核心库(如
java.lang.*,java.util.*,java.io.*),它是大 Boss,最顶层。扩展类加载器 (Extension ClassLoader):负责加载 Java 的扩展库,官方提供的"插件包。
应用程序类加载器 (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. 不可变性
javapublic 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,底层是可变字符数组 。它们在原有的数组上进行修改,如果空间不够,会自动进行扩容如何选择
少量字符串操作 :直接用
String即可,代码简洁易读。单线程、大量拼接操作 :首选
StringBuilder。它是现代 Java 开发中最常用的工具,性能最强。多线程、共享变量修改 :选择
StringBuffer。虽然性能稍低,但它通过内部加锁确保了并发情况下的数据正确性(虽然现在更推荐用String或手动加锁,但在遗留代码中常见)