内存管理
Java
内存管理是java
虚拟机(JVM
)在运行时负责分配、使用和释放内存的过程。
Java
内存管理主要包括垃圾回收和内存分配两个方面。
垃圾回收
基本概念
Java GC (垃圾回收)
机制。在java
虚拟机中,存在自动内存管理和垃圾清扫机制。
该机制对JVM
中的内存进行标记,并确定那些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM
中的内存空间,放置出现内存泄露和溢出问题。
JVM
堆
- 新生代(
Young Generation
): 新生代是JVM
堆内存的一部分,用于存储新创建的对象。Eden
区和两个Survivor
区(通常称为From
和To
区),也可以说它被进一步划分为三个区域:Eden
区、SurvivorFrom
区(通常称为S0
区)和SurvivorTo
区(通常称为S1
区)。新创建的对象首先被分配到Eden
区,经过一次Minor GC
(年轻代垃圾回收),仍然存活的对象会被移动到Survivor
区。在Survivor
区中,存活对象在多次垃圾回收后可能会晋升到老年代。 - 老年代(
Old Generation
): 老年代用于存储长时间存活的对象和从新生代晋升过来的对象。老年代中的对象会在Major GC
(老年代垃圾回收)时被清理。 - 永久代(
Permanent Generation
): 永久代在Java 8及以前的版本中用于存储类的元数据、静态变量、常量等。但由于永久代的管理和调优比较复杂,并且容易导致内存泄漏等问题,从Java 8开始,永久代被元空间(Metaspace
)取代。元空间不再位于堆内存,而是使用本地内存。这也是为什么在Java 8及以后的版本中,不再提及永久代。 - 元空间(
Metaspace
): 元空间是Java 8及以后版本中取代了永久代的概念。它用于存储类的元数据信息,如类的结构、方法信息等。元空间不受固定内存大小的限制,可以根据应用程序的需要动态地分配和释放内存。
元空间是啥
在JVM
中,元空间(Metaspace
)是Java虚拟机中用于存储类元数据的区域,包括类的名称、方法、字段等信息。当加载大量类或者动态生成类时,元空间的内存可能会被耗尽,导致该错误的发生。解决方法包括增加元空间的大小、减少类的加载或者优化代码等 。
图示:
内存分配
程序计数器
在JVM
中,每个线程都有一个独立的程序计数器,它是一个小的内存区域,用于存储当前线程执行的字节码指令的地址。当一个线程执行方法时,程序计数器会跟踪执行到哪一条指令。由于线程切换是随时可能发生的,程序计数器的值可以保证线程恢复执行时能够继续执行正确的指令。
主要作用如下:
- 线程切换恢复: 当线程切换后再切换回来时,程序计数器可以指导线程从正确的地方继续执行,以保证线程的执行状态得到恢复。
- 分支、循环、异常处理: 程序计数器指示了线程正在执行的指令地址,这对于分支、循环和异常处理非常重要,因为它们依赖于正确的指令地址来进行流程控制。
- 方法调用和返回: 在方法调用时,程序计数器会记录调用后需要返回的地址。这对于方法返回后继续执行非常重要。
- 多线程: 每个线程都有独立的程序计数器,这允许多个线程同时执行不同的指令,从而实现多线程的并发。
需要注意的是,程序计数器不会执行具体的指令,它只是记录指令地址。在Java虚拟机规范中,程序计数器是唯一一个不会出现
OutOfMemoryError
的区域,因为它是线程私有的,且存储的信息较少。
Java
虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack
)是Java虚拟机中的一种线程私有的栈结构,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个线程在创建时都会创建一个独立的Java虚拟机栈,其生命周期与线程相同。
当一个线程执行一个方法时,Java虚拟机会将该方法的局部变量表、操作数栈等信息压入Java虚拟机栈中,当方法执行完毕后,这些信息会被弹出Java虚拟机栈,以便下一个方法可以继续使用。
如果Java虚拟机栈的深度超过了其允许的最大深度(-Xss
参数指定),则会抛出StackOverflowError
异常。
本地方法栈
本地方法栈(Native Method Stack)也是Java虚拟机中的一种线程私有的栈结构,用于存储本地方法(Native Method)的调用信息。
与Java虚拟机栈类似,每个线程在创建时都会创建一个独立的本地方法栈,其生命周期与线程相同。
当一个线程调用一个本地方法时,Java虚拟机会将该方法的调用信息压入本地方法栈中,当本地方法执行完毕后,这些信息会被弹出本地方法栈,以便下一个本地方法可以继续使用。
如果本地方法栈的深度超过了其允许的最大深度(-Xss
参数指定),则会抛出StackOverflowError
异常。
Java
堆
Java堆(Java Heap)是Java虚拟机(JVM
)中的一个内存区域,用于存储对象实例和数组。它是程序运行时动态分配的内存区域,主要用于存放创建的对象。
以下是关于Java堆的一些基本概念:
- 对象存储: Java堆是存储所有类实例和数组对象的地方。当你创建一个新的对象时,JVM会在堆内存中分配足够的空间来存储该对象的实例变量。
- 自动内存管理: Java堆内存的分配和释放是由
JVM
自动管理的,这意味着你不需要手动分配或释放堆内存。JVM
中的垃圾回收机制会自动回收不再使用的对象,以释放占用的内存。 - 堆内存大小: 堆的大小可以通过命令行参数
-Xmx
和-Xms
来进行设置。-Xmx
表示堆的最大大小,-Xms
表示堆的初始大小。堆的大小会影响程序的性能和内存使用情况。 - 分代结构: Java堆通常被划分为不同的代,包括新生代(Young Generation)和老年代(Old Generation)。新创建的对象首先分配到新生代的Eden区,然后根据其生命周期可能晋升到Survivor区和老年代。
- 垃圾回收: 垃圾回收是为了回收不再使用的对象,以释放内存空间。在新生代中,采用了复制算法来进行垃圾回收,而在老年代中,通常采用标记-清除、标记-整理等算法。
- 内存分配策略: 堆内存的分配是有一定策略的,比如针对新对象的分配会在Eden区进行,针对大对象的分配可能会直接进入老年代等。
- 内存管理工具: 对于内存问题的分析和调优,可以使用工具如
VisualVM、MAT(Memory Analyzer Tool)
、JVisualVM
等来监控和分析堆内存的使用情况和对象分布。
方法区
方法区(Method Area
)是Java虚拟机(JVM
)的一部分,用于存储类信息、常量、静态变量、即时编译器生成的代码等。它是JVM
内存的一部分,与堆、栈等区域一起构成了Java程序在内存中的运行时结构。
方法区主要包含以下内容:
- 类信息: 方法区存储了每个加载的类的结构信息,包括类的名称、父类的名称、实现的接口、字段、方法等。这些信息在运行时用于对象的实例化和方法的调用。
- 静态变量: 类的静态变量(
static变量
)也存储在方法区中。这些变量在类加载的过程中被初始化,它们的生命周期与类的生命周期相同。 - 常量池: 常量池是方法区中的一部分,用于存储编译时生成的字面量和符号引用。它包含了类、方法、字段的符号引用以及字符串、数字等字面量。
- 即时编译器生成的代码: 在某些情况下,
JVM
会将字节码编译为本地机器码以提高执行效率。这些本地机器码也存储在方法区中。
tips:
Java
虚拟机栈和本地方法栈的关系
拟机栈和本地方法栈之间的关系在于它们都是用于方法调用和执行的,但它们处理的方法类型不同。
虚拟机栈主要用于Java方法的调用和执行,而本地方法栈主要用于本地方法(Native方法)的调用和执行。
在一些JVM
实现中,虚拟机栈和本地方法栈可能合并在一起,或者使用相同的内存区域。但从概念上来说,它们仍然代表不同类型的方法调用栈。
堆和栈的联系
给堆分配了一个地址,把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它
以下是它们之间的一些联系和区别:
- 数据存储方式:
- 堆:堆用于存储动态创建的对象实例和数组,这些对象的生命周期可能比较长,可以在程序的不同部分访问。堆内存的分配和释放是由JVM自动管理的。
- 栈:栈用于存储方法调用的局部变量、方法参数、返回值等。栈中的数据具有较短的生命周期,它在方法的调用和返回过程中进行入栈和出栈。
- 分配和释放:
- 堆:堆内存的分配是动态的,程序可以在运行时创建对象。对象不再被引用时,会被垃圾回收机制自动回收,释放内存。
- 栈:栈内存的分配和释放是基于方法的调用和返回的。当一个方法被调用时,栈帧会被推入栈,当方法返回时,栈帧会被弹出栈。
- 内存管理:
- 堆:堆内存的分配和释放是自动的,程序员无需手动干预。但要注意,糟糕的内存管理可能导致内存泄漏。
- 栈:栈内存的分配和释放是自动的,不需要程序员手动管理。但由于栈的大小是有限的,过多的方法调用可能导致栈溢出。
- 速度:
- 堆:由于堆内存的分配和释放需要动态管理,所以相对来说较慢。
- 栈:栈内存的分配和释放是固定的,直接入栈和出栈,所以操作速度相对较快。
- 数据大小:
- 堆:堆内存的大小可以通过
JVM
参数进行设置。堆内存通常较大,可以存储大量的对象。 - 栈:栈内存的大小通常较小,受限于操作系统和程序运行环境。
- 堆:堆内存的大小可以通过
垃圾收集器的算法
垃圾收集器是Java虚拟机中的组件,负责自动回收不再使用的对象,并释放它们占用的内存。不同类型的垃圾收集器使用不同的算法来实现这一目标。以下是常见的垃圾收集算法和相关概念:
- 标记-清除(Mark-Sweep)算法: 这是最基本的垃圾收集算法之一。它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾收集器会遍历可达对象,将它们标记为存活。在清除阶段,垃圾收集器会清除未标记的对象,从而释放未被引用的内存。标记-清除算法有可能导致内存碎片的问题。
- 标记-整理(Mark-Compact)算法: 这个算法也有标记和清除的阶段,但在清除阶段不仅会删除未标记的对象,还会将剩余的存活对象紧凑地移到内存的一端,从而消除内存碎片。
- 复制(Copying)算法: 这是在新生代中常用的算法。它将内存分为两个区域:Eden区和两个Survivor区(通常是S0和S1)。新创建的对象会被分配到Eden区,经过一次Minor GC,仍然存活的对象会被复制到一个Survivor区,然后在多次GC后,存活的对象会被晋升到老年代。
- 分代收集(Generational Collection): 这是现代垃圾收集器的基本思想。它将堆内存分为新生代和老年代。新创建的对象首先分配到新生代,因为新生代中的对象生命周期较短。在新生代中使用复制算法,而在老年代中可能使用标记-清除、标记-整理等算法。
- 增量式收集(Incremental Collection): 垃圾收集器将整个垃圾回收过程分成多个小步骤,让垃圾回收和应用程序交替进行。这可以减少垃圾回收造成的停顿时间。
- 并发收集(Concurrent Collection): 并发垃圾收集器允许垃圾回收和应用程序同时进行,从而减少应用程序的停顿时间。它需要处理并发时的一致性问题,可能会引入一些复杂性。
- G1收集器: G1(Garbage-First)收集器是一种现代的垃圾收集器,它基于分代收集思想,并采用了区域化内存管理,可以有效控制停顿时间。
- ZGC、Shenandoah、CMS等: 这些是一些面向低停顿时间的垃圾收集器,它们在设计上都有自己的独特算法,旨在减少应用程序的停顿时间。
不同的垃圾收集器适用于不同的场景和性能需求。在选择垃圾收集器时,需要根据应用程序的特点和需求来进行权衡和选择。
内存管理技术
Java
提供一些内存管理计数来优化内存的使用和性能:
- 对象生命周期管理: 确保对象的生命周期与其实际需求相匹配是内存管理的基本原则之一。如果对象不再被引用,它应该能够被垃圾回收器正确地回收,以释放占用的内存。避免产生不必要的长生命周期对象可以减少内存占用和垃圾回收的频率,从而提高性能。
- 对象引用管理: 在Java中,对象的引用决定了对象是否可以被访问和保持在内存中。了解不同类型的引用(强引用、软引用、弱引用、虚引用)可以帮助您管理对象的生命周期。使用适当类型的引用可以实现内存敏感的资源管理,例如缓存。
- 内存分配策略: Java堆内存的分配是有一定策略的。选择适当的内存分配策略可以减少垃圾回收的频率和成本。一些策略包括对象的复用、使用对象池、避免频繁的大对象分配等。
- 调优参数设置: JVM提供了许多参数,允许您调整垃圾回收行为、堆内存大小等。通过调整这些参数,您可以根据应用程序的需求进行优化。例如,通过设置不同的垃圾收集器、调整新生代和老年代的比例,以及合理设置堆的大小,可以显著影响性能和内存使用情况。
tips:
OutOfMemoryError
问题
OutOfMemoryError
是 Java 程序在运行时可能遇到的一种错误类型,表示内存不足以满足程序的内存需求。这通常发生在程序尝试分配更多内存时,而堆内存已经耗尽。
OutOfMemoryError
可以在不同的内存区域出现,例如:
- Java 堆内存不足: 当程序需要分配对象的内存,但是堆内存中没有足够的空间时,会抛出
OutOfMemoryError
。这可能是由于创建了过多的对象、内存泄漏、内存分配策略不合理等原因导致的。 - 方法区(
Metaspace
)不足: 方法区用于存储类的元数据信息,如果类的元数据过多,或者使用了大量的动态生成的类(比如反射或动态代理),会导致方法区不足,抛出OutOfMemoryError
。 - 栈溢出: 虽然不是
OutOfMemoryError
,但栈溢出(StackOverflowError)也与内存有关。当方法调用的层次太深,导致栈空间不足以容纳更多的方法调用栈帧时,会抛出栈溢出错误。 - 本地内存不足: 如果 Java 程序中使用了
ByteBuffer
等手动管理的本地内存,也可能出现本地内存不足的情况。
处理 OutOfMemoryError
的方式包括:
- 分析内存使用情况: 使用内存分析工具,如 VisualVM、MAT(Memory Analyzer Tool)等,来识别内存泄漏和内存占用高的地方。
- 调整堆大小: 可以通过命令行参数
-Xmx
和-Xms
来调整堆的最大和初始大小,以适应程序的内存需求。 - 优化代码和资源释放: 确保及时释放不再使用的对象和资源,避免造成不必要的内存占用。
- 减少对象创建: 使用对象池等技术来减少对象的创建和销毁,从而降低内存压力。
- 使用合适的数据结构和算法: 选择适合场景的数据结构和算法,可以减少内存占用。
- 避免过度使用反射和动态代理: 减少动态生成类的使用,以减少方法区的负担。
图片来源:https://juejin.cn/post/6874810532730404878/