JVM中内存是如何分配的
JVM在执行Java程序时,会把它管理的内存划分为若干个不同的数据区域。这些区域可以分为两大类:线程共享区域和线程私有区域。
线程共享区域 (所有线程共享)
-
Java堆 (Java Heap): 这是JVM管理的内存中最大的一块,也是我们最关心的区域。它的唯一目的就是存放对象实例和数组。几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器(GC)管理的主要区域。
-
方法区 (Method Area): 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。在JDK 1.8及以后,方法区的实现从永久代(PermGen)变为了元空间(Metaspace),元空间使用的是本地内存,而不是JVM堆内存。
线程私有区域 (每个线程独有一份)
-
虚拟机栈 (JVM Stack): 每个线程在创建时都会创建一个虚拟机栈。每当一个方法被调用,就会创建一个“栈帧”(Stack Frame)并压入栈中,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用结束,栈帧就出栈。
-
本地方法栈 (Native Method Stack): 与虚拟机栈类似,但它是为虚拟机使用到的本地(Native)方法服务的。
-
程序计数器 (PC Register): 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。它是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError情况的区域。
核心:Java堆中的内存分配
现在我们来聚焦于对象内存分配的核心区域——Java堆。现代主流的JVM(如HotSpot)都采用分代收集算法,因此堆内存的分配是基于这个理论来进行的。
分代策略
Java堆被划分为两个主要部分:
- 新生代 (Young Generation)
- 老年代 (Old Generation)
新生代又被细分为: * 一个伊甸园区 (Eden Space) * 两个幸存者区 (Survivor Space),通常称为S0和S1。
这种划分基于一个重要的观察:绝大多数Java对象都是“朝生夕死”的,生命周期很短。
对象的分配与晋升流程
一个对象的内存分配通常会经历以下旅程:
-
诞生于伊甸园 (Eden) 绝大多数情况下,新创建的对象会被首先分配在新生代的Eden区。这是一个非常高频的操作。
-
伊甸园满,触发Minor GC 当Eden区没有足够空间进行下一次分配时,虚拟机会触发一次新生代的垃圾收集,即Minor GC。
- 在GC期间,Eden区中还存活着的对象,会被复制到其中一个空的幸存者区(比如S0)。
- 同时,对象的“分代年龄”会加1。
- GC完成后,Eden区被清空。
-
在幸存者区之间“倒腾” 当Eden区再次被填满,又一次触发Minor GC时:
- GC会扫描Eden区和当前正在使用的幸存者区(比如S0)。
- 将所有存活的对象,复制到另一个空的幸存者区(S1)。
- 这些被复制的对象的年龄再次加1。
- GC完成后,Eden区和S0区都被清空。 此后,S0和S1的角色会互换,如此循环往复。
-
晋升到老年代 (Promotion) 一个对象不会永远在新生代停留。当它的分代年龄达到一个阈值(默认是15)时,它就会在下一次Minor GC时被“晋升”到老年代。
特殊情况
- 大对象直接进入老年代:如果一个对象非常大(比如一个巨大的数组),需要大量连续的内存空间,JVM会选择让它直接在老年代分配。这样做是为了避免这个大对象在新生代的Eden区和两个Survivor区之间进行大量的复制,这会带来很高的性能开销。
- 分配并发处理 - TLAB:为了提高对象分配的效率,特别是在多线程环境下,JVM使用了TLAB(Thread Local Allocation Buffer)技术。即为每个线程在Eden区预留一小块私有缓冲区域。线程创建新对象时,首先在自己的TLAB中分配,避免了多线程竞争堆内存时需要加锁的开销。
栈中的内存分配
与堆不同,栈的内存分配是高度确定的。当一个方法被调用时,一个栈帧被创建并压入栈;当方法返回时,栈帧被弹出并销毁。栈帧中用于存放局部变量的空间,在编译期间就已经确定大小。这种分配和回收速度非常快,不涉及复杂的GC过程。