Java相比于C++,其最显著的特点就是不需要我们手动去回收内存,一切交给GC线程在后台自动完成,我们可以专心于代码的逻辑设计,而不用在意内存回收的细节。那么Java是如何进行内存回收的?下面对于堆内存结构和垃圾回收的算法进行总结。
Stop-the-world
除了GC线程之外的其它线程都停止执行,直到GC完成。GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。
如何判断对象是否可以回收
1.引用计数法
通过判断一个对象的引用数量来决定对象是否可以被回收。
引用计数法很难解决对象之间循环引用问题。
2.可达性分析算法
通过判断对象的引用链是否可达来判断对象是否可以被回收。
当一个对象到GC Roots没有任何引用链相连(从GC Roots出发到这个对象不可达),则说明这个对象是不可用的。
可作为GC Roots的对象有以下几种:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中Native方法引用的对象;
垃圾收集算法
标记清除算法
从根集合进行扫描,将存活的对象标记,标记完毕后,扫描整个空间中未标记的对象进行清除。
缺点:
- 效率低下:标记和清除两个过程效率都不高。
- 空间问题:不进行对象移动,产生大量不连续的内存碎片,空间利用率低。
复制算法
将内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这种算法适用于对象存活率低的场景,现在商用的虚拟机都使用这种方法来回收新生代,因为研究发现,新生代每次回收之后基本只有10%的对象存活。
实践中,新生代区域一般分为 eden 和两块 survivor(s0和s1) 区域。
每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
Eden和s0,s1的比例为:8:1:1.
标记整理算法
老年代对象存活率较高,使用复制算法效率比较低,而且还浪费空间,因此需要使用标记整理算法。
标记整理算法第一阶段和标记清除算法类似,第二阶段则不同,标记整理算法会将存活的对象向内存的一端移动,然后清除掉边界以外的所有内存,类似于磁盘整理。
分代收集算法
不通对象的生命周期是不一样的,而不同生命周期的对象位于堆中不同的区域,对于不同区域采用不同的回收策略可以提高JVM的执行效率。
新生代
新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的,而且大部分对象在Eden区中生成。
新生代分为eden,s0,s1,比例为8:1:1,采用复制算法。每次GC,eden和一个survivor区域生存的对象会进入另一个survivor区域,当survivor区域满了之后,存活的对象会进入老年代。
如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等Eden区满了才触发。
老年代
老年代存放的都是一些生命周期较长的对象,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多(大概比例是1:2),当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,因此FullGC发生的频率比较低。
老年代采用标记整理算法。
由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。
永久代
永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。
永久代是Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。
在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。
方法区(method area)只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。
垃圾处理器
Serial收集器(复制算法): 新生代单线程收集器
Serial Old收集器 (标记-整理算法): 老年代单线程收集器
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
内存分配与回收策略
对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
方法区的回收
方法区的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。
回收废弃常量与回收Java堆中的对象非常类似,通过引用判定即可。
判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
加载该类的ClassLoader已经被回收;
该类对应的 java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。