1. JVM内存区域
- 线程共有:堆、元空间
- 线程独有:虚拟机栈、本地方法栈、pc寄存器
1.1. 堆
存储对象实例、数组等。
分为新生代和老年代:
- 新生代:存新创建的对象。分为Eden区、Survivor区(分为From Survivor和To Survivor)。新对象首先在Eden区分配内存,一次Minor GC(新生代垃圾回收)后,Eden区和From Survivor区中存活的对象会被复制到To Survivor 区,然后交换From Survivor和To Survivor的角色。多次Minor GC后还存活的对象会被晋升到老年代。
- 老年代:存放生命周期较长的对象。老年代空间不足时,会触发Major GC(老年代垃圾回收)。
1.1.1. 三种GC
- Minor GC是新生代垃圾回收,也叫YoungGC;
- Major GC是老年代垃圾回收;
- Full GC的范围很广,它包括新生代、老年代和方法区的垃圾回收。
1.1.2.三种GC发生的时机
- Minor GC新生代内存不足时发生。Eden满了,会触发Minor GC,将Eden中存活的对象复制到Survivor;如果Survivor也没有足够的空间,也会触发Minor GC来为新生代腾出可用空间。
- Major GC在老年代内存不足时发生,比如多次Minor GC后,仍存活的对象被晋升到老年代,导致老年代空间不足。
- Full GC发生的情况比较复杂,比如:
- 老年代空间不足。
- 方法区(元空间)空间不足。
- 显示调用 System.gc() 方法(不建议在生产环境中使用)。
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
注意:老年代不足的时候,不一定发生major gc,也不一定发生full gc;发生了major gc或full gc时,老年代也不一定不足。
1.2. 方法区
方法区是JVM规范中规定的一个区域,在jdk1.8前用永久代来实现,jdk1.8后用元空间来实现(元空间使用本地内存)。
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.3. 虚拟机栈
每个线程私有的,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。当一个方法被调用时,会在虚拟机栈中创建一个栈帧,用于存储该方法的局部变量、操作数栈、返回地址等信息。当方法执行完毕后,对应的栈帧会被出栈。
1.4. 本地方法栈
与虚拟机栈类似,也是每个线程私有的,为虚拟机使用到的本地Native方法服务。
1.5. 程序计数器(PC Register)
一个较小的内存空间,是当前线程所执行的字节码的行号指示器。
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
2. JAVA中GC回收的区域
- 堆:包括新生代和老年代,jdk1.8以后,字符串常量池也在堆里。
- 方法区:jdk1.8前是永久代,之后是元空间,元空间使用本地内存。里有运行时常量池。
3. 字符串常量池和运行时常量池的区别
- 字符串常量池:在堆中。存放字符串字面量。
- 运行时常量池:在方法区中。存放类名、方法名、字段名、字面量(字符串字面量之外的其他基本数据类型的字面量,如整数、浮点数)以及符号引用等。
4. GC算法
4.1. 标记-清除(Mark-Sweep)
- 原理:先标记可达对象(遍历1次),再清除未标记对象(再遍历1次)。
- 优缺点:简单,但会产生内存碎片且效率低(需要两次遍历内存空间)。
4.2. 复制算法(Copying)
- 原理:把内存分为两块,将存活对象复制到另一块后清理原块。
- 优缺点:无碎片、效率高(只需要遍历一次),但内存利用率低(只有一半的内存空间可被使用)、操作耗时(需要复制大量的存活对象)。
4.3. 标记-整理算法(Mark-Compact)
- 原理:标记后移动存活对象再清理。
- 优缺点:无碎片,但整理过程较耗时(需标记和整理这两个操作,且在整理时需要移动大量存活对象)。
4.4. 分代收集算法
- 原理:按对象生命周期分新生代和老年代,新生代用“复制”,老年代用“标记清除”或“标记整理”(方法区也是用“标记清除”或“标记整理”)。
- 优缺点:整体效率高,但较复杂。
类比一下来记忆:
- 标记清除:像清理杂乱仓库,先标记有用物品,再清理没用的,会留下不规整空间(内存碎片)。
- 复制:两个房间搬物品,只搬有用的到另一个房间后,清空原房间,无碎片但浪费一半空间。
- 标记整理:像清理杂乱仓库,把有用物品放到一起,再清理空出来的地方,无碎片但整理过程麻烦。
- 分代收集:像家里,小孩房间(新生代用复制算法)和大人房间(老年代用标记清除或整理算法),用不同清理方式。
5. 垃圾回收器
5.1. Serial和Serial Old
单线程回收,简单,适用于小型应用和客户端程序。
垃圾回收时,会暂停所有用户线程(Stop The World),就它自己的一个线程在跑。
- Serial用在新生代,使用“复制”算法;
- Serial Old用在老年代,使用“标记整理”算法。
5.2. ParNew
Serial的多线程版本,配合老年代的CMS回收器工作,用于对响应时间有一定要求的场景。
进行垃圾回收时,会暂停所有用户线程,使用多个线程进行垃圾回收操作,比Serial的垃圾回收时间短。
- 用于新生代,使用“复制”算法。
5.3. Parallel Scavenge和Parallel Old
多线程回收,以高吞吐量为目标,适合后台运算任务,如批量数据处理任务。它的目标是达到一个可控制的吞吐量,即运行用户代码的时间与垃圾回收时间的比例。
- Parallel Scavenge用在新生代,使用“复制”算法;
- Parallel Old用在老年代,使用“标记整理”算法。
5.4. CMS
以低停顿时间为目标,采用 “标记清除” 算法,适合互联网等对响应时间要求高的应用。
分四个阶段:初始标记、并发标记、重新标记和并发清除。
- 初始标记和重新标记会暂停用户线程,但这两个阶段耗时很短。
- 并发标记和并发清除可以与用户线程同时进行,减少了垃圾回收的停顿时间。
- 用于老年代,使用“标记清除” 算法。
5.5. G1
面向服务器的垃圾回收器,用于大内存的多处理器计算机,目标是实现低延时垃圾回收。它将堆内存划分为多个大小相等的Region(区域),不再区分年轻代和老年代,而是根据每个Region中对象的存活情况,动态地决定将其回收或复制到其他Region。
G1不需要与其他垃圾回收器配合。
G1回收器提供了两种GC模式:Young GC(年轻代垃圾回收)和Mixed GC(混合垃圾回收,包括年轻代和部分老年代的垃圾回收)。
适用于对停顿时间要求非常短的大型应用程序,如互联网应用、实时交易系统等。它可以在满足短停顿时间的同时,保持较高的吞吐量,能够有效地处理大内存和多处理器环境下的垃圾回收问题。
使用的算法主要是“标记整理”与“复制”算法的混合。
G1 的四个阶段:
- 初始标记:暂停用户线程,快速标记根对象可达的对象,耗时短。
- 并发标记:与用户线程同时进行,标记更多可达对象。
- 最终标记:暂停用户线程,修正并发标记中变动的对象标记。
- 四筛选回收:根据停顿时间目标,对Region进行回收价值排序,采用合适算法回收垃圾多的区域。
5.6. ZGC
是一个低延迟垃圾回收器。特点是停顿时间短,支持TB级堆内存。通过染色指针和读屏障技术,分四个阶段进行垃圾回收,适用于对延迟敏感的高要求场景。
类比记忆这几种垃圾回收器:
- Serial/Serial Old:一个人打扫整个屋子(无论是年轻代还是老年代),一次只做一件事,必须等打扫完一个区域才能进行下一个。一个人独自打扫,虽然慢,但对于小空间来说也能完成任务。适用于小型应用或对停顿时间要求不高的场景。
- ParNew:几个人一起打扫年轻代这个区域,速度比一个人快。可以想象成几个员工专门负责打扫公司的公共办公区域,提高了效率。常和其他老年代回收器配合使用。
- Parallel Scavenge/Parallel Old:以高效完成任务为目标,就像一个专业的清洁团队,在年轻代和老年代分别用不同的策略快速打扫。适合后台运算等对吞吐量要求高的场景,比如工厂的大规模清洁工作,更注重整体的清洁效率而不是瞬间的停顿。
- CMS:努力减少停顿时间,就像在商场营业期间打扫,尽量不影响顾客购物。一边让顾客正常活动,一边快速清理垃圾,适合对响应时间要求高的应用,如互联网应用。
- G1:把屋子分成很多小区域,优先清理垃圾多的区域。适合大内存、复杂场景,比如大型商场可以分区域逐步打扫,不影响整体运营。
- ZGC:超级高效,停顿时间极短,就像一个高科技清洁团队,在几乎不影响人们活动的情况下快速完成打扫。适合对延迟要求极高的场景,如金融交易系统。