内存模型
一说到内存管理,首先需要了解它的内存模型。
虚拟机的内存模型在jdk1.8之后有了一些变化,我们分开来看,请看下图:
由图我们可以看出,jdk每个版本都会有新生代和老年代,唯一不同的是小于1.8的版本为永久代,而大于等于1.8的版本去掉了永久代,转为元空间(Meta Space)。
永久代也就是存储的数据区里面的方法区,如果程序在运行中发生PermSpace溢出,则说明永久代内存不够,需要调整JVM参数增加永久代内存空间。
jdk1.8以后出现了MetaSpace,它和永久代不同的是,它的内存空间是动态扩展的,当然我们也可以设置MaxMetadaSpace来设置最大元空间内存数量,也就是在1.8以后设置PermSize是无效的。
本文主要讲解jdk1.8以前得内存管理机制
垃圾回收机制
在现代编程语言中,对垃圾回收算法主要有两种方式:引用计数器和可达性分析。
引用计数器
引用计数的原理大致是这样的:为每一个创建的对象设置一个引用计数,每当对象被引用一次后,引用计数加1,当对象引用失效时,引用计数减1,当引用计数为0是说明没有任何对象引用了,即可释放该对象。
引用计数的一个弊端是,他无法解决对象间相互引用的问题,比如下面这段代码:
public class RefrenceCountingGC {
private Object instance = null;
public static void main(String[] args) {
RefrenceCountingGC gc1 = new RefrenceCountingGC();
RefrenceCountingGC gc2 = new RefrenceCountingGC();
gc1.instance = gc2;
gc2.instance = gc1;
gc1 = null;
gc2 = null;
//假设采用引用计数算法,在这里发生GC,gc1和gc2能否被回收?
System.gc();
}
}
两个对象始终处于相互引用阶段,因为引用计数永远无法为0,因此就不能自动释放它。
可达性分析
为了解决引用计数出现的这些问题,可达性分析算法出现了。
在主流的编程语言中,java和c#都是通过可达性分析来判定对象是否存活的。
它的原理大致是:通过一系列的被称为“GC Roots”的对象作为起始点,然后从这些节点开始向下搜索,搜索的路径连成的一条线,我们称之为“引用链”,当前一个对象到GC Roots没有任何引用链,即我们说的这个对象不可达时,则说明该对象是可以被回收的,通过下图可以更好的理解:
当一个对象不可达时,并不能代码这个对象就能马上被回收,他会处于死缓状态,而一个对象真正要回收时,至少需要经历两次标记。第一次标记的前提条件是,看该对象是否覆盖了finalize()方法或者finalize()方法被虚拟机调用过,如果覆盖了finalize()方法并且虚拟机还没有调用过,这时会标记它,该对象还有机会存活,方法很多,比如在finalize()方法内存引用该对象。
如果进行第二次回收时,由于虚拟机已经调用过finalize()方法,就不会再调用他了,这时该对象就会真正宣告死亡了。
请看下面这段代码:
public class GCRoot {
private static GCRoot instance = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalized执行");
instance = this;
}
public static void main(String[] args) throws Exception{
instance = new GCRoot();
instance = null;
System.gc();
//因为finalize执行优先级较低,这里等待0.5秒
Thread.sleep(500);
if(null != instance){
System.out.println("对象拯救成功!");
}else{
System.out.println("对象被释放!");
}
//和上面的代码一样,不会执行finalize方法,所以拯救失败
instance = null;
System.gc();
if(null != instance){
System.out.println("对象拯救成功!");
}else{
System.out.println("对象被释放!");
}
}
}
运行结果:
finalized执行
对象拯救成功!
对象被释放!
java引用
jdk1.2之前的引用很简单,这里我们不探讨,我们主要探讨jdk1.2之后的引用。
java中将引用分为了:强引用、软引用、弱引用和虚引用。
强引用
强引用在java程序中最常见的一种引用类型,类似Object o = new Object()这类引用,只要强引用还在,垃圾回收器就永远不会回收它。
软引用
软引用通常用来描述一些可以用但非必须的对象,在内存溢出之前会先回收掉软引用相关联的对象,如果回收后内存依然不够,则才会抛出内存溢出异常。
弱引用
弱引用用来描述一些非必须的对象,但是它的强度比软引用还有弱一些。被弱引用关联的对象只能存活到下次垃圾回收器工作之前,当垃圾回收器开始工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
虚引用
虚引用是最弱的一种引用类型,为对像设置虚引用关系的唯一目的就是能在这个对象被垃圾回收之时收到一个系统通知。
垃圾收集算法
1、标记-清除算法
这是最基础的一种垃圾收集算法,后续所有的算法都是在这个算法的基础上进行扩展。
通过算法的名字大致能够看出,该算法分为了“标记”和“清除”两个阶段:首先需要标记出需要回收的所有对象,待标记完成后清除掉所有标记过的对象。这个算法的不足主要有两个:一是性能问题,标记和清除两个阶段的性能都不高,二是标记清除后会产品不连续的大量内存碎片,内存碎片太多会导致下一次在需要分配占用大量内存的对象时,无法找到足够的连续碎片而不得不再一次触发垃圾收集动作。
2、复制算法
为了解决效率问题,复制算法出现了。这种算法会将可用内存区域划分为大小相同的两块,当需要垃圾回收时,会先将可用的对象复制到另一个内存区域,从而将当前区域一次性清除。这样做的好处是每次都将一整块内存区域清除掉,从而避免了大量的内存碎片出现。
目前主流的商用虚拟机大多是采用复制算法来回收新生代。
3、标记-整理算法
当对象的存活率较高时采用复制算法,效率就会很低,因此对于老年代一般不采用复制算法。
鉴于这种问题,一种称之为“标记-整理”算法的思路出现了。它和“标记-清除”算法一样,都需要先进行标记,但是它不会简单一次性清除标记的对象,而是将所有存活对象都移动到另一端,然后清除掉边界外的对象。
4、分代收集算法
分代收集其实就是将内存划分为几块,比如将java堆划分为新生代和老年代,根据不用年代采取最适合的算法,所以目前主流商用虚拟机都是采用这种方式。
学习交流群:669823128