JVM 内存区域
首先,了解Java GC过程之前,要简单了解JVM的内存区域都有哪些
Java中,JVM主要分为五个部分:线程共享的(java堆、方法区),线程私有的(本地方法栈、Java栈、程序计数器)
Java堆(运行时数据区)
创建的对象、数组保存在java堆中,同时也是GC的主要区域
方法区(永久代)
用来存储java类加载信息、常量、静态变量、即时编译后的代码
- 运行时常量池 是方法区的一部分
- 永久代已经在Java8中移除,被“元数据区”取代
本地方法栈
主要用来服务非Java代码的程序(Native方法)
Java栈(虚拟机栈)
java方法执行的内存模型,每个方法执行时都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法的开始与结束,对应Java栈的入栈和出栈过程
程序计数器
当前线程字节码行号指示器,Java中唯一没有OOM异常的区域
Java GC 方法
JavaGC主要发生在Java堆中,目前比较常用是GC方法是分代收集法。
分代收集法
Java堆保存了所有创建的对象、数组。而这些新建的对象中,会经常产生无用的垃圾。JVM采用分代收集法将Java堆分成两部分 ---- 新生代和老年代
新生代
Java堆中的1/3为新生代,新生代中又分为三个区域:Eden区、SurvivorFrom区、SurvivorTo区。通常情况下,Eden区的大小是最大的
当一个对象创建时,会进入Eden区(如果对象较大将直接进入老年代),如果Eden区已满,会触发一次MinorGC
MinorGC
MinorGC采用了复制算法
MinorGC经历了:复制 -> 清空 -> 互换 的过程
(具体如何清除将在GC垃圾收集器提到、这里只说明MinorGC的过程)
- 首先,把Eden区和SurvivorFrom区的存活对象(已清理垃圾)复制到SurvivorTo区,并将存活对象的年龄 + 1 (通常年龄到15的对象直接移入老年代)
- 将Eden区和SurvivorFrom区清空
- SurvivorFrom区与SurvivorTo区互换,原SurvivorTo区将成为下一次GC的SurvivorFrom区
老年代
Java堆中的2/3为老年代,主要存放一些生命周期较长的对象。因为老年代对象相对比较稳定,所以不会频繁触发GC操作,当老年代空间不足时,将触发MajorGC以腾出空间。当老年代没有多余空间且无法通过MajorGC回收时,程序抛出OOM异常。
- MajorGC采用标记清除算法(具体实现将在GC算法中提到)
永久代(Java8已废弃)
内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在加载的时候被放入永久区域。GC不会在主程序运行期间对永久代进行清理。所以这也导致了永久代会随着加载的Class文件增多而胀满,抛出OOM异常
分区收集法
分区算法将整个堆空间划分为连续不同的小区间,每个小区间独立使用,独立回收,这样做的好处是可以控制一次回收多少个小区间,根据目标停顿时间合理的回收若干个小区间(而不是整个堆),从而减少一次GC的停顿。
- 本文主要介绍分代收集法
Java GC 算法
如何确定垃圾?
引用计数法
在Java中,引用和计数是有关联的,要操作对象则必须用引用进行。因此,很显然,一个简单的办法是通过引用计数来判断一个对象是否可以回收。即一个对象如果没有任何与之关联的引用,则他们的引用计数为0,说明该对象不太可能会被用到,那么这个对象就是可回收对象
可达性分析
为了解决引用计数法的循环引用问题,Java使用了可达性分析方法。通过一系列“GC roots”对象作为起点搜索。如果在“”“GC roots”和一个对象之间没有可达路径,则该对象就是不可达的。
- 不可达对象不代表就一定会被回收,至少要经过两次标记过程。若两次标记过程后依然不可达,该对象将被回收
如何清除垃圾?
标记清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段:标记 -> 清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间
从图中可知,该算法的最大问题是内存碎片化严重,后续可能造成较大的对象找不到可用空间的问题。
复制算法(Copying)
为解决Mark-Sweep算法内存碎片化严重的问题,提出了复制算法的思想。按内存容量将内存划分为等大小的两块。每次只用其中一块,当一块内存满了以后,将存活对象复制到另一块内存区域上,把已使用的内存清理掉。如图:
这种算法虽然简单,内存效率高,不易产生碎片,但最大的问题是可用内存被压缩到了原来的一半。且存活对象增多的话,Copying效率会大大降低。
标记整理算法(Mark-Compact)
结合以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同。标记后不清楚对象,而是将存活对象移向内存的一段。然后清除端边界外的对象。如图:
Java GC 垃圾收集器
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法; 年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不 同的垃圾收集器,JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
Serial垃圾收集器(单线程、复制算法)
Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是 JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Seria l 垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
ParNew垃圾收集器(Serial + 多线程)
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过 -XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】 ParNew 虽然是除了多线程外和 Serial 收集器几乎完全一样,但是 ParNew 垃圾收集器是很多 java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
Parallel Scavenge收集器(多线程赋值算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
Serial Old(单线程标记整理算法)
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法, 这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。 在 Server 模式下,主要有两个用途: 1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。 2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。 新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:
新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似,都是多线程的收集器,都使 用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。新生代 Parallel Scavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:
Parallel Old(多线程标记整理算法)
Parallel Old 收集器是 Parallel Scavenge 的年老代版本,使用多线程的标记-整理算法,在 JDK1.6 才开始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。 新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:
CMS收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。 CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并 发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看 CMS 收集器的内存回收和用户线程是一起并发地执行。 CMS 收集器工作过程:
G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是: 1. 基于标记-整理算法,不产生内存碎片。 2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
附:Java四种引用类型
强引用
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之 一。
软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。