距离上篇文章已经过了半个多月了,这期间自己也有不断的进一步研究jvm的特性,那么这篇文章将继续深入了解更多的jvm的内容
前言
上一篇文章主要聊了关于,java的编译和加载的原理,例如java编译成class文件后运行时的加载方式,以及jvm的内存结构,最后聊了关于jvm堆和方法区中的分代机制。 java之所以叫做java正是因为其特有完美兼容一次编译遍地运行的特性,python和c#虽然也效仿java但是由于其平台的并没有java的兼容性好所以难以大面积推广,比如python的2.X版本和3.X版本是一个较大的跨域,但是并不完全兼容,而且其字节码转码效率并没有java高,而C#更加不用说了,几乎是windows独占,对linux并不是很合适,也间接注定这两门语言的能力发挥上收到了限制
为什么要分代?
首先我说明一点,并不是因为分代java才能进行GC垃圾回收,没有分代机制只要能判断哪些对象需要回收,jvm一样可以回收,那么为什么要分代呢?凡是都是有原因的很明显,就是为了效率,分代能让GC更加高效
大家可以试着想想,如果没有分代所有的对象都在内存里面,那么jvm每次回收对象都要从头扫描一遍内存,这样的效率会有多低,而且又加上很多对象其实存活时间并不长,执行完就死了,那么频繁的大规模扫描内存必然让jvm的效率低下,所以分代有利于高效的垃圾回收
怎么分代?
jvm里面主要把对象分为了三代(可以阅读我上篇文章:java虚拟机和垃圾回收机制(一)):年轻代,老年代,持久代
年轻代:
所有新生成的对象首先都是放在年轻代的。年轻代大部分都是生命周期比较短的对象。年轻代里面有三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时触发垃圾回收,还存活的对象将被复制到Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代:
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代(永久代):
用于存放静态文件,常量池,如今Java类、方法等。基本上垃圾回收不会参与到,不影响垃圾回收(jdk7开始已经废除这个分类,其实可以说从jdk8开始没有永久代了,jdk8常量池放入到堆区中参与垃圾回收)
如何回收?
垃圾回收原理上分为两步,第一步筛选出需要回收的对象,第二步回收这些对象。你要回收这个对象,首先必须要能确定这个对象是不在需要的在海量的对象面前,一个个遍历判断固然很低效,所以我们需要算法去解决这个问题
判断垃圾对象:1、引用计数法 2、可达性分析
传统垃圾回收算法:1、标记清除法 2、复制算法 3、标记-整理算法
分代垃圾回收算法:1、分代收算法
引用计数法:
public static void main(String[] args){
Object object1=new Object();
Object object2=new Object();
object1.object=object2;
object2.object=object1;
object1=null;
object2=null;
}
上面的对象已经不可能再被使用到,但是他们的引用计数永远不为0,所以永远不会被回收
可达性分析:
为了解决如上面出现的循环引用的情况,gc改良后加入了一种新的算法,可达性算法,java会构建一个逻辑上的对象根节点GC ROOT 通过判断对象当前是否能包含在根节点的引用树种,如果没有则证明当前对象也是垃圾对象
从图中可以看出,对象四和五都不包含在ROOT的引用树里面,所以判断这两个对象是需要被回收的
标记-清除算法:
当对象被标记出来后,下一步就是执行了,当然执行如果直接遍历,效率也是很低下的,标记-清除算法是jvm里面最基础的垃圾回收算法,通过上面的方式,标记出来对象后,直接回收如图:
从图中可以看出来,回收后的内存空间参差不齐,如果多回收几次后,可能内存碎片化过于严重导致大型对象的内存无法分配的情况
复制算法:
为了解决上面出现碎片化的情况,最初的解决方法就是采用内存分块的方式,jvm将内存平均分为两片,每次对其中一片空间进行回收,存活的对象复制到另一片内存,这样交替回收,可以一定程度上的避免大面积的内存碎片的情况
但是这个算法有一个很严重的问题,就是他会导致内存空间直接减少一半,对于靠内存而活的程序员来说,这个是无法容忍的
标记整理算法:
为了避免内存大量浪费的情况,并结合了上面的两种算法,而提出的新方式。与标记-清除算法不同的是,标记后不是清理垃圾对象,而是将认定为存活对象移向内存的一端。然后清除端边界外的对象。如图:
如果说以上都是传统的垃圾回收思路,那么下面这个就是jvm目前最流行的垃圾回收方式,也就是使用了分代机制后的gc模式
分代收集算法:
分代收集是目前大部分jvm所采用的方法,其思想是根据对象的存活的不同生命周期将内存划分为不同的区域,一般情况下将GC堆划分为年轻代和老年代。老年代的特点是,每次触发垃圾回收机制的时候,老年代中只有少量的对象需要被回收,因此可以根据不同不同的区域选择以上的不同算法进行垃圾回收。
目前大部分jvm的GC对年轻代都采用复制算法,因为年轻代中每次GC都要回收大量的对象,按照上面的例子年轻代对象创建在Eden区经过多轮GC,仍然存活的对象将进入老年代,例:
老年代因为需要回收的对象较少所以采用标记整理算法,一般来说对象创建基本上都是在年轻代,很少一部分是直接到老年代中,当新生代的Eden区内存不足的使用就会触发一次GC,存活的对象每存活一次年龄就会加一,并复制到存活的新区中,直到转入老年区