【Java】深入理解Java虚拟机JVM内存结构与垃圾回收机制全解析
JVM内存结构概述
Java虚拟机(JVM)的内存结构是其核心组成部分,它定义了程序执行过程中数据的存储区域和访问方式。JVM内存主要分为线程共享区域和线程私有区域。线程共享区域包括堆(Heap)和方法区(Method Area),它们在JVM启动时创建,负责存储对象实例、类信息、常量等数据。线程私有区域则包括程序计数器(Program Counter Register)、Java虚拟机栈(Java Virtual Machine Stack)和本地方法栈(Native Method Stack),每个线程都拥有独立的这些区域,用于记录线程的执行状态和方法调用信息。理解这些区域的功能和生命周期是深入掌握JVM内存管理的基础。
程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有自己独立的程序计数器,线程之间互不影响。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是本地(Native)方法,则计数器值为空(Undefined)。此区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
Java虚拟机栈也是线程私有的,其生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行时,JVM都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量表存放了编译期可知的各种基本数据类型和对象引用。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当扩展时无法申请到足够的内存则会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。《Java虚拟机规范》并未强制规定本地方法栈的具体实现方式,因此不同的虚拟机可以自由实现它。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆
Java堆(Java Heap)是JVM内存管理中最大的一块,被所有线程共享,在JVM启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都应当在堆上分配(随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换等优化技术将导致一些微妙的变化)。Java堆是垃圾收集器管理的内存区域,因此也被称作“GC堆”。从内存回收的角度看,由于现代垃圾收集器大部分都基于分代收集理论设计,因此Java堆中经常会出现新生代、老年代等划分。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,JVM将会抛出OutOfMemoryError异常。
方法区
方法区(Method Area)也是各个线程共享的内存区域,它用于存储已被JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中将其描述为堆的一个逻辑部分,但它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。在JDK 8及之后,使用元空间(Metaspace)替代了永久代(Permanent Generation)来实现方法区,元空间使用本地内存,从而避免了永久代常见的性能问题和内存溢出错误。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中(如String类的intern()方法)。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
垃圾回收机制
Java的垃圾回收(Garbage Collection, GC)机制是JVM自动内存管理的关键,它负责自动回收不再被使用的对象所占用的内存,从而防止内存泄漏。垃圾回收器主要关注的是Java堆内存的回收。判断对象是否存活的算法主要有引用计数法和可达性分析算法。Java使用可达性分析算法,通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的,可以被回收。
分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计。该理论建立在两个分代假说之上:弱分代假说(绝大多数对象都是朝生夕灭的)和强分代假说(熬过越多次垃圾收集过程的对象就越难以消亡)。根据这两个假说,JVM将Java堆划分为不同的区域(新生代和老年代),然后根据各个区域的特点使用不同的垃圾回收算法。新生代中对象存活率低,适合使用复制算法;而老年代中对象存活率高,没有额外的空间进行分配担保,通常使用标记-清除或标记-整理算法。
垃圾收集算法
常见的垃圾收集算法包括标记-清除算法、标记-复制算法和标记-整理算法。标记-清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。它的主要不足是执行效率不稳定且会产生内存碎片。标记-复制算法将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法实现简单,运行高效,但代价是可用内存缩小为了原来的一半。标记-整理算法的标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。这种算法避免了内存碎片,但增加了移动对象的开销。
常见的垃圾收集器
JVM提供了多种垃圾收集器,每种都有其特定的用途和适用场景。Serial收集器是一个单线程工作的收集器,它进行垃圾收集时,必须暂停所有其他工作线程,适用于客户端模式下的虚拟机。ParNew收集器本质上是Serial收集器的多线程并行版本。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它基于标记-清除算法实现,允许垃圾收集线程与用户线程并发工作。G1(Garbage-First)收集器是面向服务端应用的垃圾收集器,它采用基于Region的内存布局,能够实现可预测的停顿时间模型。ZGC和Shenandoah则是新一代的低延迟垃圾收集器,旨在实现大堆内存下的极低停顿时间。
内存分配与回收策略
JVM中对象的内存分配,主要是在Java堆上进行的,但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配。对象优先在新生代的Eden区分配,如果Eden区没有足够空间,JVM将发起一次Minor GC。大对象(需要大量连续内存空间的Java对象,如长字符串或大型数组)会直接进入老年代,以避免在Eden区及两个Survivor区之间来回复制。长期存活的对象(经历一定次数的Minor GC后)也将晋升到老年代。JVM还提供了动态对象年龄判定和空间分配担保等策略来优化内存分配和回收过程。
更多推荐
所有评论(0)