# JVM内存结构
# 简要回答
- JVM内存结构(运行时数据区)可以分为Java虚拟机栈、堆、方法区、本地方法栈和程序计数器五个部分。在JDK8之前,方法区通过永久代实现,JDK8及以后,永久代被元空间取代。
- Java虚拟机栈:线程私有,存储方法栈帧。
- 堆:线程共享,存储对象实例,是GC主要区域。
- 方法区:线程共享,存储类信息、常量、静态变量等,JDK8后由元空间实现。
- 本地方法栈:线程私有,为本地方法服务。
- 程序计数器:线程私有,记录当前线程执行的字节码指令地址。
# 详细回答
Java虚拟机栈(Java Virtual Machine Stacks) :每个线程都有一个私有的虚拟机栈,和线程同时创建。每个方法在执行时会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
- 栈的生命周期和线程一致,由操作系统管理。
- 虚拟机栈的大小可以是动态的或者是固定不变的。如果是固定大小,每个线程的虚拟机栈容量在线程创建时指定,线程请求分配的栈容量大于虚拟机栈允许的最大容量时,会抛出StackOverflowError异常。如果是动态扩展的,在没有足够内存时可能会抛出一个OutOfMemoryError异常。
Java堆(Java Heap) :是Java虚拟机中最大的一块内存区域,存储对象实例和数组。堆被所有线程共享,在虚拟机启动时创建。默认初始堆内存大小是电脑内存大小的1/64,最大堆内存大小是电脑内存大小的1/4。
- 新生代:可以分为Eden区和Survivor区,大多数新创建的对象首先存放在Eden区,当Eden区满时会触发一次Minor GC(新生代垃圾回收)。Survivor区分为两个大小相等的区域(S0和S1),每次Minor GC后存活下来的对象被移动到S0或S1中,继续对象的生命周期。Eden和S0,S1的比例是8:1:1。
- TLAB:在Eden空间内,JVM为每个线程分配的私有缓存区域,内存占Eden空间的1%。多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时可以提升内存分配的吞吐量。
- 老年代:存放一次或多次Minor GC后仍存活的对象,这些对象生命周期较长,Major GC(老年代垃圾回收)频率低,但是执行时间长于Minor GC。新生代和老年代的比例大概在1:2,老年代大于新生代,存储更多的长期存活对象。
- 大对象区:有的JVM实现会为需要大量连续内存空间的对象分配专门区域,这些大对象会直接分配在老年代,避免新生代频繁GC导致内存碎片化。
- 字符串常量池:JDK7之后出现,JVM为了提升性能和减少内存消耗,对字符串(String类)专门开辟的一块区域,可以避免字符串的重复创建。
- 永久代:JDK7及之前存在堆中,是方法区的实现,存储类的元数据,运行时常量池,已加载的类信息,静态变量和常量。
- 元空间:JDK8及之后,取代永久代,存储在本地内存中,存储类的元数据信息(字段、方法信息),能够解决永久代容易出现的内存溢出问题。
方法区(Method Area/Non-Heap) :存储已经被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码。大小和堆空间一样,可以固定或者可扩展,能够决定系统可以放多少个类,JVM关闭时方法区被释放。
- 运行时常量池:JVM会为每个已加载的类型维护一个常量池。是方法区的一部分,在类加载后,存放编译器生成的各种字面量和符号引用。与class文件中的常量池不同,运行时常量池可以动态改变。
本地方法栈(Native Method Stack) :管理本地方法的调用,是线程私有的。本地方法是使用其他编程语言实现的,通过JNI(Java Native Interface)与Java代码进行交互。
- 与虚拟机栈相同,允许固定或者可动态分配的内存空间。
- 本地方法栈会登记本地方法,生成本地方法接口,可以根据本地方法接口加载本地方法库中对应的实现方法,本地方法栈为本地方法提供内存支持。
程序计数器(Program Counter Register) :当前线程所执行的字节码的行号指示器,在多线程环境下,每个线程有自己独立的程序计数器,当线程执行Java方法时,程序计数器记录正在执行的虚拟机字节码指令的地址。
- 是一个很小的内存空间,运行速度快。
- 是线程私有的,生命周期和线程一致。
直接内存(Direct Memory) :直接内存与JVM的内存管理有关。Java的NIO库允许直接分配堆外内存,这些内存不受Java堆大小的限制,也不受垃圾回收器管理。通常通过ByteBuffer类来使用。
# 知识图解
jvm运行时数据区组成示意图

# 知识扩展
- 扩展:
- 栈帧(Stack Frame) :每个栈帧会存储方法的局部变量表、操作数栈、动态链接和方法返回地址等。
- 局部变量表:存储方法参数和定义在方法中的局部变量。其中的变量只在当前方法中有效。局部变量表需要的容量大小是编译器确定的。被局部变量表中直接引用或间接引用的变量都不会被回收。
- 操作数栈(表达式栈):保存计算过程的中间结果,在方法执行过程中会根据字节码指令往操作数栈中写入数据或提取数据。
- 动态链接:指向运行时常量池的方法引用,可以将符合引用转换成调用方法的直接引用。
- 方法返回地址:存放调用该方法的PC寄存器的值。当方法返回时,将返回值压入调用者栈帧的操作数栈,设置寄存器值,让调用者方法继续执行。如果是因异常退出方法,不会给调用者返回值。
- 面试官可能追问:
- Q1:为什么需要程序计数器记录当前线程的执行地址?
- CPU需要不停切换各个线程,JVM的字节码解释器需要通过改变程序计数器的值来明确下一条应该执行什么字节码指令。
- Q2:为什么会使用本地方法?
- Java是一次编写、到处运行的语言,无法直接操作操作系统的核心资源,本地方法可以直接与操作系统交互,比如java.io.File类通过本地方法调用操作系统的文件系统API。线程管理也依赖本地方法调用操作系统的线程调度接口。
- 使用本地方法可以复用非Java的代码库。
- Q3:为什么使用元空间取代永久代?
- 永久代受堆大小限制,元空间使用的是本地内存,而且有动态扩容机制,能够解决永久代容易内存溢出的问题。
- 永久代很难设置合适的内存大小,并且调优困难。
- Q4:给对象的分配内存过程是什么?
- 新创建的对象会计算其所需内存是否为大对象,如果是则直接分配在大对象区(老年代)。
- 非大对象会优先在新生代的Eden区的TLAB中分配,如果TLAB已满,则分配在新生代Eden区。
- 如果Eden区已满,JVM会进行一次Minor GC,将存活对象移动To Survivor区(S0,S1交替),再将新对象放在Eden区。
- 如果S0或S1中的对象经历一定回收次数(默认15次),会被移动到老年代。
- 如果老年代内存不足,会触发Major GC,若仍旧内存不足则会触发OOM异常。
评论
验证登录状态...