垃圾回收
Java 语言中一个显著的特点就是引入了内存自动回收机制,使 c++ 程序员最头疼的内存管理的问题迎刃而解,它使得 java 程序员在编写程序的时候不再考虑内存管理(委托给 JVM 管理)。由于有个垃圾回收机制, java 中的对象弱化了作用域的概念,只有对象的引用才有作用域。垃圾回收可以有效防止内存泄露,有效地使用空闲的内存。
程序计数器、 虚拟机栈、 本地方法栈 3 个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。 每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。 而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
垃圾回收原理
Java 语言规范没有明确地说明 JVM 使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做 2 件基本的事情:
( 1 )发现无用信息对象;
( 2 )回收被无用对象占用的内存空间,使该空间可被程序再次使用。
引用计数法
概述:引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为 1 。当任何其它变量被赋值为这个对象的引用时,计数加 1 ( a = b, 则 b 引用的对象实例的计数器 +1 ),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减 1 。任何引用计数器为 0 的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减 1 。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为 0 。
可达性分析
在主流的商用程序语言中 (Java 和 C#) ,都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链( Reference Chain ),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
在 Java 语言中,可作为 GC Roots 的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中 JNI (即一般说的 Native 方法)引用的对象。
注意:
即使在可达性分析算法中不可达的对象,也并非是 “ 非死不可 ” 的,这时它处于 “ 缓刑 ” 阶段,要正真宣告一个对象 “ 死亡 ” ,至少要经历两次标记过程。如果对象在经历可达性分析后发现没有与 GC roots 的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize() 方法或者 finalize() 方法已经被虚拟机调用过,则虚拟机将这两种情况视为 “ 没有必要执行 ” 。如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放到一个叫做 F-Queue 的队列中,并在稍后在一个由虚拟机自动建立的低优先级的 Finalizer 线程负责运行,但是虚拟机并不 “ 承诺 ” 会等待它执行结束。 finalize() 方法是对象逃脱死亡命运的最后一次机会。
很多人认为方法区(或者 HotSpot 虚拟机中的永久代)是没有垃圾收集的, java 虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集。但是, GC 仍然会对方法区进行垃圾收集,收集的内容包括无用的类和废弃常量。
垃圾回收算法
标记 - 清除算法
先标记出哪些对象需要回收,再做清理。没啥突出优点,缺点是,清除后会产生大量的内存碎片。具体如下:
标记 - 整理算法
先标记,再把所有存活的对象向一端移动,然后直接清理端边界意外的内存。具体如下:
复制算法
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于 copying 算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面 ( 使得活动对象所占的内存之间没有空闲洞 ) ,这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于 coping 算法的垃圾回收是 stop-and-copy 算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
分代回收算法
现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是前面算法的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记 - 清理算法或者标记 - 整理算法。
垃圾回收器
垃圾收集器就是上面讲的理论知识的具体实现了。不同虚拟机所提供的垃圾收集器可能会有很大差别, JDK7/8 后, HotSpot 虚拟机所有收集器及组合(连线),如下图:
ZGC
在 JDK 11 当中,加入了实验性质的 ZGC 。它的回收耗时平均不到 2 毫秒。它是一款低停顿高并发的收集器。 ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么其他阶段是怎么做到可以并发执行的呢? ZGC 主要新增了两项技术,一个是着色指针 Colored Pointer ,另一个是读屏障 Load Barrier 。 ZGC 是一个并发、基于区域( region )、增量式压缩的收集器。 Stop-The-World 阶段只会在根对象扫描( root scanning )阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增加。
ZGC 的设计目标:
TB 级别的堆内存管理;
最大 GC Pause 不高于 10ms ;
最大的吞吐率( Throughput )损耗不高于 15% ;
关键点: GC Pause( 停顿 ) 不会随着 堆大小的增加 而增大。
说明:
ZGC 虽然目前还在 JDK 11 还在实验阶段,但由于算法与思想是一个非常大的提升,相信在未来不久会成为主流的 GC 收集器使用。 ZGC 在 jdk11 支持, ZGC 目前仅适用于 Linux / x64 。和 G1 开启很像,用下面参数即可开启:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
关键技术:
着色指针 (Colored Pointer) : ZGC 利用指针的 64 位中的几位表示 Finalizable 、 Remapped 、 Marked1 、 Marked0 ( ZGC 仅支持 64 位平台),以标记该指向内存的存储状态。相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于 Java 术语当中的引用。在这个被指向的内存发生变化的时候(内存在 Compact 被移动时),颜色就会发生变化。
读屏障 (Load Barrier) :由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。把这两项技术联合下理解,引用 R 大( RednaxelaFX )的话,与标记对象的传统算法相比, ZGC 在指针上做标记,在访问指针时加入 Load Barrier (读屏障),比如当对象正被 GC 移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与 GC 一致而粗暴整体的 Stop The World 。
漫谈至此,你对 JAVA 的内存机制是否更了解了呢?
原文 http://rdc.hundsun.com/portal/article/955.html