系列文章目录
第一章 Java核心篇之JVM探秘:内存模型与管理初探
第二章 Java核心篇之JVM探秘:对象创建与内存分配机制
第三章 Java核心篇之JVM探秘:垃圾回收算法与垃圾收集器
第四章 Java核心篇之JVM调优实战:Arthas工具使用及GC日志分析
目录
前言
一、JVM整体结构图
二、JVM内存区域概述
三、JVM内存参数设置
XX:PermSize
-XX:MaxMetaspaceSize和-XX:MetaspaceSize
三、垃圾回收(Garbage Collection)
分代收集理论:
垃圾回收算法:
四、内存溢出与内存泄漏
内存溢出:
内存泄漏:
总结
前言
在Java的世界里,JVM(Java Virtual Machine)扮演着至关重要的角色。它不仅提供了运行Java程序的环境,还负责管理内存,确保程序的高效和安全执行。本文将深入探讨JVM内存模型,理解其内部结构与工作原理,帮助开发者更好地优化代码,避免常见的内存问题。
一、JVM整体结构图
二、JVM内存区域概述
- 程序计数器(Program Counter Register):
每个线程都有一个独立的程序计数器,用于指示当前线程所执行的字节码指令的位置。当线程被中断或恢复时,程序计数器可以帮助JVM找到上一次执行的位置。
-
虚拟机栈(Virtual Machine Stack):
也是线程私有的,用于存储局部变量、操作数栈、动态链接和方法出口等信息。每个方法调用都会创建一个新的栈帧,方法退出后,相应的栈帧也会被销毁。
-
本地方法栈(Native Method Stack):
与虚拟机栈类似,但用于支持本地(非Java)方法的调用。在现代JVM中,它往往与虚拟机栈合并,使用相同的实现方式。
-
Java堆(Heap):
所有线程共享的内存区域,用于存储对象实例和数组。这是垃圾收集的主要区域。Java堆是JVM管理的内存中最大的一块,它的大小可以通过参数进行配置。
-
方法区(Method Area):
也称为“非堆”,用于存储类信息、常量、静态变量、即时编译后的代码等数据。它与Java堆一样,由所有线程共享。
-
直接内存(Direct Memory):
不属于JVM内存的一部分,但是通过java.nio.ByteBuffer.allocateDirect()
等API分配的内存。直接内存的使用需要特别注意,因为不受JVM的常规内存管理机制控制。
-
局部变量表(Local Variable Table):
局部变量表用于存储方法参数和方法内部定义的局部变量。局部变量包括基本类型的变量、对象引用和返回地址等。
每个局部变量占据一个或多个“slot”(槽),一个slot可以存放一个32位数据类型,如int
、float
或对象引用;64位数据类型,如long
和double
,则占用两个slot。
变量的生存周期是从方法被调用开始直到方法结束,此时局部变量表的内容会被销毁。
-
操作数栈(Operand Stack):
操作数栈是一个后进先出(LIFO)的数据结构,用于存放中间运算结果,同时也作为方法调用和返回的参数传递的场所。
当执行任何计算表达式时,操作数栈用来存放运算符的左右操作数,以及存放运算结果。
方法调用时,参数会被压入操作数栈,方法返回时,返回值会被放入操作数栈,然后传递给调用者。
-
动态链接(Dynamic Linking):
动态链接是指将方法调用解析为方法在内存中的直接引用。在JVM中,当一个方法调用另一个方法时,它需要知道目标方法的确切位置,即其在内存中的入口点。
这个过程在运行时完成,允许方法在不同的类加载器之间动态查找和链接。
-
方法出口(Method Exit):
方法出口指的是方法执行完毕后,如何清理现场并返回调用者的过程。这包括恢复上层方法的局部变量表和操作数栈,将返回值(如果有的话)压入调用者的操作数栈中,以及恢复方法调用之前的程序计数器值,以便继续执行调用者的方法。
三、JVM内存参数设置
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加到bin目录下的catalina文件中即可)
java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar xxx.jar
XX:PermSize
在JDK 8之前,类的元数据(包括类信息、常量池、字段信息、方法信息等)是存储在永久代(Permanent Generation)中的。XX:PermSize
参数用于设置永久代的初始大小,而 -XX:MaxPermSize
则用于设置永久代的最大大小。如果永久代的空间不足,JVM 将会抛出 OutOfMemoryError: PermGen space
的错误。
-XX:MaxMetaspaceSize和-XX:MetaspaceSize
从JDK 8开始,永久代被移除,类元数据被移到了一个名为“Metaspace”的本机内存区域中。这意味着类元数据的管理不再受限于堆内存的限制,而是受限于系统可用的物理内存。
-
-XX:MaxMetaspaceSize
:用于设置Metaspace的最大大小。默认情况下,Metaspace可以使用系统的大部分物理内存,但这可能会导致其他应用程序的内存不足。因此,在多应用服务器环境中,通常需要显式地限制Metaspace的大小。 -
-XX:MetaspaceSize
:用于设置Metaspace的起始大小。当Metaspace的使用量超过这个值时,JVM会尝试增加Metaspace的大小,直到达到-XX:MaxMetaspaceSize
指定的上限。
三、垃圾回收(Garbage Collection)
垃圾回收是JVM内存管理的核心机制之一。JVM通过自动检测不再被引用的对象,并回收这些对象占用的内存,从而避免了手动内存管理带来的问题,如内存泄漏和野指针。
分代收集理论:
- Java堆通常分为新生代(Young Generation)和老年代(Old Generation)。新生代又细分为Eden空间和两个Survivor空间(S0和S1)。对象首先在Eden空间创建,经过几次GC后,存活的对象会被移动到Survivor空间,最终可能晋升到老年代。
垃圾回收算法:
- 包括标记-清除(Mark-Sweep)、复制(Copying)、标记-压缩(Mark-Compact)和分代收集等算法。每种算法都有其优缺点,适用于不同的场景。
四、内存溢出与内存泄漏
尽管JVM提供了自动内存管理,但在实际开发中,仍需警惕内存溢出和内存泄漏的问题。
内存溢出:
- 当JVM无法申请到足够的内存空间时,会抛出
OutOfMemoryError
异常。这可能是由于堆大小设置不当、内存泄露或大对象过多导致的。
内存泄漏:
- 即应用程序中存在不再使用的对象,但由于某些原因(如循环引用)它们仍然被引用,导致垃圾收集器无法回收它们,从而浪费内存资源。
总结
深入理解JVM内存模型对于Java开发者至关重要。它不仅有助于编写更高效的代码,还能有效预防和解决内存相关的问题。随着Java应用的复杂度不断增加,对JVM内存管理的掌握将成为开发者技能树上的重要一环。