String s = new String("abc")执行过程中分别对应哪些内存区域?
1. 元空间 (Metaspace)
在代码执行前,JVM 需要加载 String 类和包含这行代码的类。
- 动作 :类加载器将类的结构信息(方法、字段定义)、运行时常量池存入元空间。
- 存储内容 :
String类的模板信息,以及字节码指令。
2. 堆:字符串常量池 (String Constant Pool)
执行到 "abc" 这个字面量时:
- 动作 :JVM 会先检查字符串常量池中是否存在内容为
"abc"的对象。 - 存储内容 :如果池中没有,则在堆的字符串常量池区域 创建一个
"abc"对象实例。如果已经存在,则直接复用。
3. 堆:普通对象区域 (Java Heap)
执行到 new String(...) 时:
- 动作 :无论常量池中是否已有
"abc",new关键字都会强制在堆的普通对象区域(非池化区域)开辟一块新内存。 - 存储内容 :创建一个全新的
String对象实例。这个对象的内部value属性会指向刚才常量池中"abc"对象的底层数组。
4. 虚拟机栈 (JVM Stack)
执行到 String s 和赋值操作 = 时:
- 动作 :在当前线程方法的栈帧 中,为局部变量
s分配空间。 - 存储内容 :将堆中新创建的
String实例的内存地址 存入局部变量表的s变量中。
5. 程序计数器 (Program Counter)
在整个执行过程中:
- 动作 :程序计数器不断更新,指向
ldc(从常量池加载)、new(分配内存)、invokespecial(调用构造函数)、astore(存入局部变量)等字节码指令的偏移量。
方法区/元空间 中方法执行过程
1.找到元空间中该方法的定义
2.根据定义在虚拟机栈中创建栈帧
3.程序计数器中创建对应计数器
4.线程执行
5.执行完退出,清理该方法栈帧,恢复计数器为调用前位置
四种引用类型
在 Java 中,引用类型的设计核心在于控制垃圾回收器(GC)对对象内存的回收时机 。通过 java.lang.ref 包下的类,程序可以根据内存状态干预对象的生命周期。
1. 强引用 (Strong Reference)
这是 Java 程序中最普遍的引用方式,即通过 new 关键字创建并赋值给变量。
- 实现方式 :
Object obj = new Object(); - 回收算法逻辑 :只要 GC Roots 可达性分析链中存在强引用指向该对象,GC 即使触发也不会回收该对象。
- 内存影响 :当堆空间不足以支撑新的强引用对象分配时,JVM 抛出
java.lang.OutOfMemoryError。 - 释放方式 :将变量显式赋值为
null,使其失去与 GC Roots 的连接。
2. 软引用 (Soft Reference)
软引用用于描述还有用但并非必须的对象。
- 实现类 :
java.lang.ref.SoftReference - 回收算法逻辑 :
- 当内存充足时,GC 不会回收软引用对象。
- 当系统内存不足(即将发生
OOM)时,JVM 会在抛出异常前将这些对象列入二次回收范围并执行清理。
- 参数控制 :可通过
-XX:SoftRefLRUPolicyMSPerMB控制软引用在内存中的存活时长。 - 应用场景 :实现内存敏感的高速缓存。
3. 弱引用 (Weak Reference)
弱引用的强度比软引用更低,它不影响对象的生存周期。
- 实现类 :
java.lang.ref.WeakReference - 回收算法逻辑 :在垃圾收集器工作时,无论当前内存是否足够,只要发现该对象仅被弱引用指向,就会立即回收其内存。
- 存活时间:仅能生存到下一次垃圾收集发生之前。
- 应用场景 :防止内存泄漏 。典型实现包括
ThreadLocalMap的 Key 以及WeakHashMap。
4. 虚引用 (Phantom Reference)
虚引用是最弱的引用,它不对对象的生命周期产生任何实质性影响。
- 实现类 :
java.lang.ref.PhantomReference - 核心特性 :
- 无法通过虚引用获取对象实例(调用
get()永远返回null)。 - 必须 关联一个引用队列(
ReferenceQueue)。
- 无法通过虚引用获取对象实例(调用
- 回收算法逻辑:对象被回收时,JVM 会将该虚引用加入关联的队列中。
- 应用场景 :监控对象的回收状态。主要用于管理直接内存(Direct Memory) ,在
Cleaner机制中用于释放堆外资源。
内存泄漏常见情况
1. 静态集合类(Static Collections)
静态变量的生命周期与类加载器一致(通常随应用启动而生,随应用停止而死)。
-
原因 :如果使用
static修饰List、Map等集合,并不断向其中添加对象,这些对象会因为被静态变量强引用而永远无法被 GC 回收。 -
示例 :
javapublic class BadCache { private static final List<Object> CACHE = new ArrayList<>(); public void add(Object obj) { CACHE.add(obj); // 只要类不卸载,obj 永远在堆里 } }
2. ThreadLocal 内存泄漏
这是面试和生产环境中最隐蔽的泄漏点,尤其是在使用线程池时。
- 原因 :
ThreadLocalMap的 Key 是弱引用 ,但 Value 是强引用 。当ThreadLocal变量被回收后,Key 变为null,但 Value 依然被当前线程(Thread)持有。如果线程不结束(线程池复用线程),这些 Value 就会一直堆积。 - 对策 :必须在使用完后显式调用
remove()。
3. 未关闭的资源(Resources Not Closed)
数据库连接、网络套接字(Socket)、文件流等资源。
- 原因 :这些资源通常涉及操作系统的直接内存 或句柄。如果不手动调用
close(),虽然 Java 包装对象可能被回收,但底层物理连接可能一直占用系统资源,直到发生Finalizer调用(这非常不可靠)。 - 对策 :使用
try-with-resources语法。
4. 内部类持有外部类引用
- 非静态内部类 (Non-static Inner Class)和匿名内部类(Anonymous Inner Class)都会隐式地持有外部类实例的强引用。
- 原因:如果内部类的生命周期长于外部类(例如内部类被交给了一个长周期的线程执行),那么外部类即使不再被使用,也无法被回收。
- 对策 :尽量使用静态内部类。
5. 错误的 hashCode() 和 equals() 实现
-
原因 :在使用
HashMap或HashSet时,如果作为 Key 的对象没有正确重写hashCode()或equals(),或者在存入后修改了参与计算 Hash 值的字段。 -
后果 :你会发现无法通过
remove()删除该对象,导致对象在 Map 中不断累积。javaMap<Point, String> map = new HashMap<>(); Point p = new Point(1, 1); map.put(p, "data"); p.setX(2); // 修改了 Hash 相关字段 map.remove(p); // 无法删除,因为 Hash 变了,找不到原桶位置
6. 改变生命周期的缓存(Caching)
正如你之前看到的例子,如果将对象放入缓存后没有过期策略(TTL)或清理机制。
- 原因:缓存往往是全局共享的,如果只进不出,最终会耗尽堆内存。
- 对策 :使用
WeakHashMap、软引用(SoftReference)或者成熟的缓存库(如 Caffeine、Guava Cache)。
7. 监听器与回调(Listeners and Callbacks)
- 原因:在 GUI 编程或事件驱动模型中,如果你在长生命周期的组件(如单例)中注册了短生命周期对象的监听器,但销毁时忘记注销。
- 后果:长生命周期组件会一直持有短生命周期对象的引用。