前言
有输入肯定要有输出。
内存区域划分
- 程序计数器
唯一一个没有规定 OOME 的地方。执行 Java 方法时记录当前方法的 JVM 指令地址;执行 Native 方法时,计数器为空。 - Java 虚拟机栈
- 本地方法栈
- 堆
内存管理的核心区域,分为新生代和老年代,再细致一点分为 Eden 空间、From Survivor、To Survivor 等 - 方法区
所有线程共享的内存区域,存储元数据,如类结构信息、字段、方法等。无法满足内存分配需求时,抛出 OOME。- 运行时常量池
方法区的一部分,存放编译期生成的各种字变量,还有运行时的符号引用等。例如 String.intern() 后放置的地方。
- 运行时常量池
- 直接内存
JDK 1.4 引入 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。 - Code Cache
JIT Compiler 在运行时对热点方法的编译;GC 运行时部分需要占用的空间等,但 JVM 规范中并没有涉及。
OutOfMemoryError
堆溢出
不断创建对象并且无法回收
栈溢出
在单线程下,当内存无法分配时,虚拟机抛出通常是 StackOverflowError 异常。
方法区运行时常量池溢出
JVM 对于方法区的回收非常不积极,例如老版本的 JDK 处理 String.Intern() 时占用太多空间。随着元数据区的移除永久代,类元数据只受本地内存影响。
本机直接内存溢出
垃圾回收不会主动收集 Direct Buffer,需要自己手动调 System.gc()。另外。不会在 Heap Dump 中看见。
垃圾回收
判断对象是否存活,引用计数法很难解决循环引用的问题,所以才有了可达性分析
GC Roots 的对象包括以下几种:
- 虚拟机栈中引用的对象
- 方法去类静态属性引用的对象
- 方法去常量引用的对象
- Native 方法引用的对象
可达性分析算法
至少两次标记过程,第一次标记筛选出有必要执行的 finalize() 方法的对象,对象可在 finalize() 中自救一次,因为 finalize() 方法只会被系统自动调用一次。
回收算法
- 标记-清除算法
效率不高,会产生大量不连续的内存碎片 - 复制算法
新生代采用复制算法收集内存,大对象通过分配担保机制进入老年代 - 标记-整理算法
将存活的对象移至一端,减少内存碎片 - 分代收集算法
根据年代特点采用最适当的算法,需要上述算法支持
内存分配
都可通过参数设定
- 对象优先在 Eden 中分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
年龄计数器 - 动态对象年龄判断
Survivor 空间中相同年龄所有对象大小总合大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就直接进入老年代 - 空间分配担保
需要检查老年代的空间是否满足新生代所有对象空间,不够的话还要查看设置的值是否允许冒险,以决定 FullGC 是否有必要
类加载
- 加载
获取字节流,生成对象,提供数据访问入口 - 验证
是否符合虚拟机规范 - 准备
为变量分配值 - 解析
将符号引用替换为直接引用 - 初始化
执行类构造器() 方法