区域 | 描述 | 线程私有 | 如何溢出 |
---|---|---|---|
程序计数器 | 为了线程切换后能恢复到正确的执行位置,每个线程都要有一个独立的程序计数器。 | ? | 唯一一个不会内存溢出的地方 |
虚拟机栈 | 1. 每个方法执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、方法出口等信息。 2. 每一个方法从调用到执行完毕都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 3. 局部变量表存储了编译期可知的各种Java基本数据类型和对象引用。 | ? | 1. 线程请求的栈深度大于虚拟机所允许的深度时抛出 StackOverFlowError异常。 2. 栈扩容时无法申请到足够内存的时候抛出 OutOfMemoryError。 |
本地方法栈 | 和虚拟机栈类似,本地方法栈是为本地(Native)方法服务的 | ? | 同【虚拟机栈】 |
方法区 | 线程共享,用于存放被虚拟机加载后的类型信息、常量、静态变量、即时编译器编译后的代码缓存数据。 注:运行时常量池、元空间都属于方法区的一部分。 | ? | 无法满足新的内存分配会抛出 OutOfMemoryError |
堆(垃圾收集的主要区域) | 1. 基本上所有的对象都是在堆上分配的。 2. Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。 | ? | 无法满足新的内存分配会抛出 OutOfMemoryError |
直接内存 | NIO通过使用Native函数库直接分配对外内存。不受Java堆大小限制,但是受机器的物理内存限制。 | ? | 无法满足新的内存分配会抛出 OutOfMemoryError |
堆其实就是一大块内存区域,是用来存放对象的,对于一个应用来说最耗费内存的就是“对象”。因为在运行的过程中会创建无数个对象,所以内存回收(垃圾回收)的时候主要就是针对堆的垃圾进行回收。
常见的堆划分是:
上面我们谈到内存空间,内存是有限的,想要健康持续的运行下去,就一定要回收“垃圾”。
那怎么判定一个对象是不是垃圾呢,就成了新的问题。
算法 | 描述 | 备注 |
---|---|---|
引用计数算法 | 当某个对象被引用的时候引用计数器就加一,引用失效时就减一,当没有引用的时候就说明可以被回收了。 | 几乎没有使用它的,因为它无法解决循环依赖的问题。 |
可达性分析 | 某些对象被定义为根(GC Roots),从GC Roots向下搜索的路径成为“引用链”,如果某个对象到GC Roots间没有任何引用,那说明它不可达,它就可以被回收了。 | 目前都是用这种算法。 |
GC Roots并不是一个固定的对象,它是一组对象:
一个对象是否可以被回收,是要看有没有被GC Roots触达,而不是仅仅是 触达
不管是引用计数算法,还是可达性分析,都提到了引用。Java中的引用并不是简单的引用,它有四种不同的引用
引用类型 | 描述 |
---|---|
强引用 | 被强引用的对象不会被回收。 new 的方式创建就是产生强引用。 |
软引用 | 被软应用的对象,只有在内存不足的时候才会被回收。 |
弱引用 | 被弱引用的对象,在下一次垃圾回收的时候就会被回收。 |
虚引用 | 虚引用又被称为幽灵引用,它和回收没太大关系,只是在回收的时候,会收到一个系统的通知。 |
名称 | 描述 | 优缺点 |
---|---|---|
标记-清除 | 标记所有未被引用的对象,在GC的时候清空它们。 | 优点:简单直观 缺点:会产生大量的内存碎片。如果下次需要分配一个大对象,没有连续空间的时候会提前触发GC。 |
标记-整理 | 标记所有被引用的对象,将还存活的对象移动到一端,然后清除边界外的内存。 | 优点:减少了内存碎片,相对于标记-清除减少了碎片化问题。 缺点:移动对象需要成本。 |
标记-复制 | 将内存划分成两个相同大小的块,每次只使用其中一块。当一块的内存用完了,就将还存活的对象复制到另外一块,然后再把之前那块空间清空。 IBM公司有一项研究的结论是:新生代中98%的对象熬不过第一轮回收,所以不必按照 1:1 的比例来划分。 新生代分为三个区:一个Eden、两个Survivor,对象优先分配在Eden区,每次只使用Eden和一个Survivor,在垃圾回收的时候把还存活的对象移动到另外一个没有被使用的Survivor中。如果Survivor区空间不够,会把对象移动到老年代。 注 1. 两个Survivor,在有的地方被称为From和 To,或 S0、S1 2. 默认情况下Eden和两个Survivor的比例是 8:1:1 | 优点:减少了内存碎片,适用于对象生命周期短的场景。 缺点:空间浪费和复制成本。 |
回收算法是理论,回收器是实践,不同回收器都是基于理论进行真正的实践,在讨论回收器之前需要先了解下面几个点
下面是各个回收器的作用域,连线表示它们可以组合使用,红色线表示JDK9已经不推荐了。
名称 | 描述 | 效率 | STW | 回收算法 | 作用域 | 目标 | 使用场景 | 回收步骤 |
---|---|---|---|---|---|---|---|---|
Serial | 它是单线程工作的,且在进行垃圾回收的时候,必须暂停所有的工作线程。 | 串行 | 是 | 标记-复制 | 新生代 | 快速的回收 | 1. 在客户端模式下的默认新生代收集器。 2. 对于内存资源不多的情况下,它是所有收集器里额外内存消耗最小的。 | |
Serial Old | 同上 | 串行 | 是 | 标记-整理 | 老年代 | 快速的回收 | 同上 | 同上 |
ParNew | 它支持多线程并行回收垃圾,其它与Serial收集器没什么大的差别。 | 并行 | 是 | 标记-复制 | 新生代 | 快速的回收 | 是除了Serial之外唯一可以和CMS收集器配合工作的。 | |
Parallel Scavenge | 它和ParNew有很多相似的地方,不同的是它关注的是 达到一个可控制的吞吐量。 | 并行 | 是 | 标记-复制 | 新生代 | 提高吞吐量 | 大规模的后台服务、批处理任务等,对吞吐量要求高的场景。 | 同上 |
Parallel Old | 同上 | 并行 | 是 | 标记-整理 | 老年代 | 提高吞吐量 | 同上 | 同上 |
CMS | CMS可以并发的去执行,并且可以部分STW的回收器。 | 并发 | 部分STW | 标记-清除 | 老年代 | 快速的回收 | 对延迟敏感的应用,需要较短垃圾回收停顿时间。 | |
G1 | 1. G1将堆分成多个大小相同的Regin(大小在 1-32MB,默认是 2048个),每一个Regin都可以充当新生代或老年代中的某个区。 2. Regin中还有一个特殊的区域 Humongous Regin(大对象直接分在老年代,防止了反复拷贝移动) ,G1规定大小超过普通Regin一半的对象是大对象,大对象就存在Humongous Regin,它会独占一个、或多个连续Region。 3. 使用 Mixed GC 回收(下面讲) | 并发 | 部分STW | 标记-整理 | 通吃 | 满足高吞吐量的同时,尽可能地减少垃圾回收耗时 | 大内存应用、需要可预测停顿时间的应用,JDK9开始成为默认的垃圾回收器。 | G1对于CMS并不是完全的碾压,G1的实现更加复杂,所以它所额外使用的内存和程序运行时的负载都比CMS高。 |
CMS和G1的对比
MinorGC、MajorGC、Full GC、Mixed GC
注:G1并不是终点,后面还有ZGC它关注更低的延迟,但现在大家都还没用到,暂时先不去学习了
参考: