自动内存管理
文章目录
- 自动内存管理
- Java 内存区域与内存溢出异常
- 运行时数据区域
- 垃圾收集器与内存分配策略
- 怎么判断对象是否存活?
- 垃圾回收算法
- 经典的垃圾收集器
Java 内存区域与内存溢出异常
运行时数据区域
-
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
-
-
程序计数器:
- 程序计数器(PC寄存器)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 由于每个线程需要执行的字节码指令都不一样,所以程序计数器也是线程隔离的,各条线程之间计数器互不影响。
- 程序计数器不会发生内存溢出(OOM)。
-
Java 虚拟机栈:
- 每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 每个栈帧的局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用等。局部变量表所需的内存空间在编译器完成分配。
-
本地方法栈:
- 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
-
Java 堆:
- 堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用 new 关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间。
- Java 堆又分为 年轻代 和 老年代,年轻代 又分为 伊甸园 和 幸存区,幸存区又分为 From Survivor 空间和 To Survivor空间。
- 年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发 Minor GC,清理年轻代内存空间。
- 老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次 GC 后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发 Full GC,Full GC 是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OOM异常。
-
方法区:
- 方法区同 Java 堆一样是被所有线程共享的,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。更具体的说,静态变量、常量、类信息(版本、方法、字段等)+ 运行时常量池存在方法区中。常量池是方法区的一部分。
- 在 HotSpot 虚拟机,就会常常提到 永久代 这个词。HotShot 虚拟机在 JDK 8 前用永久代实现了方法区,而很多其他厂商的虚拟机是没有永久代这个概念的。在JDK8中,已经用元空间来替代了永久代作为方法区的实现了,元空间存储不在虚拟机中,而是使用本地内存,JVM不会再出现方法区的内存溢出,以往永久代经常因为内存不够而导致OOM异常。
- 方法区主要是用来存放已被虚拟机加载的类相关信息:包括类信息、常量池
- 类信息又包括了类的版本、字段、方法、接口和父类等信息
- 常量池又分为静态常量池和运行时常量池
- 静态常量池主要存储的是字面量和符号引用等信息,静态常量池包括了我们说的字符串常量池
- 运行时常量池存储的是类加载时生成的直接引用等信息
- 从逻辑角度而言常量池属于方法区的,但自从JDK7以后,就已经把运行时常量池和静态常量池转移到了堆内存中进行存储,所以从物理分区角度来说,运行时常量池和静态常量池就属于堆。
-
运行时常量池:
- 运行时常量池是方法区的一部分。Calss 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
-
直接内存:
- 在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
垃圾收集器与内存分配策略
怎么判断对象是否存活?
- 引用计数算法
- 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
- 客观的说,引用计数算法虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下都是一个不错的算法。也有一些比较著名的应用案例,例如微软COM技术、FlashPlayer、Python等都使用了引用计数算法进行内存管理。
- 缺陷:在 Java 中并没有虚拟机使用该算法来管理内存,它很难解决对象之间相互循环引用的问题。
- 可达性分析算法
- 当前主流的商用程序语言的内存管理子系统,都是通过可达性分析算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
- 在 Java 技术体系中,固定可以作为 GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中 JNI 引用的对象
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象,还有系统类加载器。
- 所有被同步锁持有的对象
- 四种引用
- 强引用
- 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
- 可以配合引用队列来释放引用自身
- 弱引用
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放引用自身
- 虚引用
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
- 终结器引用
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 是才能回收被引用对象
- 强引用
垃圾回收算法
-
分代收集理论
- 1)弱分代假说:绝大多数对象都是朝生夕灭的
- 2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数
- 收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是区标记那些大量将要回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一起,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
- 名词解释:
- 部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集,其中又分为
- 新生代收集(Minor GC / Young GC):指目标指示新生代的垃圾收集。
- 老年代收集(Major GC / Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
- 部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集,其中又分为
-
标记 - 清除算法
- 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
- 缺点:
- 执行效率不稳定,如果堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除动作,导致标记和清除两个过程的执行效率都随着对象数量的增长而降低
- 内存空间的碎片化问题,标记、清除之后产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
标记 - 复制算法
-
为了解决标记 - 清除 算法面对大量可回收对象时执行效率低的问题,1969年 Fenichel 提出了一种“半区复制”的垃圾收集算法,它将可用内容按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为原来的一半,空间浪费未免太多了一些。
-
-
半区复制的优化:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotShot 虚拟机默认 Eden(伊甸园) 和 Survivor(幸存区)的大小比例是 8 : 1,每次最多浪费一个 Survivor 的空间。老年代对 Survivor 进行分配担保,如果发生一次 Minor GC 后 Survivor 不足以容纳存活的对象时,直接晋升老年代。
-
-
标记 - 整理算法
- 标记 - 复制 算法在对象存活率较高时就要进行较多的复制操作,效率会降低。
- 标记 - 整理 算法与 标记 - 清除 一样,但后续步骤不是直接对可回收的对象进行清除,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
-
总结:
- HotShot 虚拟机里面关注吞吐量的 Parallel Scavenge 收集器是基于 标记 - 整理 算法的,而关注延迟的 CMS 收集器则是基于 标记 - 清除 算法的。
- CMS 面临空间碎片过多时的做法是:平时多数时间都采用标记 - 清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次。
经典的垃圾收集器
-
Serial 收集器
- Serial 是一款单线程的垃圾回收器,在进行垃圾回收的时候会发生 Stop the world
- 对于其他来及收集器而言,它的内存占用最小
- 它是 HotSpot 虚拟机运行在客户端模式下的默认新生代收集器
-
ParNew 收集器
- ParNew 实际上是 Serial 收集器的并行版本
- ParNew 收集器是激活 CMS 后的默认新生代收集器
-
Parallel Scavenge 收集器
- 新生代收集器,注重吞吐量优先
- 主要适合在后台运算而不需要太多交互的分析任务
-
Serial Old 收集器
- Serial 的老年代版本,它同样是一个单线程收集器,使用 标记 - 整理 算法
- Serial Old收集器是运行在 Client 模式下的默认老年代回收器;在 Sever 模式下主要有两个用途:
- 1)与新生代的 Parallel Scavenge 配合使用
- 2)作为老年代CMS收集器的后备GC方案
-
Parallel Old 收集器
- Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记–整理算法实现。在JDK6之前,Parallel Scavenge收集器一直处于非常尴尬的状态,原因是如果新生代选择Parallel Scavenge,而老年代能够选择只有Serial Old收集器。但是由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器不一定能够在整体上面获得最大吞吐量最大化的效果。 Parallel Old收集器出现很好的改善了这一情况,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器的组合
-
CMS 收集器
- CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者基于浏览器的B/S系统的服务端上,这类应用通常都较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。CMS是基于标记–清除算法的。
- CMS 的运行步骤:
- 初始标记:该阶段仅仅只是标记一下GC Roots能直接关联到对象, 速度很快。STW
- 并发标记:该阶段就是从GC Roots的直接关联对象开始遍历整个对象的过程,这个过程虽然耗时比较长但是不需要停顿用户线程,可以和垃圾收集线程一起并发执行。
- 重新标记:该阶段是为了修正并发标记期间,因为用户线程继续运作而导致的标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也比并发标记的时间短。STW
- 并发清除:该阶段清理删除标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
- 优点:并发收集、低延迟
- 缺点:
- 会产生内存碎片
- STW
- 无法处理“浮动垃圾”
-
Garbage First 收集器
- G1 是一款主要面向服务端应用的垃圾收集器。
- G1 可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的 Mixed GC 模式。
- G1 收集器虽然也是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有着非常明显的差异。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 对划分为多个大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden空间、Survior空间、或者老年代区域。
- G1 收集器对于垃圾的具体收集思路是跟踪各个 Region 里面的垃圾堆积的“价值”大小,价值即回收所需的空间大小以及回收所需要时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定的收集停顿时间,优先处理回收价值收益最大哪些Region,这也是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了 G1 收集器在有限的时间内获取尽可能高的收集效率。
- G1 的处理过程:
- 初始标记:仅仅是标记 GC Roots 能直接关联的对象,速度很快。stop the world
- 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里面的对象图,找出要回收的对象,这阶段耗时长,但可与用户程序并发执行。
- 最终标记:对用户线程另一个短暂的暂停,用于处理并发阶段结束后人遗留下来的那最后少量的 SATB记录,即修正在并发标记阶段因为用户线程继续运行而导致标记记录产生变动的那一部分对象标记记录。stop the world
- 筛选标记:负责更新 Region 的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划。这阶段停顿用户。stop the world
- 优点:
- 与其他GC收集器相比,G1使用了全新的分区算法
- 并行性:G1 回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW
- 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会再整个回收阶段发生完全阻塞应用进程的情况。
- 分代收集:
- 从分代上来看,G1 仍然属于分代型垃圾回收期,但是从堆结构上来看,它不要求整个 新生代、老年代都是连续的,也不再坚持固定大小和固定数量。
- 将堆空间分为若干区域,这些区域包含逻辑上的年轻代和老年代。
- 与其他几种经典的垃圾回收器不同,它兼顾了年轻代和老年代。
- 空间整合:
- CMS :使用 标记 - 清除 算法,会有内存碎片、进行若干次GC 后进行一次碎片整理。
- G1 :将内存划分为一个各个的Region,内存回收以 Region 作为基本单位。Region 之间使用复制算法(如从 Eden 区复制到 Survivor区),但整体上实际可看作 标记 - 整理 算法,两种算法都可以避免内存碎片,因此有利于程序长时间运行,分配大对象是不会因为无法找到连续内存空间而提前触发下一次GC,尤其当堆非常大时,G1 优势更明显。
- 可预测的停顿时间模型:
- 每次根据允许的收集时间,优先回收价值最大的 Region,保证了G1 收集器在有限时间内可以获取尽可能高的收集效率。
- 缺点:
- 每次根据允许的收集时间,优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
-
七种垃圾回收器的比较
-
垃圾收集器 分类 作用位置 使用算法 特点 使用场景 Serial 串行运行 新生代 标记 - 复制 响应速度优先 单CPU环境下的Client模式 ParNew 并行运行 新生代 标记 - 复制 响应速度优先 多CPU环境下 Server 模式下与 CMS 配合使用 Parallel 并行运行 新生代 标记 - 复制 吞吐量优先 后台运算而不需要太多交互的场景 Serial Old 串行运行 老年代 标记 - 整理 响应速度优先 单CPU环境下的Client模式 Parallel Old 并行运行 老年代 标记 - 整理 吞吐量优先 后台运算而不需要太多交互的场景 CMS 并发运行 老年代 标记 - 清除 响应速度优先 互联网或者 B/S 业务 G1 并发、并行 新生代、老年代 标记 - 整理、标记 - 复制 响应速度优先 面向服务端应用
-