JVM垃圾收集(GC)
文章目录
介绍
当垃圾收集称为系统达到更高并发量的瓶颈时,我们必须对这些“自动化”的技术实施必要的监控和细节,防止各种内存溢出和内存泄漏问题。
运行时区域
虚拟机栈,本地方法栈,程序计数器随线程而生随线程而灭。且每个栈帧中分配多少内存基本上是在类结构确定下来就可知的。不需要我们过多考虑。
Java堆与方法区
这两个区域有很明显的不确定性,一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支需要的内存也不一样,只有处于运行期间,我们才能知道程序会创建哪些对象,创建多少个对象。
如何确定对象已经可以回收了?
引用计数算法
在对象中添加一个引用计数器,有一个地方引用它时,计数器加一;引用失效时,计数器减一,任何时刻计数器为零的对象时不能再使用的。问题是,如果有两个对象A.instance = B,B.instance = A 就会造成循环引用,无法回收。引用计数算法很少有虚拟机会用。
可达性分析算法
这个算法的基本思路是通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索路径为“引用链”,如果某个对象到GC Roots之间没有任何引用链相连,那么这个对象就是可以被回收的。可达性分析算法是目前主流虚拟机使用的算法。
GC Roots
- 在虚拟机栈引用的对象,譬如各个线程被调用的方法使用到的参数,局部变量等
- 在方法区中类静态属性引用的对象,譬如Java类引用类型的静态变量
- 在方法区中常量引用的对象,譬如字符串常量池里的引用
- 本地方法栈中本地方法引用的对象
- 在虚拟机内部的引用,如基本数据类型对应的class对象,一些常驻的异常对象如NullPointerException,OutOfMemoryError等,还有系统类加载器
- 所有被同步锁(synchronized)持有的对象
- 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
几种引用
- 强引用: 像Obj A = new Obj();这种引用,只要强引用关系还在,垃圾收集器永远不会回收
- 软引用:用SoftReference实现软引用。在系统将要发生内存溢出前,会对这类对象进行回收
- 弱引用:用WeakReference实现,当只有弱引用关联时,无论当前内存是否足够都会回收
- 虚引用:使用PhantomReference类来实现。唯一目的是为了能在某个对象被回收时收到一个系统通知
生存还是死亡?
即使是在GC Roots中没有引用到的对象,也不一定会被回收,至少要经历两次标记过程。
- 首先如果没有与GC Roots的引用链,将会被第一次标记
- 除非对象在重写的finalize()方法中重新获得了引用,否则将会被第二次标记
垃圾收集算法
标记-清除
首先标记出要回收的对象,在标记完成后统一回收掉所有被标记的对象。优点:效率高;缺点:导致空间碎片化
标记-复制
先标记,然后将所有存活对象都复制到另一个区域,然后清空这个区域
标记-整理
标记、清除、然后整理。会造成Stop the World:先暂停用户程序
分代收集
根据分代特性选择不同垃圾收集方式
Java内存分配机制
这里说的内存分配主要是指堆上的内存分配。
年轻代(Young Generation)
对象被创建时,内存的分配首先发生在年轻代(大对象可能直接进入老年代,如很长的字符串或元素量很大的数组)。年轻代的对象大多朝生夕死,所以年轻代的GC也比较频繁,年轻代的GC也叫Minor GC或Young GC。
年轻代的内存分配
年轻代的内存被按8:1:1的比例分为三个区域,分别为Eden区,和Survivor 0、Survivor 1。
绝大多数刚创建的对象会被分配在Eden区,其中大多数对象很快就会消亡。Eden区满的时候执行Minor GC,将消亡的对象清理,并将存活对象复制到一个存活区域Survivor 0;当Survivor 0也满了的时候,将其中仍然活着的对象直接复制到Survivor 1,以后每次Minor GC后,就将剩余对象添加到Survivor 1(此时0为空白,两个存活区总有一个空白)。当切换了几次之后,分代年龄达到15(可通过参数设置),就会进入老年代。(复制算法)
年老代(Old Generation)
对象如果在年轻代存活了足够长的时间而没有被清理掉(在多次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。
还有就是过大的对象可能会直接分配到老年代上。
永久代(Perm Generation)
Java8中已经被移除,取而代之的是元空间。区别是元空间是在本地实现的。
常见的垃圾收集器
Serial
从单词字面意思理解,它是一个单线程的垃圾收集器,在JDK1.3.1之前是新生代垃圾收集器的唯一选择,它的“单线程”不仅仅代表它会使用一个线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须停止其他工作线程,直到它收集结束(Stop the World)。它是当前HotSpot虚拟机运行在客户端模式下默认的新生代收集器。
- Serial 新生代采用复制算法,暂停所有用户线程。Serial Old老年代采用标记整理算法,同时暂停所有用户线程。
- 缺点:Stop the World是在后台由虚拟机自动发起和结束的,是在用户不知道的情况下把用户正常的工作线程全部停掉。
- 优点:与其他收集器相比简单高效,在所有收集器中内存消耗最小,由于是单线程没有线程交互的开销。
ParNew(Serial的多线程版本)
除了是多线程之外,其余和Serial并没有太多不同。
Parallel Scavenge
Java8默认使用 Parallel Scavenge + Parallel Old。基于标记复制算法实现的一个新生代收集器,也是能并行收集的多线程收集器。
Parallel Old收集器,是它的老年版本,采用标记-整理算法,支持并发收集。
CMS (Concurrent Mark Sweep)
它是一种以获取最短回收停顿时间为目标的收集器。目前很大部分Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常会较为关注服务的响应速度,希望系统停顿时间尽可能短。CMS就非常适合这个需求。
从Mark Sweep可以看出它是基于标记-清除算法的一个支持并发(用户线程和垃圾收集线程可以同时运行)的垃圾收集器。回收过程主要分为四步:
- 初始标记 (Stop The World):初始标记就是仅仅标记一下GC Roots能直接关联到的对象,速度很快,整个过程会Stop The World。
- 并发标记 (Concurrent):并发标记就是从GC Roots的直接关联对象开始遍历整个对象图(一组对象和它们之间的联系)的过程,这个过程耗时较长但是不需要停顿用户线程。
- 重新标记 (Stop The World):修正并发标记期间因用户程序变动而导致标记产生变动的那一部分对象的标记记录,速度比初始标记慢,但是也远比并发标记快。
- 并发清除 (Concurrent):删除标记阶段判断已经死亡的对象。
优点:并发,低停顿。
缺点:
- CMS垃圾收集对处理器资源非常敏感,因为是并发收集所以占用了部分线程
- 由于是标记-清除算法,会产生碎片空间,不利于给大对象分配空间
- 无法处理浮动垃圾:本次垃圾收集在并发标记和并发清除时,程序产生的垃圾无法在本次回收
Garbage First (G1)
面向全堆,而不是像其他收集器一样仅使用于新生代或老年代。
它将连续的Java堆分为若干个大小相等的独立区域(Region),每一个区域都能根据需要去扮演Eden,Servivor或是老年代空间。收集器能对扮演不同角色的Region采用不同策略去处理。
G1将Region看作单次回收的最小单元,即每次回收的大小都是Region的整数倍,这样避免了在整个Java堆中进行全区域的垃圾收集。G1根据每个Region进行回收所能释放的空间以及所需时间,在后台维护一个优先级列表,在用户设定允许的收集停顿时间优先处理回收收益最大的Region,这就是Garbage First名字的由来。
如何减少GC出现的次数
- 对象不用时及时置null
- 少用静态变量
- 尽量使用StringBuffer代替String累加字符串
- 能用基本类型就不用基本类型封装类
- 增加-Xmx的大小等
这就是Garbage First名字的由来。
如何减少GC出现的次数
- 对象不用时及时置null
- 少用静态变量
- 尽量使用StringBuffer代替String累加字符串
- 能用基本类型就不用基本类型封装类
- 增加-Xmx的大小等