JVM虚拟机(中)--堆,GC机制

寵の児 提交于 2020-01-23 14:02:18

内容承接上节,上节说到了虚拟机栈,本地方法栈及程序计数器及类的加载等相关内容,这节聊聊上节没提到的元空间,堆及堆上的GC机制,GC的算法将在下节继续说明.


 java创建的对象大部分都在堆上存储,是GC操作的主体.那么堆在内存如何分配的呢?如下图:

 首先堆上分为新生代和老年代,新生代又分为Eden区,From Survivor 区及To Survivor区,新生代上发生的GC叫做Minor GC,老年代发生的GC叫做Major GC或者Full GC.

Minor GC速度较快,比较频繁,Major GC/Full GC则速度较慢,是Minor GC 的十分之一以下,一般来说,发生Major GC/Full GC时会伴随至少一次Minor GC.

在谈GC之前,先说说新生代与老年代,新生代中Eden,From Survivor及To Survivor之间的联系.以及触发GC的条件.

Eden区,java生成的对象一开始大多数(特例后文会讲)都在Eden区,当Eden区中的对象占用空间越来越大时,会触发Minor GC,此时在Eden区的对象如果是可回收的(下文会对可回收判断逻辑进行解释)则直接回收,而不可回收的部分会被放入From Survivor区.

From Survivor区与To Survivor区,发送GC时,From Survivor区中的数据(上一次GC时产生的及这一次GC刚从Eden里放入的)会将其拷贝到To Survivor区,完成后将原本两个Survivor区的名称互换,也就是From Survivor改名成To Survivor ,To Survivor改名成 From Survivor.完成轮换.

有点不明白,没关系,这里对Minor GC 时这三者的关联进行描述.

总体上java对象的的路径是 Eden==>From Survivor==>To Survivor.而From Survivor和To Survivor是会互相轮换的,这里的轮换就需要稍微说道说道了,首先From Survivor中的数据在GC时会被干掉一部分(回收),不能干掉的那部分则会留下,并复制给To Survivor,然后将From Survivor中的数据清空,随后改名.也就是说同一个内存空间,有时被称呼为"From Survivou"有时又被称呼为"To Survivor",名字换得很频繁.这样就会导致一个现象,名叫"To Survivor"的那块区域一直是空着的,而"From Survivor"那块区域一直是有数据的,就像两个水杯,互相倒水,有水的那个被叫做"From Survivor".

那么问题来了,为什么要搞这么麻烦?两个杯子互相倒水,直到杯子装不下,再把数据放入老年代.换一个大一点的杯子不是更好吗?其实不然.我们知道内存中存储数据是要占用空间的,当对象大的时候,可能需要某块连续的空间才能被存下.而内存中可能有余量,却没有这么大的连续空间,这就是空间碎片,而如果不停的互相拷贝呢?每次拷贝,都会重新分配内存,消灭空间碎片,提升空间利用效率.而付出的代价就是10%(默认情况下)的空间无法被利用,因为To Survivor一直是空的 .这点和下节要说的复制算法有点类似这里先留个底.

 

再来说说老年代,老年代与新生代的比例为2:1,并且上文有说过,老年代中的GC,Full GC速度较慢.那么哪些对象会进入老年代呢?

1:大对象会直接进入老年代.这里的大对象指需要大量连续内存空间的对象,这是为了避免在两个Survivor空间之间出现大量的内存复制.而大对象可以通过-XX:PretenureSizeThreshold=1M -XX:+UseParNewGC 来设置(必须配合Serial、ParNew收集器,下节将会说到这两种收集器算法)

2:年龄大的.在之前文章里,谈synchronized 锁膨胀及相关知识点的文章里有提到过,java对象头中存在一个age字段.这个age字段就是记录当前对象的"年龄".age对象占四个字节,每次GC发生时,如果对象未被回收,则age自增1,当age增加到一点大小时(默认15),则会晋升至老年代.晋升的年龄阈值可以通过-XX:MaxTenuringThreshold 来设置.

3:Survivor空间不足时,会将部分数据直接放入老年代.当Survivor中相同年龄的对象大小总和超过空间容量一半时,年龄大于或者等于该年龄的对象会直接进入老年代.换言之,有同龄对象总量大于空间容量一半时,等于或超过这个年龄的对象会被放入老年代.

顺带说下元空间,在jdk1.8之前使用的是永久代,1.8之后使用元空间替换了永久代.永久代存在一个固定的大小上限,JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制.这样就能降低内存溢出出现的概率.


终于进入本篇重点了,GC机制.哪些对象会被回收?

上文已经提到过,GC也分为新生代GC,Minor GC与老年代GC,Major GC/Full GC.而在GC之前,需要知道哪些对象能够被回收,也就是GC判断策略.

GC判断策略分为两种,引用计数以及跟节点可达性.

引用计数:引用计数实现简单,效率高,但是却没有被目前主流的虚拟机所采用.为什么呢?看看他的逻辑就明白了.引用计数,顾名思义,被引用了则计数.比如A被B所引用,那么A的计算器+1,未被引用的,计数器则为0,那么释放掉计数器为0的对象就不会造成问题.非常简单,效率也高,但是存在问题.倘若A中引用了B,B中也引用了A,这两个对象互相循环引用,那么即使他们没有任何作用,他们的计算器也不为0,无法被删除.

跟节点可达性:如图所示.从GC Roots开始往下找,被经过的对象就是不可回收的对象,未被遍历到的对象,就是可用被回收的对象.

那么哪些可用被当做GC Roots呢?

有以下这些:

---虚拟机(栈帧中的本地变量表)中引用的对象

---方法区中类静态属性引用的对象

---方法区中常量引用的对象

---本地方法栈中JNI(即一般说的native方法)中引用的对象


除了这两种策略之外,还有一个需要注意的地方,引用类型.

引用类型分为强引用,软引用,弱引用及虚引用.这里依次来说说他们和GC的关系.

1.强引用,平时使用的大部分引用都是强引用,比如:String a="a";这里便是强引用,强引用的特点便是JVM不会回收它们,当内存空间不足时,即使抛出OutOfMemoryError 错误也不会将强引用对象回收.

2.软引用,比如:SoftReference ss=new SoftReference("3");这里便是软引用,软引用的特点在于当内存不足时,JVM会将它们回收,这种类型可以用来实现对内存敏感的高速缓存.

3.弱引用,比如:WeakReference<String> hello = new WeakReference<String>(new String("hello"));这里便是弱引用,弱引用的特点便是只要发生了GC,无论内存空间是否充足,都会将它们回收.

4.虚引用,虚引用和以上三者不同,虚引用不会决定对象的生命周期.形同虚设的引用.虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用.比如:

ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
PhantomReference<String> p =new PhantomReference<>("p",rq);

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中.程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收.程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动.


GC的算法将在下节继续描述.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!