您的位置:首页 > 编程语言 > Java开发

Java内存区域分配、存储、垃圾回收策略与回收机制(深入JVM虚拟机)

2015-07-19 14:43 1176 查看

1. Java垃圾管理机制

对象已死判断方法:

1引用计数法,2可达性分析算法(由GC ROOTs到该类是否可到达)

引用:

强引用:在代码中普遍存在,用new生成对象,这样的强引用永远不会回收掉引用的对象

软引用:在系统内存溢出前,会把这类对象进行第二次回收,如果这次回收后内存还是不足,会抛出OOM错误,提供了SoftReference实现软引用。

弱引用:被弱引用关联的对象只能生存到下一次GC发生之前,当GC工作时,无论当前内存是否足够,都会回收掉弱引用对象,WeakReference类,如WeakHashMap

虚引用:无法通过虚引用获得实例,为对象设置虚引用的唯一目的就是在这个对象被GC回收时收到一个系统通知,PhantomReference类。

回收对象:任何对象的finalize()方法只会被系统执行一次(System.gc()后执行对象的该方法)

回收方法区:

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。废弃常量如字符串“abc”进入了常量池,当前系统没有任何String对象为“abc”,也没其他地方引用了该字面量,此时内存回收的话,该”abc”就会被清理出常量池;判定无用类:

该类所有实例都已被回收,即java堆内不存在该类实例

加载该类的ClassLoader被回收

该类对应的java.lang.Class对象没在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述条件的类进行回收,仅仅是“可以“,不是必然的。

垃圾收集算法(为了清理内存,尽量留出较多连续的内存区域):

一般都采用分代收集算法,java堆分为新生代和老年代,根据年代不同采用最合适的算法;在新生代中,每次垃圾收集时都发现有大批对象死去,少量存活,用复制算法(复制那些存活的,将这些副本填充到要回收的对象的内存空间上,留出更多连续的内存区);老年代中因对象成活率高、没额外空间对它进行分配担保,采用标记-清理(标记完清楚已死亡的,会导致内存碎片化)或标记-整理(清理完后将保留的对象向前移动,保证前面内存尽量填满,剩下的内存区域就基本上连续了,不需要复制对象)的算法。

一般都用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor;当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor中,最后清理掉Eden和第一块Survivor空间上的内容(HotSpot中默认Eden:Survivor是8:1,每次新生代最多占90%(两块Survivor有20%,一般来说新生代回收的变量占了98%,只有2%的变量需要复制保留),当然,当Survivor空间不足,需要依赖其他内存,一般用老年代部分区域进行分配担保(Handle
promotion)
)





什么是分担担保?

在minor GC之前,JVM会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若该条件成立,minor GC可保证安全;若不满足会检查老年代中连续空间是否大于历次晋升到老年代的对象的平均大小,若该条件满足,虽minor GC有风险也会进行,若不满足第二个条件,会进行FullGC。(直白一点,先看老年代中是否能装下新生代的所有对象?若不能则看是否能装下有可能从新生代转换为老年代的对象?若这也不能,就FullGC,否则就MinorGC)第二步中存在风险,若确实发生担保失败,会重新执行一次FullGC,这样代价就大一些,因为预防工作没做好,minorGC完成不了还搭上去一次Full的。

老年代由于可能有100%存活,所以复制算法会导致空间不足,一般使用标记-整理算法。

JVM一般都采用分代收集的办法。

垃圾收集器:有串行并行收集器等等多种

2. 内存分配和回收策略:

对象内存分配,主要在堆上分配,

· 对象优先在Eden分配,当Eden没有足够内存分配,JVM会发起一次Minor GC。若还是不够则分配到老年代。

吃个栗子:

比如3个2MB和1个4MB的对象分配到堆大小为20MB,不可扩展,10MB为新生代,10MB为老年代,Eden区和Survivor区空间比例为8:1,可见Eden区8MB,fromSurvivor 1MB, toSurvivor 1MB,新生代总共9MB。先分配前3个2MB后都放在Eden区,在放入4MB对象时发生MinorGC,而此时前面3个2MB对象都存活,若回收则通过复制算法放到fromSurvivor区,但是fromSurvivor区只有1MB,放不下这些对象,因此通过分配担保机制将这3个2MB对象放到老年代。

所以此次GC结束后,4MB对象放在Eden区,Survivor空闲,老年代占用6MB(3个2MB对象)

使用JConsole观察HotSpot上内存分配状况:



代码为:

public class oomTest {
static class OOMObject{
public byte[] placeHolder =  new byte[64 * 1024];
}
public static void fillHeap(int num) throws Exception{
List<OOMObject> list = new ArrayList<OOMObject>();
for(int i = 0;i < num;i++){
Thread.sleep(50);
list.add(new OOMObject());
}
}
public static void main(String[] ag) throws Exception{
fillHeap(1000);
System.gc();
Thread.sleep(10000);
}
}
可见Eden区在分配内存时,若超过Eden限制大小,会考虑放到将需要保留的对象放到Survivor区,若Survivor放不下则晋升为老年代,可见图中老年代最后都接近100%了,如果老年代也满了就要发生Full GC了(标记-整理)。

· 大对象直接进入老年代

大对象是指需要大量连续内存空间的对象。如很长的数组。部分虚拟机可以通过-XX:PretenureSizeThreshold参数来设置大于该值的对象直接分配到老年代,这样的目的是避免Eden区和两个Survivor区之间发生大量拷贝;当然,如果该对象直接大于了Eden和1个Survivor区(即大于新生代总空间),直接放入老年代。

· 长期存活的对象进入老年代(Tenured Generation)

虚拟机给每个对象都有年龄计数器,当默认超过15次GC会晋升到老年代。通过-XX: MaxTenuringThreshold=15来设置。

· 动态对象年龄判定

若在Survivor空间中相同年龄的所有对象大小之和大于Survivor空间的一半,年龄大于或等于该年龄的对象就会直接进入老年代,无需等到上面的年龄阈值。

新生代(Eden+ fromSurvivor区,new Generation):新生对象一般在新生代Eden区分配。

老年代:大对象,长期存活的对象(虚拟机给对象定义了对象年龄计数器,当经过GC之后能活过的就增加一岁,超过15岁就会到老年代中)

MinorGC和Full GC有什么区别?

新生代GC(Minor GC)指发生在新生代的垃圾回收动作,特点:频繁、速度快

老年代GC(Full GC):发生在老年代的垃圾回收动作,经常会伴随至少一次MinorGC,特点:速度慢,按收集策略回收

3. Java内存区域存储

程序计数器:当前线程执行到的字节码的行号,线程私有

Java虚拟机栈:线程私有,java方法执行的内存模型,存储方法出入口等

本地方法栈:nativemethod,native方法服务相关的方法

Java堆:各线程共享,存放对象实例,不需要连续内存new class()..

方法区:各线程共享的区域,存储已加载的类信息,常量、静态变量、编译后的代码数据等。不需要连续内存

运行常量池:是方法区的一部分,存放编译器生成的各种字面量和符号引用,运行期也可将新常量放入常量池,如String对象的intern(),就是在常量池添加对象,还有String a = “abc”也是直接在常量池
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: