JVM的内存结构

Java虚拟机(JVM)的内存结构是其运行时数据区域的划分,这些区域用于存储程序运行时所需的各种数据。下面是对JVM内存结构各个部分的详细解释:

1. 程序计数器(Program Counter Register)

每个线程都有它自己的程序计数器,这是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。当线程正在执行一个Java方法时,程序计数器记录的是当前正在执行的虚拟机字节码指令地址;如果线程正在执行的是native方法,则程序计数器的值为空(undefined)。程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

在Java虚拟机的多线程环境下,为了支持线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,否则就会出现线程切换后执行位置混乱的问题。

native方法在Java中指的是那些由非Java语言(如C或C++)实现的方法,它们通过Java Native Interface (JNI) 与Java代码进行交互。这些方法主要用于执行Java本身不支持的操作或者需要高性能处理的任务,例如直接访问操作系统底层资源或硬件接口。

  • 声明:在Java类中使用native关键字声明一个没有方法体的方法。
  • 加载库:使用System.loadLibrary()方法加载包含该方法实现的本地库。
  • 实现:用C/C++等语言编写对应的方法实现,并将其编译为动态链接库(如Windows上的DLL文件或Linux上的.so文件)。
  • 调用:一旦库被加载,Java程序可以像调用普通Java方法一样调用这些本地方法。

native方法允许Java程序与本地系统资源进行高效交互,但同时也牺牲了部分Java的跨平台性和安全性。

这种机制使得Java能够利用现有本地代码库的优势,同时保持其高级特性和开发效率。然而,开发者需要注意管理和维护不同平台上的本地库,以及处理可能由此带来的兼容性问题。

🔢小结:程序计数器

  • 作用:保存当前线程下一条要执行的字节码指令的地址
  • 特点:
    • 每个线程有自己的程序计数器
    • 生命周期与线程相同
    • 不存在内存溢出(OutOfMemoryError)

2. Java虚拟机栈(Java Virtual Machine Stacks)

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同。栈由一系列栈帧组成,每当调用一个方法时,就会创建一个新的栈帧并将其压入栈中。栈帧包含局部变量表、操作数栈、动态链接和方法返回地址等信息。局部变量表存放了编译期可知的各种基本数据类型、对象引用等。栈的大小不是无限的,过深的方法调用会导致StackOverflowError错误,而试图扩展栈的空间但内存不足则会抛出OutOfMemoryError

  1. 局部变量表:这是存放方法参数及方法内部定义的局部变量的地方。局部变量表中的变量数量在编译期就已经确定,并且每个变量都有其特定的索引位置。对于实例方法来说,局部变量表的第一个位置会默认分配给this引用,以便访问当前对象。

  2. 操作数栈:这是一个后进先出(LIFO)栈,用于字节码指令的运算和操作。在方法执行过程中,大多数字节码指令都是针对操作数栈进行操作的,例如将数据压入栈、从栈中弹出数据以及执行算术运算等。通过操作数栈,Java虚拟机可以执行复杂的计算任务。

  3. 动态链接:每个栈帧都包含一个指向运行时常量池(Runtime Constant Pool)中该栈帧所属方法的引用,通过这个引用可以访问类或接口的方法、字段等信息。动态链接的主要作用是在调用其他方法时能够正确地找到目标方法的入口地址,并将操作数栈和局部变量表传递给被调用的方法。

  4. 方法返回地址:当一个方法执行完毕后,需要返回到它被调用的地方继续执行后续代码。方法返回地址就是保存了方法退出后应该返回到的位置,即调用者的下一条指令地址。此外,如果方法有返回值,也会在这个时候将返回值压入调用者栈帧的操作数栈中。

小结:Java虚拟机栈(JVM Stack)

  • 每个线程都有自己的虚拟机栈
  • 生命周期与线程一致
  • 栈帧结构:
    • 局部变量表:存放方法参数及内部定义的局部变量
    • 操作数栈:基于后进先出(LIFO)原则执行字节码指令运算
    • 动态链接:指向运行时常量池中该栈帧所属方法的引用,支持方法调用
    • 方法返回地址:保存方法退出后返回到的位置,以及可能的返回值
  • 异常机制:
    • 若栈深度大于JVM允许的最大深度,将抛出StackOverflowError
    • 如果JVM尝试扩展栈时没有足够的内存,将抛出OutOfMemoryError

3. 本地方法栈(Native Method Stacks)

本地方法栈与Java虚拟机栈的作用类似,只不过它是为虚拟机使用到的Native方法服务的。在Sun JDK中,本地方法栈和Java虚拟机栈是同一个区域,它们都可能出现StackOverflowErrorOutOfMemoryError异常。

4. 堆(Heap)

堆是JVM管理的内存中最大的一块,所有线程共享的内存区域,在虚拟机启动时创建。堆的主要用途是在运行时分配对象实例和数组。堆被划分为新生代和老年代,其中新生代又进一步划分为Eden区和两个Survivor区(From Survivor和To Survivor)。垃圾收集器主要工作在堆上进行内存回收,如果堆中没有足够的空间完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

堆内存是程序运行时动态分配的内存区域,主要用于存储程序在执行过程中创建的对象和数据结构。以下是关于堆内存的简洁明了的概括:

  1. 动态分配:堆内存允许程序在运行时根据需要请求任意大小的内存块。

  2. 手动管理:在C/C++中,程序员需要使用malloccallocrealloc等函数来分配内存,并通过free来释放内存;而在Java等语言中,内存由垃圾回收器自动管理。

  3. 灵活性与共享性堆内存可以提供比栈更大的空间,并且可以在不同函数或线程之间共享。

  4. 生命周期较长与栈内存相比,堆上的对象不会随着函数调用结束而自动销毁,其生命周期更长

  5. 性能考量:由于需要搜索可用内存块并可能产生内存碎片,堆内存的分配和释放速度较慢。

  6. 潜在问题:如果不正确地管理堆内存,可能会导致内存泄漏(未释放不再使用的内存)或内存碎片化(难以找到足够大的连续空闲内存块)。

  7. 多线程同步:在多线程环境中,访问堆内存可能需要适当的同步机制以避免竞态条件。

简而言之,堆内存为程序提供了灵活但需谨慎管理的动态存储解决方案,适用于那些需要长期存在或大小不确定的数据。

5. 方法区(Method Area)

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.                                                                                                                                                                                  ——Java虚拟机规范

Java虚拟机具有一块被所有Java虚拟机线程共享的方法区。方法区类似于传统语言编译代码的存储区域,或类似于操作系统进程中的"文本"段。它存储与类相关的结构,例如运行时常量池、字段和方法数据,以及方法和构造方法的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法(§2.9)。

方法区在虚拟机启动时创建。尽管方法区在逻辑上属于堆的一部分,但简单实现可以选择不进行垃圾回收或内存整理。本规范并不强制规定方法区的位置或管理编译代码的策略。方法区可以是固定大小的,也可以根据需要扩展,当更大的方法区不再必要时也可以收缩。方法区的内存不需要是连续的。

方法区也是各条线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。尽管Java虚拟机规范将方法区描述为堆的一个逻辑部分,但它有一个别名叫做Non-Heap(非堆),以区别于真正的堆。在Java8之前,方法区通常被称为永久代(Permanent Generation),但在Java8中,永久代的概念被移除,取而代之的是元空间(Metaspace)。

常量池 constant pool(javap -v XXX.class

就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池

常量池是 *.class 文件中的,当该类被加载,它的常量池(constant pool)里的信息就会放入运行时常量池,并把里面的符号地址变为真实地址

StringTable特性

  1. 常量池中的字符串仅是符号,第一次用到时才会变为对象
  2. 利用串池的机制,避免重复创建字符串对象
  3. 字符串变量拼接的原理是StringBuilder(1.8)
  4. 字符串常量拼接的原理是编译期优化
  5. 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
  • 如果字符串已经存在于常量池中intern() 方法会直接返回常量池中已存在的那个字符串的引用。
  • 如果字符串不存在于常量池中intern() 方法会将该字符串添加到常量池中,并返回这个新添加的字符串在常量池中的引用。

总结:不管之前常量池中有没有,它总是会返回常量池中的引用

String s1 = new String("hello"); // 在堆上创建一个新的字符串对象
String s2 = "hello";             // 从字符串常量池中获取"hello"的引用

String s3 = s1.intern();         // 调用 intern() 方法

System.out.println(s1 == s2);    // false, s1是堆上的新对象,s2是常量池中的引用
System.out.println(s1 == s3);    // false, s1是堆上的新对象,s3是常量池中的引用
System.out.println(s2 == s3);    // true, s2和s3都是常量池中的同一个"hello"引用

StringTable的位置

在 JDK 1.6 中,StringTable位于永久代中;在 JDK 1.8 中,永久代已经被移除,取而代之的是Metaspace,StringTable也随之移动到了中。这意味着在 JDK 1.8 中,字符串常量池中的字符串也可以被垃圾回收器回收,而在 JDK 1.6 中则不行。

添加VM参数-XX:StringTableSize=1024,实际上设置的是哈希表的大小(即桶的数量)

6. 直接内存(Direct Memory)

虽然直接内存并不属于JVM运行时数据区的一部分,但它也是影响系统性能的重要因素之一。直接内存可以通过Java NIO库中的ByteBuffer.allocateDirect()方法来分配,它可以避免在Java堆和本地堆之间复制数据,从而提高性能。但是,由于直接内存不受Java堆大小的限制,所以可能会导致OutOfMemoryError异常。

总结来说,理解JVM的内存结构对于开发高效、稳定的Java应用程序至关重要。通过对内存区域的合理配置和优化,可以有效提升应用的性能,减少内存溢出等问题的发生。同时,了解不同内存区域的特点有助于更好地进行调试和故障排查。

NIO(Non-blocking I/O,非阻塞I/O)是Java平台提供的一个用于高效I/O操作的API集合。它是在Java 1.4中引入的,并且与传统的基于流的I/O(Stream-based I/O)相比,提供了更为灵活和高效的I/O处理方式。NIO主要设计用于支持非阻塞模式下的网络编程和文件操作,特别适合需要高并发处理的应用场景。

参考文献:链接

This article was updated on