JVM——内存模型
1、程序计数器(Program Counter):
用于记录当前线程待执行的字节码指令位置。若执行的是Native方法则计数器值为null。是唯一一个不会触发OOM的区域,生命周期与线程相同。
- 函数调用:当前PC值压入栈,PC更新为函数入口地址,返回时函数地址弹出到PC地址
- 循环/条件分支:直接跳转到对应的字节码指令位置
- 多线程切换:涉及到各个线程PC的保存和恢复
2、虚拟机栈:
线程独享内存,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,可能会抛出StackOverflowError(栈过深,超出栈容量)和OOM异常。生命周期与线程相同。
3、本地方法栈:
与虚拟机栈类似,但是为Native方法准备的栈,在HotSpot虚拟机中和虚拟机栈合二为一。也可能发生栈过深异常或OOM异常
4、堆(Heap):
JVM中最大的内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例。被划分为新生代和老年代,利于GC。新生代又被分为Eden区和Survivor区。堆溢出时也会发生OOM异常。
5、方法区(元空间):
使用本地内存,用于存储已被虚拟机加载的类信息、常量、静态变量、方法字节码、符号引用等数据。被称为堆的逻辑部分,可以不选择垃圾回收。内存不足抛出OOM异常。
- 运行时常量池:方法区的一部分,用于存放编译期或运行时生成的各种字面量和符号引用。内存不足抛出OOM异常,jdk1.8后移入本地内存中的元数据区域,jdk1.7前位于堆内的永久代
6、直接内存:
不属于JVM运行时数据区的一部分,通过NIO类引入,是一种堆外内存,可显著提高IO性能。受本机总内存的限制。内存不足抛出OOM异常。
栈详解:
- 栈中存储的是当前线程中的局部变量和方法调用信息,方法返回地址及以一些临时数据
- 栈中一般存储的是对象的引用,指向堆中的对象实例
- 每当有方法调用时都会创建一个栈帧,用于存储该方法的信息,方法执行完毕后对应栈帧也会移除
- 栈的存取速度比堆快,使用的是先进后出结构
- 栈空间相对较小且固定,可能会发生栈溢出(递归过深或内存超限)
- 栈中数据对于线程来说是私有的
堆详解:
- 存储对象实例,数据的生命周期交由GC管理
- 存储空间较大,但存取速度相对较慢
- 对所有线程共享数据
新生代:
分为Eden区和Survivor区。Eden区用于存放存活概率较低的对象(如大多数新创建的对象),Eden区相对较小;Survivor区分为from区和to区,每次进行Minor GC后,Eden区存活的对象和form区存活的对象会移动到to区域,并增加它们的年龄。
当Eden区满或Survivor区空间不足时,会触发Minor GC,回收Eden区中的垃圾对象,将年龄较大(超过15)的对象晋升到老年代中
老年代:
存放长期存活的对象,存储空间一般是新生代的两倍,因此Major GC的发生频率较低。
老年代空间不足时,会触发Major GC的CMS并发回收,并发回收失败则触发full GC
大对象区:
某些jvm实现(如G1垃圾收集器),为大对象分配了专门的区域。大对象区域指需要大量连续空间的对象,例如大数组。这类对象直接分配在老年代,以避免新生代频繁晋升导致的内存碎片化和触发垃圾回收。
内存泄漏:
本质上是已经不再需要使用的对象仍然存在引用无法被回收。导致可用内存逐渐减少。
常见原因:
- 静态集合:使用静态数据结构(HashMap或ArrayList),静态对象生命周期与JVM进程相同,存储对象且未在使用完后及时清理
- 事件监听:未取消事件对事件源的监听,导致对象持续被引用
- 会话或连接未及时关闭:例如数据库连接,流式处理的连接都会分配内存进行处理,若没有关闭也会造成内存泄漏。
- 线程:未停止的线程持有对无用对象的引用(例如ThreadLocal实例内部持有value的强引用,而key由于弱引用已被回收)