学习:Java中的内存管理

在 Java 中,对对象进行分配和取消分配的过程,称为内存管理。Java 通过垃圾收集器 (Garbage Collector, GC) 实现了自动内存管理,这意味着开发者无需显式地释放对象所占用的内存。Java 内存管理分为两个主要部分:

  • JVM(Java 虚拟机) 内存结构
  • 垃圾回收器的工作

一、JVM 内存结构

Java 虚拟机 (JVM) 是 Java 应用程序的运行环境,其内存结构是理解 Java 内存管理的基础。JVM 在堆内存中创建了各种运行时数据区域,这些区域在程序执行期间使用。当 JVM 退出时,内存区域会被销毁;而当线程退出时,线程数据区域也会被销毁。

1.1 方法区 (Method Area)

方法区是堆内存的一部分,所有线程共享。它在 JVM 启动时创建,它用于存储类结构、超类名称、接口名称和构造函数。JVM 在方法区中存储以下类型的信息:

  •   类型的完全限定名称(例如:String)
  •   类型的修饰符(如 public、private)
  •   类型的直接超类名称
  •   超级接口的完全限定名称的结构化列表

方法区主要存储已加载类的结构信息,而不是对象实例。因此,它在 JVM 启动时创建,并在 JVM 关闭时销毁。并且方法区涉及到类的加载、链接、初始化等过程,在 JVM 的运行过程中扮演了至关重要的角色。

1.2 堆区 (Heap Area)

堆是 JVM 中最大的一块内存区域,用于存储所有的对象实例。堆内存在 JVM 启动时创建,所有线程共享。通过 new 关键字创建的对象实例存储在堆中,而对象的引用则存储在栈中。堆可以是固定大小,也可以是动态大小。每个 JVM 进程只存在一个堆。当堆变满时,垃圾收集器将进行内存回收。

StringBuilder sb = new StringBuilder(); // 创建对象,分配在堆中

在上面的代码中,StringBuilder 对象被分配到堆中,而引用 sb 被分配到栈中。 

堆区可以分为以下几个部分:

  • 年轻代 (Young Generation):新创建的对象首先分配在年轻代。
  • 幸存者空间 (Survivor Spaces)
  • 老年代 (Old Generation):当对象在年轻代存活足够长时间且没有被垃圾回收时,它们会被移动到老年代。
  • 永久代 (Permanent Generation):存储类的元数据信息,JDK 8 之后被元空间 (Metaspace) 取代。
  • 代码缓存 (Code Cache)

1.3 栈区 (Stack Area)

栈区用于存储方法调用过程中创建的局部变量、方法参数和部分结果。每个线程在 JVM 中都有自己的栈区,因此栈区是线程私有的。栈中的变量具有一定的作用域,当方法调用完成时,栈帧随之销毁,局部变量也不再有效。

  public void method() {
      int a = 10; // 局部变量,分配在栈中
  }

栈区的特点是存储在其中的数据生命周期短暂,通常只在方法执行期间存在。这一特点使得栈内存分配非常快,但也意味着栈内存空间相对有限,通常用于存储基本数据类型和对象的引用。

栈帧:栈帧是包含线程数据的数据结构。线程数据表示线程在当前方法中的状态。

  • 它用于存储部分结果和数据。它还执行动态链接、方法返回的值和调度异常。
  • 当方法调用时,将创建一个新帧。当方法的调用完成时,它会销毁帧。
  • 每个帧都包含自己的局部变量数组 (LVA)、操作数堆栈 (OS) 和帧数据 (FD)。
  • LVA、OS 和 FD 的大小在编译时确定。
  • 在给定的控制线程中的任何一点上,只有一个帧(用于执行方法的帧)处于活动状态。这个帧称为当前帧,其方法称为当前方法。方法的类称为当前类。
  • 如果当前方法的方法调用另一个方法,或者该方法完成,则帧将停止当前方法。
  • 线程创建的帧是该线程的本地帧,不能被任何其他线程引用。

1.4 程序计数寄存器 (PC Register)

程序计数寄存器是一个非常小的内存空间,用于存储返回地址或原生指针(普通指针),还包含当前线程正在执行的 JVM 指令的地址。每个线程都有自己的程序计数寄存器,因此它也是线程私有的。程序计数器的主要作用是跟踪程序的执行顺序,确保线程在执行过程中能正确地恢复到之前的执行点。

1.5 本地方法栈 (Native Method Stack)

本地方法栈用于支持本地方法的执行。本地方法是用 Java 以外的语言(如 C、C++)编写的代码,它们通常通过 Java 本地接口 (JNI) 调用。每当调用一个本地方法时,JVM 会在本地方法栈中创建一个新的栈帧。

二、Java 引用类型

在 Java 中,引用类型决定了对象在内存中的生命周期。Java 提供了四种引用类型:强引用、弱引用、软引用和虚引用,它们对垃圾回收的影响各不相同。

2.1 强引用 (Strong Reference)

强引用是 Java 中最常见的引用类型。任何被强引用关联的对象在垃圾回收时都不会被回收,即使内存不足,JVM 也不会主动回收这些对象。

StringBuilder sb = new StringBuilder(); // 强引用

2.2 弱引用 (Weak Reference)

它无法在下一个垃圾回收过程之后继续存在。当 JVM 进行垃圾回收时,如果一个对象只被弱引用关联,那么该对象会被立即回收。使用场景包括缓存等。

WeakReference<StringBuilder> weakRef = new WeakReference<>(new StringBuilder());

2.3 软引用 (Soft Reference)

软引用在内存不足时会被回收。软引用非常适合用来实现内存敏感的缓存。

SoftReference<StringBuilder> softRef = new SoftReference<>(new StringBuilder());

2.4 虚引用 (Phantom Reference)

虚引用是最弱的引用类型。一个对象是否具有虚引用,不会影响其生命周期。虚引用主要用于跟踪对象被垃圾回收的状态,并在垃圾回收前执行一些清理操作。

PhantomReference<StringBuilder> phantomRef = new PhantomReference<>(new StringBuilder());

三、Java 垃圾回收器

Java 的垃圾回收器(Garbage Collector,GC)是 JVM 内存管理的核心组件之一。它的主要职责是自动管理内存,确保程序不再使用的对象所占用的内存能够及时释放,从而避免内存泄漏和内存不足问题。

 1、Java 垃圾回收器概述

当一个 Java 程序执行时,它会以不同的方式使用内存。堆(Heap)是 JVM 中的一部分内存区域,专门用于存放对象实例。这也是垃圾回收器主要工作的内存区域。所有的垃圾回收操作都集中在堆上,确保堆内的可用空间最大化。

垃圾回收器的主要任务是查找并删除不再使用的对象,即那些无法被任何活动线程访问的对象。通过自动化这一过程,Java 使得开发者无需手动管理内存释放,极大地减少了内存管理的复杂性和错误率。

2、对象分配

当一个对象在 Java 程序中被创建时,JVM 会根据对象的大小来决定其内存分配的位置。JVM 通常将对象分为小对象和大对象,并根据不同的分配策略将它们存储在适当的内存区域。

2.1 小对象的分配

小对象通常被分配到线程本地区域 (Thread Local Area, TLA)。TLA 是堆中的一块空闲内存区域,专门为每个线程独立保留,用于快速分配小型对象。由于 TLA 是线程私有的,分配小对象时不需要进行线程同步操作,这使得对象分配非常高效。

int[] smallArray = new int[50]; // 小对象,可能分配到 TLA 中

当 TLA 的空间不足时,线程会请求 JVM 为其分配新的 TLA。由于 TLA 的分配是高效的,因此这种策略能够显著提高小对象分配的性能。

2.2 大对象的分配

大对象(通常大于 128 KB,根据 JVM 配置可能有所不同)则直接分配到堆中,而不经过 TLA。如果大对象分配在年轻代空间(Young Generation),并且该空间不足,则可能会直接分配到老年代空间(Old Generation)。由于大对象的分配可能需要更多的线程同步,因此其分配效率相对较低。

int[] largeArray = new int[100000]; // 大对象,可能直接分配到堆中

通过这种区分大小对象的分配策略,JVM 能够更有效地管理堆内存,减少垃圾回收的频率和开销。

3、垃圾回收的触发机制

JVM 完全控制垃圾回收的执行时机。虽然我们可以通过代码请求 JVM 执行垃圾回收(例如调用 System.gc() 方法),但 JVM 并不保证会立即执行该请求。JVM 主要基于当前内存使用情况和程序需求来决定何时触发垃圾回收。

何时对象有资格被垃圾回收?
        当一个对象没有任何活动线程可以访问时,它就有资格被垃圾回收。这通常意味着所有指向该对象的引用都已经失效,或者这些引用变量已超出其作用域。

内存不足时的垃圾回收:
        当 JVM 检测到堆内存不足时,它会自动触发垃圾回收,以释放未被使用的内存。如果内存不足以容纳新对象,而垃圾回收后仍然无法腾出足够的空间,程序将抛出 OutOfMemoryError 异常。 

4、垃圾回收的类型

根据不同的应用场景和内存管理策略,JVM 提供了多种垃圾回收器。以下是 Java 中常见的几种垃圾回收器类型:

4.1 串行垃圾回收器 (Serial GC)

串行垃圾回收器使用单线程执行垃圾回收任务。它采用“标记-清除”算法,对年轻代和老年代分别执行垃圾回收。串行 GC 适用于单处理器环境,简单易行,但在多处理器系统中效率较低。

4.2 并行垃圾回收器 (Parallel GC)

并行垃圾回收器也是基于“标记-清除”算法,但不同之处在于它为年轻代的垃圾回收生成多个线程(通常与 CPU 核心数相同),从而加快回收过程。它适合多处理器环境,能够提高垃圾回收效率。

4.3 并行旧垃圾回收器 (Parallel Old GC)

并行旧垃圾回收器是并行 GC 的扩展版本,增加了对老年代的多线程垃圾回收支持。这意味着它在年轻代和老年代都可以并行执行垃圾回收任务,从而提高整体内存管理效率。

4.4 并发标记扫描 (Concurrent Mark Sweep,CMS) 回收器

CMS 收集器专门用于老年代的垃圾回收。与其他垃圾回收器不同,CMS 允许应用线程在垃圾回收的同时继续运行,从而减少程序暂停时间。CMS 使用多个线程执行标记和扫描操作,是一种并发低暂停收集器,非常适合对延迟敏感的应用。

4.5 G1 垃圾回收器 (Garbage-First GC

G1 垃圾收集器是 Java 7 中引入的新型收集器,旨在替代 CMS 收集器。G1 是一个并行、并发的垃圾收集器,不再区分年轻代和老年代,而是将堆划分为多个大小相等的区域(Region)。G1 优先回收垃圾最多的区域,从而提高垃圾回收效率,适合大多数现代 Java 应用。

5、标记-清除算法

Java 的垃圾回收器通常使用“标记-清除”算法来管理堆内存。这种算法包含两个主要阶段:标记阶段和清除阶段。

5.1 标记阶段

在标记阶段,垃圾回收器会遍历所有的对象引用,并标记出那些仍然被引用的对象。这些对象将被保留,而其他没有标记的对象则会被视为垃圾,等待清除。

5.2 清除阶段

在清除阶段,垃圾回收器会回收所有未被标记的对象,从而释放出这些对象所占用的内存空间。清除阶段还会记录堆中的空闲内存区域,以便为后续对象分配提供空间。

6、标记-清除算法的变种

为了提高垃圾回收的效率,Java 的垃圾回收器实现了标记-清除算法的两种变种:并发标记-清除和并行标记-清除。

6.1 并发标记-清除

并发标记-清除允许应用线程在大部分垃圾回收过程中继续执行。它包括以下几个阶段:

初始标记:标识活动对象的根集,在线程暂停时完成。
并发标记:继续标记从根集引用的对象,在线程运行时完成。
预清洗标记:标记在并发标记期间发生变化的对象,在线程运行时完成。
最终标记:标记预清洗后剩余的活动对象,在线程暂停时完成。

6.2 并行标记-清除

并行标记-清除使用系统中所有可用的 CPU 资源,以最快速度执行垃圾回收。与并发标记-清除不同,执行并行标记-清除时,应用线程会暂停。

7、标记-清除算法的优缺点

7.1 优点

效率高:标记-清除算法是一个反复执行的过程,能够有效地管理内存。
简单明了:算法本身相对简单,易于实现和优化。
自动化:无需额外的编程开销,自动处理内存回收。

7.2 缺点

程序暂停:在垃圾回收算法运行时,应用程序通常会暂停,这可能影响程序的实时性。
频繁执行:垃圾回收可能会频繁触发,影响程序的整体性能。

四、结论

Java 垃圾回收器是 JVM 内存管理中至关重要的组成部分。通过自动管理对象生命周期和内存释放,垃圾回收器大大简化了开发者的工作。然而,理解不同类型的垃圾回收器及其工作原理,可以有效避免内存泄漏和性能瓶颈,从而提升应用程序的整体表现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值