内存结构/运行时数据区
目录
背景知识:
内存结构
新生代如何转变成老年代
如何理解永久代、年轻代、老年代
核心概念
为什么图上看起来方法区是在堆外的?明明方法区应该 是在堆内的
NIO和BIO
1. 传统 I/O:BIO
2. 新 I/O:NIO
背景知识:
JVM运行流程:
- Java源代码编译成class字节码文件,由类加载系统装载到运行时数据区;
- 运行时数据区把字节码加载到内存
- 执行引擎负责将字节码翻译为底层系统指令。
纠错:是MetaSpace,不是MateSpace


内存结构
我们常说的内存结构 === 运行时数据区
运行时数据区又分为:方法区、堆、虚拟机栈、本地方法栈、程序计数器
线程私有的有:程序计数器、虚拟机栈、本地方法栈
线程共享的有:堆、方法区
程序计数器:记录当前线程正在执行的字节码指令的地址,物理上程序计数器是使用“寄存器”完成的。
虚拟机栈:
- 也叫线程栈,每个线程运行时所需要的内存,一个栈是由多个栈帧组成
- 栈帧对应着每次方法调用时所占有的内存,存储的内容是:方法参数、方法内局部变量、方法返回地址
- 一个线程每时刻只能有一个活动栈帧,对应当前正在执行的方法
- 垃圾回收不涉及到 栈内存
- 方法递归过多 会导致 java.lang.StackOverflowError 栈内存溢出
本地方法栈
- 本地方法接口运行时所需要的内存空间
- 以 native 修饰的方法就是本地方法,本地方法不是用Java写的(没有方法实现的,只有一个接口供Java调用),而是用C或C++编写,因为 Java 有些时候不能直接和操作系统底层打交道,因此Java通过接口间接调用C或C++编写的本地方法来与操作系统底层的API打交道。即Java通过本地方法调用操作系统底层功能。
堆
- 线程共享的区域,保存对象实例
- 逻辑组成:年轻代(生命周期短的对象) + 老年代(生命周期长的对象)
- JDK1.7中堆中存在 方法区的实现:永久代, JDK1.8把方法区的实现从堆移到本地内存且叫做:元空间
新生代如何转变成老年代

如何理解永久代、年轻代、老年代
一个绝佳的比喻
把JVM想象成一个 工厂:
- 年轻代 & 老年代(堆):就像是 原料仓库和生产线。
-
- 里面存放的是生产用的原材料和生产出来的产品(对象实例)。
- 这些原料和产品经常进进出出,不断更新(GC)。
- 永久代:就像是 工厂的行政办公室和设计图纸库。
-
- 里面存放的是员工手册(字节码)、产品设计图纸(类结构)、供应商名录(常量池)。
- 这些资料很少变动,一旦录入就会长期使用。
结论:
尽管“办公室”和“仓库”都在同一个工厂大院(堆内存)里,但它们的功能、管理方式和内容是完全不同的。这就是为什么我们说永久代在逻辑上是“非堆”(Non-Heap)。
现代变化:
到了JDK 8,这个“办公室”(永久代)被彻底搬出了工厂大院,在外面自立门户了,这就是元空间(Metaspace),它直接使用操作系统的本地内存,不再占用JVM堆的空间。
核心概念
首先需要明确:方法区是JVM规范的概念,而永久代/元空间是具体实现
- 方法区:JVM规范定义的内存区域,用于存储类信息、常量、静态变量等
- 永久代/元空间:方法区的具体实现方式
JDK 1.7 的永久代(PermGen)
// 在JDK 1.7中,这些数据都存储在永久代
public class Example {private static String staticVar = "静态变量"; // 存储在永久代private final String constant = "常量"; // 存储在永久代public void method() {String internStr = "字符串".intern(); // 字符串常量池在永久代}
}
永久代特点:
- 位于堆内存中
- 有固定的大小限制(通过
-XX:MaxPermSize设置) - 容易发生
java.lang.OutOfMemoryError: PermGen space
JDK 1.8 的元空间(Metaspace)
// 在JDK 1.8中,这些数据的存储位置发生了变化
public class Example {private static String staticVar = "静态变量"; private final String constant = "常量"; public void method() {String internStr = "字符串".intern(); }
}

元空间特点:
- 位于本地内存(系统内存),不在JVM堆中
- 默认无大小限制(受系统内存限制)
- 通过
-XX:MaxMetaspaceSize设置上限 - 发生OOM时错误信息:
java.lang.OutOfMemoryError: Metaspace
内存结构对比
JDK 1.7 内存布局:
JVM进程内存
├── 堆内存 (Heap)
│ ├── 年轻代 (Young Generation)
│ ├── 老年代 (Old Generation)
│ └── 永久代 (Permanent Generation) ← 方法区实现
└── 栈内存 (Stack)
JDK 1.8 内存布局:
JVM进程内存
├── 堆内存 (Heap) ← 只有对象实例
│ ├── 年轻代 (Young Generation)
│ └── 老年代 (Old Generation)
├── 元空间 (Metaspace) ← 方法区实现(本地内存)
└── 栈内存 (Stack)
为什么要做这个改变?
- 永久代问题:
-
- 容易OOM,调优困难
- FGC(FGC 就是 Full GC 的缩写,中文叫 全局垃圾回收 或 整堆垃圾回收,FGC 是指一次性回收整个 Java 堆(包括年轻代和老年代)以及方法区/元空间(Metaspace)的垃圾)会回收永久代,但效果不好(FGC 是影响应用稳定性和响应时间的“性能杀手”。
优化的核心目标之一就是:尽可能减少或避免 Full GC 的发生) - 字符串常量池在永久代中,容易内存泄漏
- 元空间优势:
-
- 自动扩展:默认不限制大小
- 垃圾回收改进:单独的垃圾回收机制
- 性能提升:减少Full GC触发
- 内存管理:使用本地内存,更灵活
方法区
- 线程共享的区域,存储的是类信息、静态变量、常量、编译后的代码、运行时常量池
- JDK7方法区的实现叫:永久代,占用的是堆的内存空间,大小固定
为什么图上看起来方法区是在堆外的?明明方法区应该 是在堆内的
1. 永久代 (PermGen) - Java 7及以前
永久代就是在堆里面的
- 物理位置:在JVM堆内存内部划出的一块特定区域。您可以把它想象成堆里的一个“特区”。
- 逻辑归属:因为它在堆里,所以从“JVM进程总内存”这个大逻辑上看,它属于堆。但为了和存放对象实例的“Java堆”区分开,它被称为 “非堆(Non-Heap)”。
- JDK8方法区的实现叫:元空间,占用的是本地内存的空间,大小自动调整
- 直接内存
- 直接内存 不属于JVM内存管理,而是操作系统的内存
- 常见于NIO操作时,用于数据缓冲区
NIO和BIO
1. 传统 I/O:BIO
BIO 指的是 Blocking I/O。
- 工作模式:同步并阻塞。
- 通俗比喻:就像在餐厅里,一个服务员(一个线程)服务一桌客人。服务员从点餐到上菜,必须一直等待这桌客人,直到他们完全吃完。在此期间,这个服务员不能为其他桌服务,即使他大部分时间只是在“等待”。
- 在程序中的体现:当服务器读取数据或写入数据时,如果数据没有准备好,线程会被挂起(阻塞),直到数据就绪。这会导致为每一个客户端连接都需要创建一个独立的线程,当连接数非常多时,线程数量暴涨,消耗大量系统资源,导致性能急剧下降。
2. 新 I/O:NIO
NIO 指的是 New I/O。
- 工作模式:同步但非阻塞。
- 通俗比喻:就像在餐厅里,一个服务员(一个线程)服务所有客人。服务员不停地巡逻(轮询),依次询问每一桌:“需要点餐吗?”、“菜上了吗?”、“需要买单吗?”。如果某桌客人说“还没想好”,服务员就立刻去问下一桌,而不是在原地等待。
- 在程序中的体现:NIO 的核心是 Channel(通道) 和 Selector(选择器)。
-
- Channel:可以设置为非阻塞模式。当没有数据可读或可写时,线程会立即返回做别的事情,而不是被阻塞。
- Selector:一个线程可以管理多个 Channel。Selector 会不断地轮询注册在它上面的 Channel,检查哪些 Channel 已经准备好了(如,有数据可读、可写、连接已建立)。然后,线程只去处理那些已经准备好的 Channel。
- 分配和回收成本较高,但读写性能高,直接内存 Java代码和系统代码都能访问的到
