首页 > 系统相关 >学习:Java中的内存管理

学习:Java中的内存管理

时间:2024-08-13 18:24:50浏览次数:17  
标签:Java 对象 回收 学习 线程 内存 JVM 垃圾

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

标签:Java,对象,回收,学习,线程,内存,JVM,垃圾
From: https://blog.csdn.net/weixin_75156045/article/details/141139770

相关文章

  • java浅拷贝BeanUtils.copyProperties引发的RPC异常
    背景近期参与了一个攻坚项目,前期因为其他流程原因,测试时间已经耽搁了好几天了,本以为已经解决了卡点,后续流程应该顺顺利利的,没想到人在地铁上,bug从咚咚来~没有任何修改的服务接口,抛出异常:java.lang.ClassCastException:java.util.HashMapcannotbecasttocn.xxx.xxx.xxx.xx......
  • 一个基于 Java 接口参数加密框架,让接口参数加密变得简单、优雅!
    SecurityApiv1.0.1一个基于Java接口参数加密框架,让接口参数加密变得简单、优雅!文章目录一、SecurityApi介绍二、SecurityApi依赖三、使用1.RSA加密(非对称加密)1.1简单示例1.2生成RSA密钥1.3加签名说明「第一个场景」B要给A传递一条加密消息「第二个场景」B......
  • 计算机毕业设计推荐-基于java的地方特色美食分享管理系统
    ......
  • 计算机毕业设计推荐-基于Java的流浪宠物援助平台【源码+文档+PPT】
    精彩专栏推荐订阅:在下方主页......
  • Java解决递归造成的堆栈溢出问题
    在Java中,递归造成的堆栈溢出问题通常是因为递归调用的深度过大,导致调用栈空间不足。解决这类问题的一种常见方法是使用非递归的方式重写算法,即使用迭代替代递归。1.方法一:非递归的方式重写算法(迭代替代递归)下面通过一个典型的递归例子——计算斐波那契数列的第n项,来演示如何用迭......
  • SpringBoot项目创建报错——解决Intellij idea Error:java: 无效的源发行版: 16
    错误信息java:错误:无效的源发行版:16分析我的JDK版本为1.8,创建SpringBoot项目时只有jdk21、22,SpringBoot版本也只有3.x.x,而jdk8仅兼容2.x.x,由此造成了不兼容解决先把所有jdk版本统统改成一样的先打开ProjectStructure再打开Setting还需要改下pom.xml文件的jdk版......
  • 内存泄漏的概念及其产生原因和规避手段
    Memoryleak内存泄漏是指:程序在动态分配内存后,由于某种原因未能释放或无法释放这些内存,导致系统内存的浪费。产生内存泄露的原因上述定义表示了一种现象,没有定义原因。要避免这种现象,就要探究产生现象的原因。内存泄漏是在程序运行过程中产生的,程序运行依赖的是我们的指令,即程......
  • Java基础继续
    Java基础继续类型转换Java是强类型语言,在运算的时候,需要用到类型转换运算中,不同类型的数据先转换成同一个类型,然后进行运算publicclassDemo04{publicstaticvoidmain(String[]args){inti=128;byteb=(byte)i;//强制类型转换d......
  • JavaScript 中的宏任务与微任务
    JavaScript是一种单线程的编程语言,这意味着在同一时间只能执行一个任务。为了有效地处理并发操作,JavaScript引入了事件循环(EventLoop)机制,其中宏任务(MacroTask)和微任务(MicroTask)在其中扮演着关键角色。1.什么是宏任务和微任务?宏任务(MacroTask)是JavaScript中执行的大......
  • 【Python机器学习】树回归——使用Python的tkinter库创建GUI
    机器学习给我们提供了一些强大的工具,能从未知数据中抽取出有用的信息。因此,能否这些信息以易于人们理解的方式呈现十分重要。如果人们可以直接与算法和数据交互,将可以比较轻松的进行解释。其中一个能够同时支持数据呈现和用户交互的方式就是构建一个图形用户界面(GUI)。利用GUI......