目录
JVM的内存划分
JVM名为java虚拟机,它的内存划分为:方法区,本地方法区,栈,堆,程序计数器。
程序计数器:是一块比较小的内存空间,可以把他看做是程序执行字节码的行号指示器。
本地方法栈:本地方法栈使用到的大多数是Native方法,由c 和c ++ 语言编写的。调用本地的类库。
java堆:Java堆是jvm管理内存中最大的一块,被所有线程共享的一块内存区域。此内存区域的唯一目的就是存放对象的实例,几乎所有的对象实例都在这里分配。java堆也是垃圾回收机制的主要区域,因此很多时候被称为GC堆。从垃圾回收的角度看,大多数采用的是分代收集算法,所以java堆中还可以细分为:新生代和老年代和元空间。细分的话可以将新生代分为Eden空间,From Survior空间,To Survivor空间。关于垃圾回收算法,在后边我们会介绍到。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区:方法区与java堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的类信息,常量,静态变量,即编译器编译后的等代码数据。虽然java虚拟机把方法区描述为堆的一个逻辑部分,但是它有一个别名Non-head。垃圾回收机制并不是不对方法区域进行gc,而是方法区中的常量池的回收和对类型的卸载。
垃圾收集器
1 引用计数算法
很多的教课书都是这样的,用来判断对象的存活。给一个对象添加一个引用计数器,如果一个引用指向了这个对象,则引用计数器+1,如果一个引用引用失败,则引用计数器-1,当一个对象的引用计数器的数值为0的时候,表示这个对象就不能再被使用了,就会触发来及回收机制,将其回收。但是在虚拟机中判断对象的存活,却不是这样的。
2 可达性分析算法
用来判断对象的死活。这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起点,从这些节点自上向下所走过的路程称为引用链,如果某个对象不在这个引用链中,换句话说,就是"GC roots"到该对象的位置不可达,则说明这个对象不可用。
再谈引用
上边所讲的无论是引用计算器算法,还是可达性分析算法,判断对象的存活都是与“引用”相关。传统对引用的定义很狭隘,是指一个变量中存在的是另一个对象的引用,就称为引用。我们希望描述这样一种对象:当内存空间充足时,可以保留这些对象,但是当经过垃圾回收之后内存还是很紧张,则可以将这些对象进行垃圾回收。很多系统的缓存就采用了这种应用场景。
在JDK1.2后,对引用进行了扩充,将引用分为了强引用,软引用,弱引用,虚引用。
强引用即使程序代码中普遍存在的,比如new出来的对象,只要强引用还存在,垃圾回收机制就永远不会回收掉被引用的对象。
软引用是用来描述一些还有用但是并非必须的对象。如果被软引用关联的对象,在系统将要发生内存溢出之前,就会把这些对象进行二次回收,如果回收之后内存依旧不够,才会抛出内存溢出异常。
软引用也是用来描述非必须对象的,但是他的强度比软引用更弱。他只能生存到下一次来及回收之前,也就是说,当来及进行第一次回收,软引用可以逃过一劫,但是第二次垃圾回收后,他就会被会回收。
虚引用是最弱的一种引用,他的主要作用就是在这个对象被垃圾回收之前接到一个系统通知。
生存还是死亡
首先声明一点,在可达性分析中,不可到达的兑现并不是“非死不可”,只能说这个对象已经到了“缓刑”的阶段,因为要想真正的宣告一个对象死亡,他至少要经历两次被标记的过程。如果对象在经过可达性分析算法后没有与GCOOTS进行相连,那么他将第一次被标记并且被筛选。筛选的条件是此对象是否有必要执行finalize()方法。当一个对象没有被覆盖finalize()方法或者已经覆盖过,虚拟机则判断他们没有必要执行。finalize()是对象逃脱垃圾回收的最后一个机会,在垃圾回收过程中,如果finalize()执行缓慢或者发生了死循环,对象只需要在最后一次回收之前只需要有一个引用指向该对象,就能逃脱掉这次垃圾回收。
回收方法区
很多认为方法区是没有垃圾收集的,其实是有收集的,在新生代中,常规的一次来及回收能回收70%-95%的空间,但是方法区的回收的效率远低于此。
方法区的回收主要收集两部分内容:废弃常量和无用的类。假如有一个字面值为“abc”的常量,已经被放入了常量池,但是当前系统没有任何String类的对象指向它,如果这个时候发生垃圾回收机制,这个常量就会被回收,判断一个常量回收比较简单,但是判断一个类回收,条件苛刻的多。
需要满足三个条件才能算无关无用的类:
1 该类的所有实例都已经被回收,也就是Java堆中没有任何该类的实例。
2 加载该类的ClassLoader已经被回收
3 该类对应的java,lang,Class对象没有在任何地方被引用,无法在任何地方访问该类的方法
垃圾收集算法
1 标记 - 清除算法
最基础的收集算法,它分为两个阶段,正如它 的名字一样,“标记”阶段和“清除阶段”, 首先标记处所需要回收的是对象,在标记后回收所标记的对象。对象的标记前面已经说过了。后边的所有的回收算法都是基于这个算法的改变。这个算法有两个不足:1)效率问题,标记和清除两个过程效率不高。另一个是空间问题,标记清除后会产生大量的不连续的内存碎片(内存碎片:分为内部碎片和外部碎片 ,这个属于操作系统的知识,如果不会,请自行查阅资料,这里不再进行阐述。)内存碎片太多,可能会导致在程序运行过程中需要分配较大的内存,但是没有足够的内存去支持这个对象多需要的内存,就可能回触发另一次的垃圾回收。
图解:
复制算法
相对于标记-清除算法,本算法更加注重的是效率问题,它将可用的内存分为容量大小的两块,每次只使用其中的一块,当一块内存清理完了,将这个内存上的可用对象给复制到另一块内存上,然后将原来的那一块的所有内存空间给清理掉,这样子不用考虑内存碎片的问题,简单高效。现代商业的虚拟机很多都是采用这种收集算法回收新生代的,将堆中新生代分为一块较大的Eden和两个比较小的Survior空间。每次使用Eden和其中比较小的一块Survivor.当回收时,将Eden和其中的一块Survivor还存活的对象一次性的复制到另一快Surivivor空间上,最后清理掉Eden和刚才使用过的Survivor。虚拟机默认的Eden和Survivor大小比例是8:1.
标记-整理算法
这种算法相对于标记-清除算法前面的步骤都一样,但是后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前商业的虚拟机都采用的是这种分代收集算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代。新生代前面已经介绍了,这里不再进行过多的阐述。在新生代中,每次垃圾收集的时候都有很多对象大批的死去,只有少量的存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率比较高,没有额外空间对他进行分配担保,就采用标记-清理 或者标记-整理算法进行收集。
内存分配与回收策略
1 大对象有先分配在Eden区
大多数情况下,对象在新生代Eden区中分配。当Eden区中没有足够的空间分配时,虚拟机将发起一次Minor GC(新生代垃圾回收).
例如:尝试分配3个2MB的对象和一个4MB大小的对象,通过虚拟机参数的设定,设定10MB分配给新生代,10MB分配给老年代。在新生代中e:s:s = 8:1:1 ,所以当是三个2MB的对象分配给Eden区之后,再分配一个4MB的对象的时候,发现塞不进去,就会触发一次Minor GC机制,由于这三个对象依旧存活,所以gc之后并没有消失,此时需要分配一个4MB的对象,就会提前触发分配担保机制,将新生代的三个对象分配到老年代去,这样Eden区就有足够的空间分配这个4MB的对象。
2 大对象直接分配到老年代
所谓的大对象是指,需要大量额连续的内存空间的Java对象,最典型的就是那种很长的字符串以及数组。详情请参考深入理解jvm。
3 长期存活的对象进入老年代
既然虚拟机采用了分带收集算法来管理内存,那么回收内存时就必须能识别哪些对象在新生代,哪些对象在老年代。为了做到这一点,虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并且经过第一次MinorGC后仍然存活,并且能被Survior容纳的话,将被移动到Survivor中,并且对象的年龄设为1.对象在Survivor中每熬过一次Minor GC,年龄就增加一岁,当他的年龄增加到一定程度,默认的值为岁,就会被晋升为老年代。
4 动态对象年龄判定
为了更好的使用不同程序的内存状况,虚拟机并不是永远的要求对象年龄都达到默认的值15才能晋升到老年代,如果在Survivor空间中相同年龄所有的对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到15默认的值。
5 空间分配担保机制
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总和,如果条件成立,那么Minor GC可以确保线程是安全的。如果不成立,虚拟机会查看HandlePromototionFailture设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次MinorGC.