JVM的内存分配与垃圾回收策略

时间:2022-04-22 00:02:12

自动内存管理机制主要解决了两个问题:
给对象分配内存以及回收分配给对象的内存。

>>垃圾回收的区域

前面的笔记中整理过虚拟机运行数据区,再看一下这个区域:

JVM的内存分配与垃圾回收策略

注意在这个Runtime Data Area中:

程序计数器、Java栈、本地方法栈3个区域随线程而生,随线程而灭;
每一个栈帧中分配多少内存基本上在类结构确定下来的时候就已知,
因此这几个区域的内存分配和回收都具有确定性,不需过多考虑回收问题,方法结束或者线程结束时,内存自然就随之回收了。

Java堆和方法区Method Area则不一样,
一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

>>判断对象是否需要回收的策略

(1)引用计数算法
定义:引用计数算法(Reference Counting):给对象添加一个引用计数器,每当一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能被再使用的;
优点:实现简单,判定效率高;微软的COM技术、Python中都使用了Reference Couting算法进行内存管理;
缺点:由于其很难解决对象之间相互循环引用的问题,主流Java虚拟机里面都没有选用Refrence Couting算法来管理内存;
(2)可达性分析算法
定义:可达性分析(Reachability Analysis)判断对象存活的基本思路:通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达)时,则证明此对象是不可用的;
JVM的内存分配与垃圾回收策略
Java语言中,可作为GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中产量引用的对象;
本地方法栈中JNI(即一般的Native方法)引用的对象

>>垃圾回收算法

(1)标记-清除算法(Mak-Sweep)

JVM的内存分配与垃圾回收策略
定义:MS算法分标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
两点不足:
效率问题,标记和清除两个过程的效率都不高;
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多后导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发一次垃圾收集动作;

 

(2)复制算法(Coping)

定义:Coping算法将可用内存按容量划分为大小相等的两块,每次使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存清理掉。

JVM的内存分配与垃圾回收策略
优点:每次对整个半区进行回收,内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效;
不足:提高效率的代价是将内存缩小到原来的一半;
现代商业虚拟机都采用这种收集算法来回收新生代,但新生代中的对象一般98%是朝生夕死,无需按照1:1比例来划分内存空间,而是将内存分为1块较大的Eden(伊甸园)空间和2块较小的Survivor(幸存者)空间,每次使用Eden和其中1块Survivor。
回收时,将Eden和Survivor中还存活的对象一次性复制到另外一个Survivor空间中,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot VM默认Eden和Survivor的比例是8:1:1,即只浪费10%的内存。
98%的对象可回收只是一般场景下的数据,无法保证每次回收都只有不多于10%的对象存活,所以当Survivor空间不足时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion),让对象进入老年代。

(3)标记-整理算法(Mark-Compact)

出场背景:复制算法在对象存活率较高时复制操作较多,效率会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以对应被使用内存中的所有对象都100%存活的极端情况,所以老年代一般不直接选用这种算法。

JVM的内存分配与垃圾回收策略
定义:根据老年代的特点,提出标记-整理(Mark-Compact)算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理调用端边界以外的内存。

(4)分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,根据对象存活周期的不同将内存分为几块。
一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

新生代每次垃圾回收时都发现有大批对象死去,只有少量对象存活,故采用复制算法,以少量对象复制的成本即可完成收集;
老年代中因为对象存活率高、没有额外空间对其进行分配担保,必须采用标记-清理或标记-整理算法来进行回收。

>>主要内存分配策略

(1)对象优先在Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
(2)大对象直接进入老年代:所谓的大对象就是指需要大量连续内存空间的JAVA对象,最典型的大对象就是那种很长的字符串和数组。经常产生大对象容易导致额外的GC操作,JVM中提供了一个-XX:PretenureSizeThreshold参数(这个参数只对Serial和ParNew这两个新生代垃圾收集器有效),
令大于这个参数的对象直接在老年代中分配,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。
(3)长期存活的对象将进入老年代
我们知道,JVM产生一个对象的时候,首先将其放在新生代的Eden区中,并且随着young gc的产生,大部分的对象都被回收了,那么“熬过”这次GC的对象呢?
JVM给了每个对象一个“年龄计数器”,所谓的年龄计数器就是指,这个对象熬过第一次GC,并且进入了Survivor区中,那么就将这个对象的年龄设为1,之后,每熬过一次GC,年龄+1,当这个值到达一个阀值(默认15,可通过-XX:MaxTenuringThreshold来设置)时,这个对象就会被移到老年代中。

 

>>垃圾收集器

不同的厂商实现由不同,不一而足。
Serial收集器
ParNew收集器(Parallel New)
Parallel Scavenge收集器
Serial Old收集器
Parallel Old收集器

 

 

转:http://www.cnblogs.com/binyue/p/3723250.html