深入探究Java内存模型

简介: 深入探究Java内存模型


🌟 Java虚拟机内存模型

Java虚拟机(JVM)是一种能够在不同平台上运行Java程序的虚拟机。JVM内部有一个内存模型,用于管理其内部的内存分配。JVM内存模型可以分为以下五个部分:

🍊 一、方法区

方法区也被称为永久代(Permanent Generation),是Java虚拟机用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的区域。它是虚拟机规范中的一部分,属于非堆内存。在JDK1.8之前,方法区是使用永久代实现的,而JDK1.8之后则使用元空间(Metaspace)来代替永久代,将方法区移到了本地内存中。

在Java虚拟机中,有一种名为“即时编译器”的技术,它可以将Java字节码转换为本地机器代码,并将这些代码存储在方法区中。一些框架,如Spring、MyBatis等,需要进行反射等操作,这些操作会用到通过类加载器加载的类信息和一些常量,而这些信息会存储在方法区中。

方法区中存储的数据包括:

  • 已加载类的类型信息,包括类的元数据(如类的访问修饰符、父类、实现的接口、字段、方法等)和类的静态变量。
  • 常量池(Constant Pool):常量池是类加载后存储在方法区的一段内存空间,用于存储编译器生成的各种字面量和符号引用。
  • 即时编译器编译后的字节码:将Java字节码转换为本地机器代码,并将这些代码存储在方法区中。
  • 字符串常量池:存储Java字符串常量。

由于方法区存储的是不可变的数据,容易出现内存泄漏的情况。特别是在使用自定义类加载器时,如果频繁进行类的加载和卸载操作,就有可能导致方法区中的数据越来越多,最终导致内存泄漏。

在JDK1.8之前,方法区使用永久代实现,其大小是固定的且无法回收的,因此容易导致过度内存占用。即使在JDK1.8之后,方法区使用元空间代替永久代,但在默认情况下,元空间也是没有大小限制的,因此仍可能导致过度内存占用。

由于方法区存储的数据通常都是不可变的,因此垃圾回收器在进行垃圾回收时需要扫描大量的无用数据,导致GC效率低下。

为了避免方法区带来的问题,可以采取以下优化手段:

  • 设置方法区最大值:可以通过JVM启动参数来进行控制方法区的大小,包括最小值、最大值、初始值。当方法区达到最大值时,JVM会发生OOM(Out Of Memory)错误。
  • 使用内存泄漏检测工具:在使用自定义类加载器时,可以采用内存泄漏检测工具来检测是否存在内存泄漏问题。
  • 对不需要的类进行卸载:在加载某个类时,可以根据需要选择是否卸载该类。当某个类不再被需要时,可以手动将其卸载,从而避免方法区中的数据越来越多,导致过度内存占用和GC效率低下。
  • 调整元空间大小:在JDK1.8之后,可以使用元空间代替永久代,将方法区移到了本地内存中。可以通过JVM启动参数来控制元空间的大小,从而避免过度内存占用。
  • 使用弱引用:在使用动态语言和代码生成技术时,可以使用弱引用来避免内存泄漏问题。当某个对象不再被使用时,弱引用会自动将其清除。

🍊 二、堆

🎉 堆的基本概念

堆是Java虚拟机运行时数据区之一,用于存储对象实例。堆是在JVM启动时创建,并且在JVM关闭时才会被销毁,堆的大小可以通过-Xmx参数控制。堆的大小不足会导致OutOfMemoryError,而堆的过大会导致GC时间过长,影响程序的实际性能。

🎉 堆的结构

堆由不同的区域构成:新生代、老年代。新生代由Eden区和Survivor0区、Survivor1区组成。

📝 新生代

新生代是堆的一部分,用于存放新创建的对象。新生代中的对象生命周期短暂,一般很快就会被回收掉。新生代分为Eden区和Survivor0区、Survivor1区。其中,Eden区用于存放新创建的对象,Survivor0区和Survivor1区用于存放经过一次Minor GC后仍然存活的对象。一块Eden区和两块Survivor区比例是8:1:1。

在新生代,每个对象都有一个年龄计数器。当对象在Eden区中被创建时,年龄计数器初始化为0,每经过一次Minor GC年龄计数器的值就会加1。当年龄计数器的值达到一定阈值时,对象将会被晋升到老年代中。晋升到老年代中的对象将会在进行Full GC时被回收。

📝 老年代

老年代是堆的一部分,用于存放存活时间较长的对象。老年代中的对象生命周期较长,一般不会被频繁回收。老年代中的对象在进行Full GC时才会被回收。

🎉 堆的分配策略

堆的分配策略包括两种:对象优先分配和空间优先分配。

📝 对象优先分配

对象优先分配是JVM默认的分配策略,它将新创建的对象分配到Eden区中,如果Eden区空间不足,就会触发Minor GC。在Minor GC时,经过垃圾回收后,如果对象还存活,就会被移动到Survivor0区或Survivor1区中,如果Survivor0区或Survivor1区空间不足,就会触发Minor GC。当对象在Survivor0区或Survivor1区中经过一定次数的垃圾回收后仍然存活,就会被晋升到老年代中。

📝 空间优先分配

空间优先分配是指JVM将新创建的对象分配到空间使用率较低的区域中。通常情况下,空间使用率较低的区域是老年代。在空间使用率较低的情况下,空间优先分配策略可以减少垃圾回收的次数,从而提高程序的性能。

🎉 堆的性能调优

堆的性能调优是Java程序优化的重要部分,主要包括以下几个方面:

  • 堆的大小调优:堆的大小直接影响程序的性能,需要根据实际情况对堆的大小进行调优。
  • 新生代和老年代的分配比例:新生代和老年代的分配比例也影响程序的性能,通常情况下,新生代占总堆大小的1/3到1/4比较合适。
  • 垃圾回收算法的选择:垃圾回收算法的选择也影响程序的性能,需要根据实际情况选择合适的垃圾回收算法。

总的来说,堆是Java虚拟机运行时数据区之一,用于存储对象实例。堆的大小和分配策略对程序的性能有着至关重要的影响。因此,需要根据实际情况对堆进行性能调优,以提高程序的效率和性能。

🍊 三、Java虚拟机栈

栈帧是Java虚拟机执行Java程序的基本单元。在Java程序中,每个方法被调用时,都会为该方法创建一个栈帧。栈帧包括了局部变量表、操作数栈、动态链接、方法出口和线程信息等。当方法执行完成时,栈帧会被销毁。Java虚拟机通过栈帧的入栈和出栈来管理Java程序的方法调用过程。在栈帧的执行过程中,Java虚拟机可以实现动态查找和链接,从而实现了Java程序的跨平台执行。

🎉 栈帧的创建过程

当Java程序调用一个方法时,虚拟机会根据方法的描述信息,为该方法创建一个栈帧。栈帧包括了局部变量表、操作数栈、动态链接、方法出口等信息。Java虚拟机栈将该栈帧入栈,使得该栈帧成为当前栈帧。从而,被调用方法开始执行。

🎉 局部变量表

局部变量表是用于存放方法参数和局部变量的。在栈帧被创建的时候,局部变量表就会被分配空间。局部变量表的大小在编译期间就已经确定了,但是其所需的大小在运行时才能确定。因此,在方法运行之前,Java虚拟机需要根据局部变量表的大小来分配栈帧所需的内存空间。局部变量表所需的内存空间取决于方法所需的局部变量的数量。

🎉 操作数栈

操作数栈是一个后进先出的栈,用于存放方法所有的中间结果。它是栈帧的一个重要组成部分,在方法执行过程中,任何操作都必须通过操作数栈来完成。当方法被调用时,操作数栈是空的。在方法执行过程中,操作数栈中的元素会因为方法中的操作而被推入或弹出。当方法执行完成时,操作数栈被清除,而栈帧也随之出栈。

🎉 动态链接

动态链接是在编译期无法确定的方法调用跳转。在Java虚拟机中,每个栈帧都有一个指向它所属的类的指针,称为类指针(Class Pointer)。在Java虚拟机中,方法的调用通常是通过一个符号引用来实现的。符号引用包括了方法的名字、返回值类型和参数列表的描述符。当Java虚拟机遇到一个符号引用时,它会通过该符号引用查找对应的方法。这个查找过程就称为动态链接。这种动态查找和链接的方式,是Java虚拟机实现跨平台的一种技术手段。如果在查找过程中发现方法没有找到,虚拟机会抛出NoSuchMethodError错误。

🎉 方法出口

方法出口是一个指向方法调用者的返回地址的指针。当一个方法被调用时,方法出口会被压入操作数栈中。当方法执行完成时,该方法的返回值会被压入操作数栈中,返回地址也会从栈中弹出到程序计数器(PC)中,使程序继续执行。

🎉 线程信息

Java程序中的线程是轻量级的执行单元。Java虚拟机会为每个线程分配一个Java虚拟机栈,每个栈由多个栈帧组成。每个线程在运行时,都有一个栈帧作为当前栈帧。每个栈帧包括了线程所需的局部变量表、操作数栈、动态链接、方法出口和线程信息等。线程信息包括了线程的ID、线程名、线程状态等信息。

🎉 栈帧的销毁过程

栈帧的销毁是指栈帧从虚拟机中出栈的过程。当方法执行完成时,Java虚拟机会将该方法的栈帧出栈,并将方法的返回值压入方法调用者的操作数栈中。当方法调用者继续执行时,它会弹出被调用方法的返回值。这个过程就完成了栈帧的销毁。

🍊 四、本地方法栈

本地方法栈是Java虚拟机中的一个重要组成部分,是Java程序中调用的本地方法所使用的内存区域,也是线程私有的。在Java虚拟机栈中,栈帧保存的是Java方法的状态,而在本地方法栈中,栈帧保存的是本地方法(Native Method)的状态。本地方法是Java程序中调用本地库(Native Library)的接口,也就是通过JNI(Java Native Interface)调用外部的C/C++等本地代码,在这种情况下,Java虚拟机就需要提供一片内存区域来支持本地方法的执行。

本地方法栈的空间大小也是可以通过JVM启动参数来控制的,参数为-Xss。默认情况下,64位JVM的本地方法栈大小为1MB,32位JVM的本地方法栈大小为320KB。当本地方法栈空间不足时,会发生StackOverflowError;当本地方法栈空间无法继续扩展时,会发生OutOfMemoryError。

本地方法栈与Java虚拟机栈的区别在于,Java虚拟机栈保存的是Java方法的状态,而本地方法栈保存的是本地方法的状态。另外,Java虚拟机栈是由JVM自动管理的,包括分配和释放;而本地方法栈则是由本地方法本身负责管理的。在调用本地方法之前,JVM需要将本地方法的参数传递给本地方法,参数传递的方式和C语言类似,有寄存器传递和栈传递两种方式。当本地方法执行完毕后,JVM需要将本地方法的返回值传递回Java程序中,返回值传递的方式同样有寄存器传递和栈传递两种方式。

本地方法栈的创建和销毁与方法调用的进入和返回有关,在Java程序调用本地方法时,JVM会检查是否已经加载了本地方法所在的本地库,并确保本地库已经正确地链接到JVM中。然后,JVM会创建一个新的本地方法栈,并将本地方法的参数复制到本地方法栈中,本地方法开始执行。当本地方法执行完毕并返回时,JVM会将返回值复制回到Java程序中,然后销毁本地方法栈,继续执行Java程序中的其他代码。

本地方法栈在Java程序中的使用相对较少,通常是在需要调用本地库的情况下才使用本地方法栈。如果本地方法栈的空间不足,可以通过增加JVM的栈空间来解决。但是,在实际开发中,我们应该尽量避免使用本地方法,因为本地方法容易引起内存泄漏和安全问题,同时本地方法的跨平台性也比较差。

🍊 五、程序计数器

程序计数器是JVM中的一块较小内存区域,主要用于记录当前线程运行的字节码指令地址,也就是下一条要执行的指令在代码中的位置。JVM中所有线程都有一个独立的程序计数器,它是线程私有的,不会被其他线程访问。

程序计数器的作用是在多线程环境下保证线程切换后能恢复到正确的执行位置。当线程被中断或被抢占时,程序计数器记录了断点的位置,下次恢复时就可以从这个位置继续执行。线程执行Java代码时,程序计数器记录的是当前执行的字节码行数。

除了恢复现场,程序计数器还有一个作用就是支持代码的解释执行。字节码解释器按照程序计数器中的地址,从方法字节码中依次获取指令并执行。如果执行的是Java方法,则程序计数器记录的是该线程当前执行的Java方法地址,如果执行的是本地方法,则程序计数器记录的是undefined。

当线程调用了本地方法时,程序计数器保存的是undefined,当返回到Java方法时,程序计数器会恢复到该方法的指令地址。Java虚拟机规范要求程序计数器是线程私有的,每个线程独立维护。这种设计方案在一定程度上简化了线程上下文切换的操作。

程序计数器的大小是固定的,不会发生OOM错误。对于32位JVM来说,程序计数器的最大值是2的32次方,也就是4GB。对于64位JVM来说,程序计数器的最大值是2的64次方,也就是18EB(Exabytes),完全无需考虑OOM的问题。

程序计数器是JVM中非常重要的一个概念,它是实现Java虚拟机线程安全的关键所在。虽然它的作用看起来比较简单,但是却不可少。程序计数器负责记录线程下一条执行的指令,遇到中断或者线程切换时能够恢复到正确的执行位置,从而保证线程的正确性和安全性。


相关文章
|
25天前
|
缓存 安全 Java
Java并发编程进阶:深入理解Java内存模型
Java并发编程进阶:深入理解Java内存模型
33 0
|
23天前
|
存储 算法 Java
深入浅出Java内存管理
【8月更文挑战第28天】Java的内存管理是每个Java开发者都绕不过去的技术话题。本文将通过生动的比喻和直观的例子,带你走进Java内存管理的奇妙世界。我们将一起探索对象在Java虚拟机中的生命周期,了解栈与堆的区别,以及垃圾回收机制如何默默守护着我们的应用程序。准备好,我们即将启程!
46 14
|
15天前
|
算法 安全 Java
Java内存管理:深入理解垃圾收集器
在Java的世界里,内存管理是一块基石,它支撑着应用程序的稳定运行。本文将带你走进Java的垃圾收集器(GC),探索它是如何默默守护着我们的内存安全。我们将从垃圾收集的基本概念出发,逐步深入到不同垃圾收集器的工作机制,并通过实例分析它们在实际应用中的表现。文章不仅旨在提升你对Java内存管理的认识,更希望你能通过这些知识优化你的代码,让程序运行更加高效。
34 3
|
23天前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
21天前
|
Kubernetes Cloud Native Java
云原生之旅:从容器到微服务的演进之路Java 内存管理:垃圾收集器与性能调优
【8月更文挑战第30天】在数字化时代的浪潮中,企业如何乘风破浪?云原生技术提供了一个强有力的桨。本文将带你从容器技术的基石出发,探索微服务架构的奥秘,最终实现在云端自由翱翔的梦想。我们将一起见证代码如何转化为业务的翅膀,让你的应用在云海中高飞。
|
6天前
|
监控 算法 Java
Java中的内存管理:理解垃圾回收机制的深度剖析
在Java编程语言中,内存管理是一个核心概念。本文将深入探讨Java的垃圾回收(GC)机制,解析其工作原理、重要性以及优化方法。通过本文,您不仅会了解到基础的GC知识,还将掌握如何在实际开发中高效利用这一机制。
|
6天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理策略和垃圾回收机制。首先介绍了Java内存模型的基本概念,包括堆、栈以及方法区的划分和各自的功能。进一步详细阐述了垃圾回收的基本原理、常见算法(如标记-清除、复制、标记-整理等),以及如何通过JVM参数调优垃圾回收器的性能。此外,还讨论了Java 9引入的接口变化对垃圾回收的影响,以及如何通过Shenandoah等现代垃圾回收器提升应用性能。最后,提供了一些编写高效Java代码的实践建议,帮助开发者更好地理解和管理Java应用的内存使用。
|
13天前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
43 11
|
15天前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
38 11
|
12天前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。