Java与C++之间有一堵由内存动态分配和垃圾收集技术所围城的高墙,墙外面的人想进去,墙里面的人却想出来。
Java凭借虚拟机自动内存管理机制,不需要为每一个new操作去配对free的操作,不容易出现内存泄露和内存溢出问题。但是我们还是很有必要了解虚拟机是怎么使用内存的。本文楼主将着重介绍虚拟机中内存是如何划分以及垃圾收集的算法。
1.Java内存区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域,下图为Java虚拟机运行时的数据区。
经常能够听到Java内存区分为对内存与栈内存,这种分法比较粗糙
1.1 Java堆
是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。所有的对象实例及数组都要在堆上分配。
Java堆是垃圾收集器管理的主要区域,也叫GC堆,现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为新生代、老年代;再细致一点有Eden空间、From Survivor空间、To Survivor空间。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常
1.2 Java栈(虚拟机栈)
线程私有,生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧Stack Frame用于存储局部变量表、操作数栈等啥的。每个方法从调用到执行完成对应一个栈帧在虚拟机中入栈到出栈的过程。
1.3 程序计数器
Program Counter Register一块较小小内存空间,线程私有,可以看作是当前线程所执行的字节码的行号指示器。
如果线程执行的是一个Native方法,这个计数器值则为空,此内存区域时唯一一个在JVM中没有规定任何内存溢出的区域。
1.4 本地方法栈
与虚拟机栈发挥的作用类似
1.5 方法区
各个线程共享区域,存储已被虚拟机加载的类信息、常量、静态变量等,别名非堆、永久代。
2.垃圾收集器与内存分配策略
Java虚拟机运行时程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊地执行着出栈和入栈操作;因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,我们只有在程序运行期间才能知道会创建那些对象,这部分的内存分配和回收都是动态的,垃圾收集器关注的就是这部分内存。
2.1 对象存活判断方法介绍
2.1.1 引用计数算法
给对象添加一个引用计数器,每当有一个地方引用计数器值加1;引用失效计数器值减1;任何时刻计数器为0的对象就是不可能再被使用的。
JVM没有选用引用计数法管理内存,最主要原因是它很难解决对象之间相互循环引用的问题。
2.1.2 可达性分析算法
在主流的商用程序语言Java,C#等都是通过Reachability Analysis来判定对象是否存活
算法基本思想:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。
在Java中可作为GC Roots的对象包括:
1)虚拟机栈中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(Native方法)引用的对象
2.2 垃圾收集算法
2.2.1 标记-清除算法
思想:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:1)效率问题,标记和清除两个过程效率都不高;2)空间问题,标记清除之后会产生大量不连续的内存碎片。
2.2.2 复制算法
思想:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
不足:将内存缩小为了原来的一般,代价太高了一点
应用:现在商业JVM都是采用这种收集算法来回收新生代。IBM统计新生代中的对象98%是朝生夕死,所以并不需要按照1:1划分内存空间。Eden:Survivor=8:1,大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。
2.2.3 标记-整理算法
老年代采用这种垃圾回收算法
思想:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
2.2.4 分代收集算法
当前商业JVM都采用分代收集。
思想:并没有什么新的思想,只是根据对象存活周期不同将内存划分为几块——老年代、新生代。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用复制算法;而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-整理算法。
3.垃圾收集器
Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法,年老代主要使用标记-整理垃圾回收算法。
4.内存分配与回收策略
1)对象优先在Eden分配
2)大对象直接进入老年代
3)长期存活的对象将进入老年代
解释:如果对象在Eden出生经过第一次Minor GC(新生代GC,指发生在新生代的垃圾收集动作,Minor GC非常频繁,一般回收速度也比较快)后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄加1,加到一定程度(默认15)就会晋升到老年代。
4)动态对象年龄判定
解释:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄所有对象将直接进入老年代。
5)空间分配担保
解释:在发生Minor GC之前,JVM会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,这个条件成立,那么Minor GC可用确保安全。否则,会去查每次晋升到老年代对象容量的平均大小作为经验值,大于进行Minor GC,这是有风险的。否则或不允许冒险,就要改为进行一次Full GC(Major GC,老年代GC,速度比Minor GC慢10倍以上)