JVM中的堆区主要存储的是实例化对象,随着对象的创建以及使用完毕,堆区的内存是经常波动的,所以堆区是垃圾回收GC的主要地方。
1、对象死了吗?
1.1、引用计数算法
对象中存在一个引用数变量,当一个对象被引用时,引用数+1,当引用数为0的时候,说明该对象没用了,就可以回收了。
引用计数算法存在一个缺陷:
互相引用,当两个对象互相引用的时候,两者的引用数都不会等于0,两者都需要等待对方被回收后,引用数才会变为0。二者相互等待。两个对象永远不会回收。
1.2、根搜索算法:
根搜索算法是JVM采用的算法。
如上图所示,从GC Root开始遍历图,遍历到的对象都是存活对象,没有遍历到的就是可回收的对象。遍历到的对象会被标记,如果对象没被标记的话,就说明可以被回收!
GCRoots是什么呢?
1、虚拟机栈中的局部变量表中的引用的对象
2、JNI本地方法栈中的局部变量表中的引用的对象。
3、类中的静态引用对象(static)
4、类中的常量引用对象(final)
强引用、软引用、弱引用、虚引用
强引用:
程序中普遍使用的类型,通过new或者getInstance()等创建的。
User u = new User();
一个对象中只要被强引用引用,哪怕只有一个,这个对象就属于强可达。
强可达对象永远不会被回收,即使发生OOM也不会回收
软引用:
通过SoftReference userSoftReference = new SoftReference(u)来创建,u是一个强引用类型,通过强引用类型来创建软引用,通过userSoftReference.get()来获取引用的对象。
一个对象中不存在强引用,存在弱引用,这个对象属于软可达。
当内存紧张的时候,软可达对象进行回收。将软引用对象加入到ReferenceQueue中,对队列中的软引用对象进行回收!
软引用适用于缓存对象,在内存紧张的时候对其进行回收,防止OOM,同时不影响系统的运行
弱引用:
通过WeakReference userWeakReference = new WeakReference(u)来创建,u是一个强引用类型,通过userWeakReference.get()来获取引用的对象。
一个对象中不存在强引用和软引用,存在弱引用,这个对象属于弱可达。
当进行垃圾回收的时候,即使内存很充足也会对弱可达对象进行回收。
弱引用也适用于缓存对象
虚引用:
通过PhantomReference userPhantomReference = new PhantomReference(u,ReferenceQueue)来创建,虚引用几乎不能获取其引用的对象,也就是通过userPhantomReference.get()获取的对象为null。
一个对象中只存在虚引用,这个对象属于虚可达。
虚引用监督对象回收
1、虚引用创建的时候必须指定一个queue
2、自定义类继承PhantomReference,在自定义类中自定义变量来保存对应的User信息,对PhantomReference对象进行标记。PhantomReference本身也是一个对象啊,只不过PhantomReference对象中包含了User对象的引用。
3、当虚引用对应的对象被回收的时候,会将PhantomReference对象加入到queue中,我们可以开一个线程来监视读取queue来查看哪些对象被回收了,进行业务处理。
4、软引用和弱引用也可以这样做来监督对象回收,做法是一样的。
下面是具体的例子:
public class ReferenceTest {
public static void main(String[] args) throws InterruptedException {
People people = new People();
ReferenceQueue queue = new ReferenceQueue();
PeopleReference reference = new PeopleReference(people,queue);
people = null;
new Thread(){
@Override
public void run() {
while (true){
try {
PeopleReference people1 = null;
if((people1 = (PeopleReference)queue.remove()) != null){
//用来处理业务逻辑
System.out.println("对象回收了:"+people1.name);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
System.gc();
}
}
class People{
public String name = "jaychou";
}
class PeopleReference extends PhantomReference<People>{
String name;
public PeopleReference(People referent, ReferenceQueue<? super People> q) {
super(referent, q);
this.name = referent.name;
}
}
具体的原理可以参照这篇文章Java Reference核心原理分析
不可达:
当一个对象对应的所有引用都设置为null时,对象就变成了不可达。
不可达对象会被回收。
2、常用的GC算法
2.1、标记-清除算法
根据1.2的根搜索算法,JVM对存活对象做了标记,所以在内存回收的时候将没有标记的对象清除即可。
优点:方便,不需要移动对象
缺点:会存在内存碎片
2.2、复制算法
将内存一分为二,其中一半内存存放对象,而另一半不存储数据。当触发GC时,将内存1中的对象依次复制到内存2中,清空内存1,下一轮GC中将内存2转存到内存1中。
优点:不存在内存碎片
缺点:需要额外复制对象,会有开销、并且会移动对象,还需要额外更改对应的指针。
当存活的对象少时,复制算法就比较优秀。
2.3、标记-压缩算法
标记-压缩算法是标记-清除的改版,就是在清除对象后,会对内存空间进行碎片整理,将对象按低地址依次排列。
缺点:需要移动对象,更改指针。
2、堆区结构
堆分为新生代和老年代:
1、新生代:
从名字就可以看出来,新生代存放的是新生成的对象。新生代的GC称为MinorGC。
新生代又分为:
a、Eden
新创建的对象存放在Eden区
b、Survivor From
存放上一次MinorGC中幸存对象
c、Survivor To
空的内存区域
Eden、Survivor From、Survivor To 的空间大小比为8:1:1.
为什么要设立Eden?
如果只有Survivor From 和Survivor To的话,会造成空间的浪费。
MinorGC算法过程:
MinorGC采用的是复制算法,因为新生代中的对象存活率不高。
1、对Eden区和Survivor From区进行扫描,将其中存活的对象复制到Survivor To区,在复制的过程中,将对象的年龄+1,当对象的年龄大于15的时候,将对象转移到老年代中。
2、这时Eden和Survivor From区就清空了,并且调换Survivor From 和 Survivor To区,所以存活的对象都转移到了Survivor From区,也就是上一轮中的Survivor To区。
2、老年代:
老年代对应的GC叫做Major GC。老年代存放的是存活时间比较长的对象。对象比较稳定,所以Major GC不会太频繁。
Major GC的过程
Major GC采用的是标记-压缩算法
1、首先利用根搜索算法,将存活的对象进行标记
2、对没有标记的对象进行回收
3、整理内存碎片
Full GC 是指同时执行minorGC 和 major GC。
为什么新生代使用复制算法、老年代使用标记-整理算法:
因为新生代的大多数对象的生命周期都很短,如果采用标记-整理算法的话,需要执行大量的清理操作,效率低。
老年代的对象都很稳定,如果采用复制算法的话,需要大量的复制对象,效率低,标记-清除的效率更高!
3、GC时机
1、Eden区空间不足以存放新对象的时候,触发Minor GC
2、老年代空间无法存储从新生代升到老年代的对象的时候,触发Major GC
3、当调用system.gc()的时候,会触发FullGC
4、垃圾回收器
JVM中提供了多个垃圾回收器,各个回收器都有其适用的场景,没有哪一个垃圾回收器是完美的。我们需要根据具体的业务场景来选择合适的垃圾回收器。
Stop-world指的是在GC过程中,造成的程序线程暂停的现象,此时系统被停顿,也被简称为STW,垃圾回收器的目标就是减少STW和提高系统的吞吐量
为什么需要停止主程序呢?
如果标记和清除和主线程一块执行的话,主线程会不断的产生垃圾,导致GC的效果差。不会产生错标的情况,因为一个对象没有了引用,是无法被再次引用的。
1、Serial收集器
**Serial收集器对新生代和老年代都可以回收。**Serial收集器是JVM最古老和最基本的收集器了。Serial就是串行化的意思。也就是说Serial收集器是单线程的。并且当单线程回收垃圾的时候,其他工作线程都需要暂停,这会造成STW,造成停顿。
新生代垃圾回收算法:复制算法
老年代垃圾回收算法:标记-清除算法 Serial Old收集器
好处:简单高效,适用于单核处理器。因为如果多线程的话,会有额外的切换开销。
缺点:会造成"stop-world",也就是其他工作线程会暂停,导致程序的不可用,是独占式的。
2、Parnew
Parnew垃圾回收器其实就是Serial回收器的多线程版本。只针对新生代GC,GC算法为复制算法。通常配合老年代CMS垃圾回收器使用
为什么会出现Parnew呢?
因为随着计算机的发展,计算机的处理器有了多核的版本,如果在继续使用单线程的话,比如8核的话,其他7个核就在空闲,使用不上了。所以才出现了多线程版本来适应多核版本。
Parnew回收器在回收垃圾的时候,其他工作线程仍然是不可用的,也会造成STW,仍然是独占式的,造成停顿。
但是如果是单核CPU或者并行能力不行的系统中,Parnew回收器的效果不如Serial回收器。
3、Parallel
**Parallel对新生代和老年代都可以回收。**Parallel收集器也是多线程的收集器。与Parnew不同的是,Parallel设置了最大垃圾清理时间,垃圾清理时间不能超过规定时间。Parallel收集器仍然是独占式的,清理垃圾的时候会将工作线程暂停STW,造成停顿。
新生代回收算法:复制算法 Parallel Scavenge
老年代回收算法:标记-清除算法 Parallel Old
4、CMS
CMS的全称是Concurrent Mark Sweep,中文叫做并发标记清除。
CMS是一个老年代垃圾收集器。可以与Serial和Parnew垃圾回收器搭配使用。常常跟Parnew收集器搭配使用。采用的GC算法是标记-清除算法。
CMS的卡表技术
JVM将堆分为了新生代和老年代,新生代和老年代的GC是分开的,新生代的GC很频繁,并且采用可达性分析来标记存活对象。如果只遍历存在新生代的GCRoots会存在这种情况,会将一些新生代对象误标,因为有可能老年代的对象引用了该新生代对象。
所以想要准确的标记新生代的对象,需要扫描整个老年代,这样的效率很低下,所以引入了卡表。
JVM将老年代分为了若干个卡页,每个卡页的大小为2的幂次方,然后用卡表来记录对应的卡页的状态,如果一个老年代中的对象引用了新生代中的对象,就将老年代对象对应的卡页对应的卡表项标记为dirty,当新生代进行GC的时候,不需要扫描整个老年代,而是只需要扫描标记为dirty的卡页就可以了。
卡表采用下标的方式来管理,通过对象的地址除以卡页的大小就得到对应的卡表下标了。
由于新生代的对象的存活时间非常短,CMS对老年代进行GC的时候,会扫描整个堆,并没有在新生代中维护卡表
4.1、初始标记:
这是CMS中两次Stop-World的第一次,造成停顿。主要标记老年代中的与GcRoots关联的对象。
CMS采用三色标记的方法,GcRoots关联的对象会被标记为灰色。
4.2、并发标记:
从初始标记中对象开始做可达性分析,并发标记还在存活的对象。在并发标记的时候,主程序是可以运行的。
4.3、重新标记
重新标记是stop-world的,不允许主程序同时运行,造成停顿。
1、在并发标记的过程中,主线程会产生新的垃圾,可能会存在漏标的情况,CMS会记录对应的对象,然后重新将其标记为白色。
2、在并发标记的过程中,可能会有对象从新生代移动到老年代,或者有新对象直接存入老年代,CMS也会记录对应的对象
4.4、并发清理
将 白色的对象进行清理。此时主程序线程可以同时工作,同时也会产生垃圾,这种垃圾叫做”浮动垃圾“。
CMS会导致碎片。
CMS已经被逐渐抛弃了,取而代之的是G1收集器。
5、G1收集器
G1的全称是Gabbage First。也就是垃圾优先。G1收集器虽然也是将堆分代,分成老年代和新生代。但是和之前不同的是,堆被分为若干个Region。每一个Region可以根据需要来充当Eden、Survivor、Old,十分灵活,对不同类型的Region采用不同的收集策略,达到最好的收集效果。
一个Region对应着一小块连续的内存。Region采用的回收算法是复制算法,是没有碎片的
G1之所以能够称为现在主流的垃圾回收器,与其Region堆内存布局密切相关。
一个Region对应着有它的价值,与其回收所消耗的时间和回收获得的空间大小有关。
按照Region的价值对其进行排序,优先回收价值大的Region,保证了G1能够在有限的时间内获得最大的收集效果,提高了G1的收集效率。
Remember Set
因为G1是按照region进行回收的,对于新生代中的一个要回收的region,需要确定region里面的对象是否被老年代引用,G1不采用对老年代整个扫描的方式,而是采用Remember Set的方式,Remember Set是一个类似于哈希表的结构,Key是引用了该region的region的起始地址,Value是引用了该region的对象的卡页集合。这样的话,不需要对整个老年代进行扫描,而是通过remember set中记录的卡页集合来确定region里的存活对象。
新生代GC:
当eden区无法存放对象的时候,会触发新生代的GC,会对新生代进行GC roots 扫描以及 Remeber Set来确定对象是否存活,接着将s1和eden的对象拷贝到s2中。
新生代GC的过程是STW的。
随着老年代的内存占用提高,当超过老年代占用率超过阈值后,会触发以下的混合GC
5.1、初始标记
标记GCRoots的直接对象。
5.2、并行标记
对根扫描中的对象进行可达性分析,对所有可达性对象进行标记。
在并行标记的过程中,程序线程还可以运行,也会产生新的对象
Region中有有两个地址prevTAMS和nextTAMS。地址之间存放的是新增的存活对象,nextTAMS随着对象的增加而升高。
5.3、重新标记
这个会造成stop-world,程序停顿。这个标记速度很快,
对于新增的对象,当对Region进行清理的时候,会将prevTAMS和nextTAMS之间的对象进行复制,不会对其进行回收。
对于并发标记中的改变的节点,G1采用的是原始快照式的,可以保证节点不被误标记为白色。
5.4、清理准备
这个会造成Stop-world,停顿。会对所有Region垃圾信息进行统计,垃圾较多的Region会被优先清除,将其Region标记为G。
5.5、清理
会造成stop-world。
在满足回收时间的条件下,挑选回收价值最大的region进行回收,将其存活对象进行复制,然后清空对应的region。
如果在回收的过程中,找不到空闲的region来存放活对象,就会触发一次Serial GC。
G1因为采用复制算法,所以不会产生碎片。
G1中除了并行标记,其他阶段都是停顿的。
CMS追求的是并发清理,低停顿,而G1追求的是在满足用户要求的延迟的情况下,追求更高的清理效果。