Java内存区域可以分为五大部分,包括方法区、堆、栈、本地方法栈和程序计数器。
一 程序计数器
程序计数器(Program Counter)是Java虚拟机栈中的一个组成部分,它的作用是记录当前线程执行的字节码指令地址。
程序计数器在每个线程中都是独立的,它的生命周期与线程相同。当一个线程被创建时,程序计数器也会被创建,并在该线程执行过程中持续更新。每当线程执行一条字节码指令时,程序计数器的值就会自动增加,指向下一条要执行的指令。
程序计数器的主要作用是控制线程的执行顺序,确保每条指令都能按照正确的顺序被执行。此外,它还用于实现方法调用和返回的功能。当一个方法被调用时,程序计数器的值会被保存到该方法对应的栈帧中,以便在方法返回时能够恢复到正确的位置继续执行。
需要注意的是,程序计数器并不直接存储字节码指令本身,而是存储指令的偏移量或地址。这是因为Java虚拟机规范规定了字节码指令必须以一定的格式进行存储,而程序计数器只需要知道下一个指令的位置即可。
线程私有,和线程同生共死。
不存在out of memory error 情况(唯一一个不存在OOM情况的区域)。
二 Java虚拟机栈
Java虚拟机栈是线程私有的,用于存储方法执行的内存模型,每个方法执行时都会创建一个栈帧,包括局部变量表、操作数栈、动态连接和方法返回地址。
Java虚拟机栈在Java的方法执行过程中起到关键作用,其生命周期与线程相同。当线程创建时,对应的虚拟机栈也被创建,并在线程终止时被销毁。这种设计使得每个线程都有其独立的执行空间,避免了多线程中的数据混乱和冲突。
Java虚拟机栈中的每个元素被称为“栈帧”,对应一次方法调用。当一个方法被调用时,一个新的栈帧会被创建并压入栈中,成为当前栈帧。该栈帧包含了方法的局部变量表、操作数栈、动态连接以及方法返回地址等信息。这些信息为方法的执行提供了必要支持,确保了指令能够按照正确的顺序被执行和返回。
局部变量表是栈帧中的一个重要组成部分,用于存储方法参数和局部变量。每种数据类型(基本类型、引用类型等)都有其对应的存储位置,同时在编译期间就已经确定了最大容量。局部变量表不仅支持方法内的变量声明和操作,还通过“变量槽”来优化存储空间,提高方法执行的效率。
线程私有,和线程同生共死,生命周期与线程相同。
如果线程请求的栈深度大于虚拟机所允许的深度(栈溢出),将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存(如递归调用,没有写明退出条件,进入无限递归状态)抛出OutOfMemoryError异常。
三 本地方法栈
本地方法栈是Java虚拟机中的一个内存区域,用于支持本地方法(Native Method)的调用和执行。
本地方法栈在Java虚拟机中的作用主要与本地方法的调用相关。当一个线程调用一个本地方法时,这个本地方法并不在Java虚拟机栈上执行,而是在本地方法栈上执行。本地方法指的是用非Java语言编写的方法,通常是C、C++或汇编语言等低级语言。Java通过JNI(Java Native Interface)来调用这些本地方法,从而能够利用本地代码的优势,比如更高效地访问硬件资源或者调用操作系统级别的功能。
本地方法栈是线程私有的,每个线程都有自己的本地方法栈,这样在一个线程调用本地方法时,其他线程不受影响。本地方法栈中的每一个方法调用也对应一个栈帧,这个栈帧包含了方法的局部变量表、操作数栈、动态链接以及方法返回地址等信息。
在HotSpot JVM实现中,本地方法栈和虚拟机栈被合二为一,这意味着Java方法和本地方法都在同一个栈中执行。这种设计简化了内存模型并降低了内存开销。
线程私有,和线程同生共死;会抛出OutOfMemoryError异常。
四 堆
堆(Heap)是Java虚拟机中用于存储对象实例的区域。
在Java程序运行时,所有的对象实例都是在堆上分配的。堆是所有线程共享的内存区域,它的大小可以在JVM启动时通过参数进行设置,也可以在运行时动态调整。
堆被划分为不同的区域,包括新生代和老年代。新生代又分为Eden区、Survivor 0区和Survivor 1区。新创建的对象首先分配在Eden区,当Eden区满时,会触发一次Minor GC(小型垃圾回收),将存活的对象复制到Survivor 0区,同时清空Eden区。如果Survivor 0区也满了,再次触发Minor GC时,存活的对象会被复制到Survivor 1区,并清空Survivor 0区。经过多次Minor GC后仍然存活的对象会被移动到老年代。老年代的空间不足时,会触发Major GC(大型垃圾回收),对整个堆进行清理。
堆内存的管理是由垃圾收集器(Garbage Collector)负责的。垃圾收集器会根据一定的算法自动回收不再使用的对象所占用的内存空间,从而避免了内存泄漏的问题。
需要注意的是,由于堆是所有线程共享的,因此在多线程环境下,需要确保线程安全地访问堆中的对象。此外,频繁地进行垃圾回收可能会导致性能下降,因此合理地设计对象生命周期和使用合适的数据结构也是优化堆内存管理的关键。
Java堆可以物理上不连续,但在逻辑上应连续。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
五 方法区
方法区在Java虚拟机中的作用主要与类的加载和静态变量的存储有关。当一个类被加载时,其二进制数据会被读取到方法区,并创建对应的运行时数据结构,包括类的信息、常量池、字段数据、方法数据等。这些数据在程序的整个运行期间都是共享的,即所有线程都能访问这些数据。
方法区的存储位置可能在堆上,也可能在堆外。这取决于具体的JVM实现。例如,在HotSpot JVM中,方法区是在堆上的永久代(PermGen)或元空间(Metaspace)中实现的。
方法区中存储的数据类型包括:
- 类元数据:包括类的名称、访问修饰符、常量池、字段、方法等信息。
- 常量池:存储编译期生成的字面量和符号引用。
- 静态变量:存储类的静态字段。
方法区的大小可以在JVM启动时通过参数进行设置,也可以在运行时动态调整。如果方法区的空间不足,会触发Full GC(全量垃圾回收),清理不再使用的数据。
需要注意的是,由于方法区是所有线程共享的,因此在多线程环境下,需要确保线程安全地访问方法区中的数据。此外,频繁地进行Full GC可能会导致性能下降,因此合理地设计类结构和使用合适的数据结构也是优化方法区内存管理的关键。
六 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中
具有动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
思考:谈谈JVMH中的常量池?
- Class文件常量池
class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
- 运行时常量池
运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
- 全局字符串常量池
字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
- 基本类型包装类对象常量池
java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。