程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了,而java堆和方法区则不一样,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
一、判断对象是否存活的方法
1.引用计数法(主流java虚拟机并没有采用这种方式)
所谓引用计数法,就是当引用对象时就+1,当引用失效时,计数器值就-1,任何时候当计数值为0的对象就是不可能在被使用了,为什么java虚拟机不用这个呢,因为这个无法解决对象互相循环引用问题。
例:对象A和对象B
A.instance=B;
B.instance=A;
除此之外就无对对象的操作,而此时无法用引用计数法来进行回收他们。
2.可达性分析算法(在主流的商用程序语言都是通过此来判断对象是否存活的)
算法思想:通过一系列的称为GC Roots对象作为起始点,从这些节点向下搜索,搜索所连接的路径称为引用链,当发现有对象与GC Roots对象无法通过引用链相连,则证明这些对象是不可用的,所以它们将被判定为是可回收的对象。
注意:在Java中,可作为GC Roots对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量与引用的对象。
- 本地方法栈中JNI(一般说的Native方法)引用的对象。
二、再谈引用
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。
- 强引用:类似于Object obj =new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:是描述一类还可用但非必需的对象,在内存还充足时保存,当将要发生内存溢出异常之前,将会对这些对象进行第二次回收,如果回收后还没有足够的内存,才会抛出内存溢出异常。
- 弱引用:非必需对象,在进行垃圾回收时,都会回收掉这些对象。
- 虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例,虚引用唯一目的就是当对象被收集器回收时收到一个通知。
三、生存还是死亡
要真正宣告对象的死亡,要经过两次标记过程;如果对象在进行可达性分析后没有与GC Roots相连接的引用链,那么它将被第一次标记并且进行一次筛选,筛选原则时finalize()方法是否有必要被执行,如果有必要则进入F-Queue队列,再队列中finalize()方法是对象逃脱死亡的最后一次机会,稍后GC会对队列进行第二次标记,如果对象要在finalize()方法中拯救自己——只要重新与引用链上的任意对象关联即可,如果没有关联,则死亡,注意finalize()方法只会被执行一次触发一次。
四、回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
判断一个常类是否是废弃常量比较简单,而判断一个类是否是废弃类则需要同时满足三个条件才能算是“无用的类”:
- 该类的所有实例都已经回收。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
五、垃圾收集算法
标记-清除算法
算法分为标记-清除两个阶段,首先标记出所有需要回收的对象,而在标记完成后统一回收所有被标记的对象。
不足:首先效率问题,效率都不高,其次容易造成内存碎片,当需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
复制算法
为了解决效率问题,复制算法是将内存空间分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象赋值到另一块上,在把这个内存空间清理一次。有点不需要考虑内存碎片问题,高效,缺点内存缩小到原来的一半,代价有点大。
标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率会贬低,有人提出标记-整理算法:标记过程与标记清除算法相同,但后续不是清除,而是将对象向某一边移动,然后清理出端边界以外的内存。
分代收集算法
当前商业虚拟机都采用该方法,该方法一般根据对象存活周期的不同分成几块,java堆一般分成新生代和老年代,新生代朝生夕死,只存在少量的存活对象则采用复制算法,移动少量存活对象,而老年代则存活大部分对象,没有额外空间对它进行分配担保,则采用标记-清理算法或者标记-整理算法进行回收。
六、安全点
如果直接用GC Roots直接找引用链的话需要进行枚举,必然会消耗很多时间,所以采用OopMap的数据结构再特定的位置记录下那些地方存放着对象引用,而这些特定的位置称之为安全点,程序执行时并非再所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
安全点停顿
如何在GC发生时,所有的线程都跑到最近的安全点上再停顿下来。两种方案:抢先式中断和主动式中断。
其中抢先式中断不需要线程的执行代码主动去配合,而是在GC发生时,中断所有线程,判断如果中断点不在安全点则恢复线程,让它跑到安全点中断,现在几乎没有虚拟机采用此方法暂停线程。
而主动式中断思想是建立标志位,当标志位为真时,就将自己挂起,标志位的设置与安全点是重合的。
安全区域
安全点看似已经解决进入GC的问题,但是当线程不执行的时候呢(处于Sleep状态),就无法响应JVM的中断请求,这种时候就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们也可以把Safe Region看做是被扩展了的Safepoint。
七、垃圾收集器
如果说收集方法是内存回收的方法论,而垃圾收集器就是内存回收的具体实现。
如图所示展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以搭配使用。
Serial收集器
是最基本、发展历史最悠久的收集器,属于单线程收集器,当进行收集时必须暂停其他线程,这对很多应用来说是很难接受的。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。
Parallel Scavenge收集器
此收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点时尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
停顿时间越短就越适合用户交互程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器
是Serial收集器的老年代版本。
Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
重点:CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。特别适用于服务的交互服务上。
是基于“标记-清除”算法实现的,整个过程分为4个步骤
- 初始标记
- 并发标记
- 重新标记
- 并发清除
CMS收集器缺点
CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
CMS是一款基于“标记-清除”算法实现的收集器,则会产生大量的空间碎片,将会给大对象分配带来很大麻烦。
G1收集器
是当今收集器技术发展的最前沿成果之一,是一款面向服务端应用的垃圾收集器。
具备以下特点:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
G1收集器的运作大致可划分为以下几个步骤
初始标记
并发标记
最终标记
筛选回收
八、内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的。
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生,哪些对象应放在老年代中。通过Minor GC来进行确定,建立一个对象年龄计数器,每进行一次Minor GC如果对象依然存在,则该对象的年龄增加一岁。当年龄达到某一个阈值时,就进入老年代中。
动态对象年龄判断
为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。