JVM相关知识(内存管理篇)

146 阅读10分钟

JVM 内存分哪几个区?

+---------------------+
|     Method Area     |  (包含运行时常量池)
+---------------------+
|         Heap        |  (年轻代 + 老年代)
+---------------------+
|     Direct Memory   |  (堆外内存)
+---------------------+
|     JVM Stack       |  (每线程独有)
+---------------------+
| Native Method Stack |  (每线程独有)
+---------------------+
|  Program Counter    |  (每线程独有)
+---------------------+

线程私有:程序计数器,本地方法栈、java虚拟机栈

共享部分:堆、方法区(含运行时常量池)、直接内存(避免了从堆到本地内存的拷贝)

每个区的作用是什么?

名称作用
程序计数器记录当前线程所执行的字节码的行号、线程切换后恢复到正确的执行位置
java线程栈存储方法执行的相关信息, 包括:局部变量表(方法中的局部变量、对象引用),方法返回地址、操作数栈等
本地方法栈为 Native 方法(非 Java 方法,如通过 JNI 调用的 C 代码)服务;存储本地方法调用的相关信息
堆是线程共享的区域,存储所有线程共享的对象实例和数组。
元空间线程共享,存储类的元信息、静态变量、常量池、方法字节码。(java 8前永久代,之后是元空间),包括运行时常量池
直接内存由 NIO(Java New IO)引入,用于直接分配堆外内存。避免了从堆到本地内存的拷贝

一个对象从创建到销毁都是怎么在这些部分里存活和转移的?

  • 对象创建:通过 new 分配在堆内存的 Eden 区,并初始化。

  • 引用存储:对象的引用地址可能存储在栈帧的局部变量表或类静态变量中。

  • GC 回收和转移

    • 在 Eden 区存活的对象进入 Survivor 区。
    • 在 Survivor 区经历多次 GC 后晋升到老年代。
  • 对象销毁:当对象无法通过 GC Root 引用时,经过 GC 回收机制,最终从内存中销毁。

  • 元信息:类元数据存储在元空间中,直至类被卸载或 JVM 终止。

解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法?

对比总结

内存区特点存储内容线程共享性
栈 (Stack)线程私有,自动分配和释放,存储生命周期较短的数据。局部变量、方法调用信息、返回地址等。线程私有
堆 (Heap)线程共享,由 GC 管理,存储生命周期较长的数据。对象实例、数组、类字段(包括静态字段)。线程共享
方法区 (Method Area)线程共享,存储类级信息、静态字段、常量池等,垃圾回收较少。类元信息、常量池、静态变量。线程共享

内存分配与生命周期示例

public class Demo {
    private static String staticField = "Method Area"; // 静态变量存储在方法区
    private String instanceField = "Heap"; // 实例变量存储在堆

    public void example() {
        int localVariable = 10; // 局部变量存储在栈
        String localString = "Stack"; // 局部变量引用存储在栈,字符串常量在堆
        Demo obj = new Demo(); // obj 的引用存储在栈,对象实例存储在堆
    }
}

JVM 中哪个参数是用来控制线程的栈堆大小?

参数: -Xss

示例:java -Xss1m MyApplication

查看默认栈大小: java -XX:+PrintFlagsFinal -version | grep ThreadStackSize

简述内存分配与回收策略简述重排序,内存屏障,happen-before,主内存,工作内存?

1. 内存分配与回收策略

1.1 内存分配策略

  • 对象优先分配到年轻代的 Eden 区:新创建的对象通常会直接分配到堆的年轻代的 Eden 区。
  • 大对象直接进入老年代:如超大数组,避免在年轻代频繁复制而耗费性能。
  • 长期存活的对象进入老年代:对象在年轻代经历多次 Minor GC 后(如 MaxTenuringThreshold 设置的次数),会被晋升到老年代。
  • 空间分配担保机制:当年轻代内存不足时,会将部分对象直接分配到老年代。

1.2 垃圾回收策略

  • 年轻代(Minor GC) :发生在年轻代,回收短生命周期的对象,使用 复制算法
  • 老年代(Major GC/Full GC) :发生在老年代,回收长生命周期的对象,通常使用 标记-清除算法标记-压缩算法
  • 分代回收策略:根据对象存活时间的不同,将堆划分为年轻代和老年代,分别采用不同的回收算法。

2. 重排序

  • 定义:编译器和处理器为了提高性能,对指令的执行顺序进行重新排列,但需要保证最终执行结果符合程序的语义。

  • 类型

    1. 编译器优化重排序:编译器改变代码的执行顺序以提高效率。
    2. 指令级并行重排序:处理器为了充分利用指令流水线,对指令执行顺序进行重排。
    3. 内存系统重排序:由于 CPU 缓存与主存交互,可能导致内存读取顺序与程序顺序不同。
  • 影响:可能导致多线程程序中出现不可预测的行为。

3. 内存屏障

  • 定义:一种 CPU 指令,用于限制指令的重排序行为,确保内存操作的顺序。

  • 常见内存屏障

    • LoadLoad 屏障:保证加载操作在屏障前的加载完成后,才能进行屏障后的加载操作。
    • StoreStore 屏障:保证存储操作在屏障前的存储完成后,才能进行屏障后的存储操作。
    • LoadStore 屏障:保证加载操作在屏障前的加载完成后,才能进行屏障后的存储操作。
    • StoreLoad 屏障:保证存储操作在屏障前的存储完成后,才能进行屏障后的加载操作。

4. happen-before

  • 定义:Java 内存模型(JMM)中定义的一种偏序关系,确保某些操作的结果对另一个操作可见。

  • 常见规则

    1. 程序顺序规则:在单线程中,前面的操作 happen-before 后面的操作。
    2. 锁定规则:锁的解锁操作 happen-before 之后对同一个锁的加锁操作。
    3. volatile 变量规则:对一个 volatile 变量的写操作 happen-before 之后对该变量的读操作。
    4. 传递性:如果 A happen-before B,且 B happen-before C,那么 A happen-before C。

5. 主内存与工作内存

  • 主内存:Java 内存模型中的共享内存区域,所有线程共享。存储对象实例、变量等数据。

  • 工作内存:每个线程独立的私有内存区域,线程从主内存中读取变量到工作内存中,并将操作结果从工作内存写回主内存。

  • 交互方式

    • 从主内存读取变量到工作内存:loadread 操作。
    • 从工作内存写回主内存:writestore 操作。

Java 中存在内存泄漏问题吗?

存在。内存泄漏是指程序中已不再使用的对象仍然被引用,导致垃圾回收器无法回收这些对象,从而占用内存资源。

常见的内存泄漏场景

1. 静态变量的引用

静态变量的生命周期与类相同,程序运行期间会一直存在。如果静态变量引用了一个对象,而该对象不再被使用,那么它就无法被回收。

示例:

public class MemoryLeakExample {
    private static List<Object> staticList = new ArrayList<>();
    
    public void addToStaticList(Object obj) {
        staticList.add(obj); // 对象会一直被静态变量引用
    }
}

2. 集合类中的未清理对象

在使用集合(如 HashMapList 等)时,如果对象被添加到集合中却没有及时移除,即使这些对象不再被使用,也会导致内存泄漏。

示例:

Map<String, Object> cache = new HashMap<>();
cache.put("key", new Object()); // 对象一直存储在 Map 中,无法被回收

3. 监听器或回调未移除

注册的事件监听器或回调如果没有正确移除,可能导致它们引用的对象无法被回收。

示例:

Button button = new Button();
button.addActionListener(event -> System.out.println("Clicked!")); // 没有手动移除监听器

4. 内部类或匿名类的引用

非静态内部类或匿名类会隐式持有外部类的引用,这可能导致外部类对象无法被回收。

示例:

public class OuterClass {
    class InnerClass {
        void doSomething() {
            System.out.println("Inner class");
        }
    }
}

5. ThreadLocal 的误用

如果不正确清理 ThreadLocal 中的值,可能会导致线程无法释放对对象的引用,特别是在线程池中复用线程时。

示例:

ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(new Object()); // 如果不调用 remove,可能导致内存泄漏

ThreadLocal 的内存泄漏主要是因为:

  1. ThreadLocal 的键是弱引用,但值是强引用。
  2. 如果没有手动清理 ThreadLocal,其值会保留在 ThreadLocalMap 中,导致无法回收。

6. 自定义类加载器

使用自定义类加载器加载的类可能会因为类加载器本身未被卸载而导致类及其静态字段占用内存。

7. 循环引用问题

虽然 Java 的垃圾回收器能处理循环引用,但如果循环引用对象的生命周期被延长且其中一些对象无用,也可能导致内存泄漏。

请举例说明简述 Java 中软引用(SoftReferenc)、弱引用(WeakReference)和虚引用?

对比总结

引用类型是否影响垃圾回收回收时机使用场景
强引用永远不会被回收(除非主动断开)普通的对象引用
软引用内存不足时,内存足够不会回收缓存(如图片缓存)
弱引用下次 GC 时一定被回收弱引用对象管理
虚引用对象回收后,通过 ReferenceQueue 获取- 它的主要作用是跟踪对象的生命周期(一般用于跟踪直接内存的释放),并在对象被垃圾回收时执行一些清理操作。

内存映射缓存区是什么?

内存映射缓存区是一种将文件或其他外部数据直接映射到进程的内存地址空间的技术。通过这种方式,可以将磁盘上的文件内容视为内存中的数组,程序可以像访问内存一样高效地操作文件内容。

在 Java 中,内存映射缓存区主要通过 NIO(New Input/Output)中的 FileChannel 类和 MappedByteBuffer 实现。

工作原理

  1. 文件映射到内存

    • 将文件的某个部分映射到内存,通过 FileChannel.map() 方法实现。
    • 映射的区域直接存储在操作系统的虚拟内存中。
  2. 直接访问文件内容

    • 映射完成后,程序可以通过 MappedByteBuffer 对象直接读取或写入文件数据,而无需多次磁盘 I/O。
    • 操作系统会根据需要加载对应的文件部分到内存中(按需分页),并负责将修改写回磁盘(脏页写回)。
  3. 性能优化

    • 避免了传统文件 I/O 中数据从用户态到内核态的多次拷贝。
    • 提升了大文件操作的效率。

特点

  • 文件数据直接映射到进程内存,无需显式加载到堆中。
  • 操作系统按需将文件内容加载到内存(页级加载)。
  • 修改的文件内容自动回写到磁盘(视映射模式而定)。

应用场景

  • 超大文件的读取和写入(无需一次性加载到内存)。
  • 数据库实现(如 LevelDB)。
  • 多进程共享数据。

什么是零拷贝?

定义
零拷贝是一种高效的数据传输技术,通过减少内核态和用户态之间的数据拷贝次数,优化 I/O 性能。它在文件传输或网络传输中尤为重要。

常见实现方式

  1. 基于内存映射

    • 文件直接映射到内存中,数据在文件和网络接口之间传输时无需显式拷贝。
    • Java 中通过 FileChannel.transferTo()FileChannel.transferFrom() 实现。
  2. 基于 sendfile 系统调用

    • 文件内容直接从磁盘拷贝到网络缓冲区,跳过用户态操作。

特点

  • 优点

    1. 减少数据拷贝次数,提高数据传输效率。
    2. 减少 CPU 和内存开销。
  • 缺点

    1. 依赖底层硬件支持,跨平台实现可能有所不同。
    2. 可能对小文件或低速设备效果不明显。

应用场景

  • 网络文件传输(如 HTTP 文件下载)。
  • 数据库系统的日志传输。
  • 分布式存储系统(如 Kafka 的数据传输)。