深刻理解 JVM 之 垃圾回收机制

虽然内存的分配和回收技术已至关成熟,但若是须要排查内存溢出、内存泄露问题,或者要求高并发、高性能时,就须要对垃圾的回收进行监控和调节,以更好优化系统提升性能。html

对象存活断定

Java 内存结构中,程序计数器、虚拟机栈、本地方法栈等随着线程而生,随线程而灭,不须要考虑内存回收问题。而 Java 堆和方法区则不一样,它们的内存分配是动态的,只有在运行期间才能知道会建立哪些对象,垃圾回收关注的就是这两部分。java

垃圾回收首先须要判断哪些对象还存活着,主要有引用计数和可达性分析两种算法。算法

引用计数算法

它的原理以下:给对象添加一个引用计数器,每当有一个地方引用它时,计时器值就加 1;当引用失效时,计数器值就减 1;若是计数器为 0,对象就不可能再被使用。数组

引用计数算法虽然实现简单、断定效率较高。但它很难解决对象之间循环引用的问题。缓存

例如两个对象相互引用,实际上两个对象都不会再访问,但由于相互引用着对方,致使它们的计数器值都不为 0,因而引用技术算法没法经过 GC 收集器回收它们。并发

可达性分析算法

它的原理以下:经过一系列称为 GC Roots 的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证实对象是不可用的。高并发

Java 中,可做为 GC Roots 的对象包括以下几种:性能

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中 JNI( Native 方法) 引用的对象。

引用类型

能够看到,对象回收断定算法判断对象是否存活都与引用有关。从 JDK1.2 开始,引用分为四种类型,用来实现不一样的功能,它们的引用强度也依次递减。优化

强引用(Strong Reference)线程

平时使用的引用就是强引用。只要强引用还存在,该对象永远不会被回收。

能够经过将对象设置为 null,使其被回收。

软引用(Soft Reference)

用于描述一些还有用但并不是必需的对象。当系统内存空间不足时,会回收这些软引用指向的对象。它经过 SoftReference 类来实现软引用。

能够用来实现高速缓存。

弱引用(Weak Reference)

用来描绘非必需对象。被弱引用指向的对象只能生存到下一次垃圾回收以前。只要垃圾收集器运行,弱引用指向的对象就会被回收。它经过 WeakReference 类来实现弱引用。

虚引用(Phantom Reference)

虚引用和没有引用没有任何区别。一个对象是否有虚引用,不会影响其生存时间,也没法经过虚引用获取对象实例。它经过 PhantomReference 来实现虚引用。必须和引用队列 ReferenceQueue 联合使用。

为一个对象设置虚引用的惟一目的是该对象被垃圾收集器回收前会收到一条系统通知。

回收方法区

方法区,或者说 HotSpot 虚拟机中的永久代,进行垃圾回收的效率通常比较低。回收主要包括两部份内容:废弃常量和无用的类。

判断一个常量是不是废弃常量比较简单,与回收 Java 堆中的对象相似。而断定一个类是不是无用的类须要知足三个条件:

  • 该类全部的实例都已经被回收;
  • 加载该类的 ClassLoader 已经被回收;
  • 该类对象的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

垃圾收集算法

标记-清除算法(Mark-Sweep)

标记-清除算法分为两个标记和清除阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。标记过程也就是对象存活断定算法。

它是最基础的收集算法,主要有两个缺点:

  • 效率问题:标记和清除两个过程的效率都不高。
  • 空间问题:标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大的对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

复制算法(Copying)

复制算法将可用内存分为大小相等的两块,每次只使用其中的一块。在一块内存用完后,将仍存活的对象赋值到另外一块上面,再把已使用过的内存一次清理掉。

复制算法的优缺点以下:

  • 优势:每次对半个分区进行内存回收,内存分配时也不用考虑内存碎片等状况,实现简单,运行高效。
  • 缺点:可以使用的内存缩小为一半,代价较大。

标记-整理算法(Mark-compact)

标记-整理算法分为标记和整理两个阶段,标记阶段和“标记-清除算法”同样,但在整理阶段,不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。

标记-整理算法的优缺点以下:

  • 避免了空间碎片,空间利用率较高。
  • 效率不高,标记和清除过程的效率较低。

分代算法(Generational Collection)

分代算法根据对象存活周期将内存划分为几块。通常是将 Java 对分为新生代和老年代,根据各个年代的特色采用适当的收集算法。

新生代中,每次垃圾收集时只有少许对象存活,选择复制算法;老年代中,对象存活率较高、没有额外空间进行分配,使用“标记-清理”或“标记-整理”算法。

为了对不一样生命周期的对象采用不一样的回收算法,因此垃圾收集器都采用分代收集算法,将堆分为新生代和老年代。

  

内存分配和回收策略

新生代

新生代主要用来存放新建立的对象,通常占堆 1/3 的空间。因为不少对象生命周期很短,每次 Minor GC 后只有少许对象存活,因此选用复制算法。

新生代又被分为一块较大的 Eden 区和两块较小的大小相等的 Survivor 区,使用 fromto 来分别指代两个 Survivor 区。HotSpot 虚拟机默认 Eden 和两块 Survivor 的大小比例为 8:1:1。每次只会使用 Eden 和其中一块 Survivor 区为对象服务,因此老是有一块 Survivor 区是空闲的,新生代实际可用的内存空间也就为 90%

一般,对象会分配在 Eden 区中,当 Eden 区没法在分配对象时,JVM 便会触发一次 Minor GC,将存活下来的对象复制到 from 指向的 Survivor 区中。

from 指向的 Survivor 区也没法分配时,对 Edenfrom 指向的 Survivor 区执行 Minor GC,将存活下来的对象复制到 to 指向的 Survivor 区中,而后交换 fromto 指针,使 to 指向的 Survivor 区为空,以保证下次 Minor GC 有复制的空闲空间。

老年代

老年代用于存放大对象,或年龄超过必定程度的对象。通常占据堆 2/3 的空间。

若是对象须要大量连续的内存空间,例如很长的字符串及数组,这些对象会直接分配在老年代,以免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。

虚拟机为每一个对象定义了一个对象年龄计数器,若是对象分配在 Eden 区,在通过一次 Minor GC 后仍然存活,以后移动到 Survivor 空间中,将其年龄设置为 1。对象在 Survivor 区中每通过一次 Minor GC,年龄就增长一次,当它的年龄增长到必定程度(默认为 15)时,也会被晋升到老年代中。

若是在 Survivor 区中相同年龄全部对象大小的总和大于 Survivor 区的一半,年龄大于或等于该年龄的对象就能够直接进入老年代。

老年代的对象通常都比较稳定,Major GC 不会频繁执行。Major GC 采用标记—清除算法:首先扫描一次全部老年代,标记出存活的对象,而后回收没有标记的对象。MajorGC 的耗时较长,并且会产生内存碎片。

三种清理方式

Minor GC(Young GC)

指发生在新生代的垃圾收集动做。当 Eden 区没有足够的空间分配时,就会触发一次 Minor GC。因为 Java 对象大多生命周期较短,因此 Minor GC 很是频繁,通常回收速度也比较快。

Major GC

指发生在老年代的垃圾收集动做,在进行 Major GC 前,通常都会进行至少一次 Minor GCMajor GC 的速度通常会比 Minor GC10 倍以上。

Full GC

指回收整个新生代和老年代的垃圾收集动做。成本较高,对系统性能产生影响。FULL GC 的时候会 STOP THE WORD

它的触发条件主要有:

  • 在执行 Minor GC 以前,若是老年代最大可用的连续空间小于历次晋升到老生代对象的平均大小,则触发一次 Full GC
  • 大对象直接进入老年代,或从年轻代晋升上来的老对象,在老年代尝试分配内存,但老年代内存空间不够时。
  • 显式调用 System.gc() 方法时。

参考资料

相关文章
相关标签/搜索