标签:错误 新生代 无法找到 本地变量 技术 是否存活 相等 检查 定义
Java和C++之间有一堵由内存动态分配和垃圾收集技术所为围城的高墙,墙外的人想进去,墙外的人想出来。简单可以理解为:JAVA虚拟机(JVM)将程序员和内存分离,所以相应的在JVM中,程序员不能直接操作内存也就成为它的劣势了。
那么垃圾收集机制(GC Gabage Collection)具体的工作过程是怎么样的呢?
对于应该被回收的内存,内存中保存的Java对象一定是“死亡”状态的对象,那么JVM怎么判断对象死亡状态呢?主要有两种算法:
给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,就说明没有任何对象引用该对象,就说明该对象的状态已经“死亡”,就可以被判定为待回收。Java不是用该算法判断对象是否存活。
public class A{
private B b;
public void setB(B b) {
this.b = b;
}
}
public class B{
private A b;
public void setA(A a) {
this.a = a;
}
}
public static void main(String [] args){
A a = new A();
B b = new B();
a.setB(b);
b.setA(a);
a = null;
b = null;
System.gc();
}
可以看到对象a和对象b之间相互进行了引用,其余地方并没有对两个对象进行引用,但是两块内存的引用计数并不会为0,所以不会将两块内存划分为待回收的内存。
2.可达性分析算法
在主流的Java,C#这类程序语言中,都是通过可达性分析算法判定对象是否存活的,基本思想就是定义一个GC Root对象,以GC Root为起点,向下搜索,走过的路径称作引用链,当一个对象到GC Root之间没有任何的引用链可以连接时(就是从GC Root到这个对象之间不可达),表示这个对象可以被回收了。
在Java中,可以作为GC Root的对象包括以下:
在虚拟机通过可达性分析算法进行内存“死亡”状态判断时,就可以避免循环引用的问题,所以主流的Java虚拟机都会采用这种方式进行垃圾回收的第一个步骤。
在JVM中,进行垃圾回收主要有以下几种方式:
接下来分别进行详细介绍:
标记清除算法是最基础的收集算法,包括两个过程:标记过程和清除过程。
首先标记处所有需要收集的对象,在标记完成后触发垃圾回收。标记过程就是上述提到的怎么判定对象需要收集的过程。但是这个算法存在两个缺陷:
第一个是效率问题,标记和清除阶段的效率都不高;第二个是空间问题,在标记-清除之后会产生大量的内存碎片,内存碎片太多会导致程序为大对象分配内存空间时无法找到合适的内存进行分配,就会再一次触发垃圾收集机制。
2. 复制算法
针对标记-清除所存在的效率和空间问题提出第二种算法:复制算法。
原理:将内存划分为相等的两块,每次只使用其中的一块,在第一块内存使用完之后,触发一次垃圾回收,将还存活的对象复制在第二块内存中,然后将第一块已使用的内存全部清除调,之后的内存分配发生在第二块当中,当第二块使用完成之后,继续重复上述步骤。这样就不用考虑内存碎片问题,而且每次只需要回收第一块内存,效率会有所提高。
但是这种算法的代价就是将内存分为两块,每次只使用其中一块,所以在后来的分代算法中,只使用这种算法来回收新生代,而且两块内存的分配也不是各占一半。
3. 标记-整理算法
复制算法虽然解决了一些问题,但是当对象存活率较高的情况下,就需要进行多次的复制操作。效率会变低。更重要的是我们不愿意浪费一半空间,就需要分配额外的空间做内存担保,以应对使用的内存中对象100%存活的极端情况。
有人就提出标记-整理算法,也分为两个阶段:标记阶段和整理阶段。整理阶段就是在标记完成之后,让所有存活的对象向内存中的一边进行移动,将死亡对象挤出内存。
4. 分代收集算法
其实分代收集算法是以上标记-整理算法和复制算法的结合:根据对象存活周期将Java堆内存分为年轻代和老年代,年轻代使用复制算法,将年轻代分为一个Eden区和两个Survivor区,内存占比大体为8:1:1,每次只使用Eden区和一个Survivor区,在需要回收时,就将存活的对象全部移动到另外一个Survivor区,然后清除掉Eden区和第一个Survivor区。之所以在年轻代中选择使用复制算法,是因为在年轻代中的对象大多是朝生夕死的对象,存活率比较低,所以可以付出少量的内存作为代价进行回收。但是老年代中对象的存活率相对于年轻代较高,也不存在额外的空间进行内存担保,所以就使用标记整理算法进行收集。
在Java所提供的自动内存管理中,我们最终可以归结为自动化的解决了两个问题,一个是对象内存的分配,一个是如何回收分配给对象的内存,关于回收内存,上面已经讲述过了,接下来简单了解一下对象内存分配的知识。
大多数情况下,对象的内存分配会发生在Eden区中,当Eden区没有足够的内存时,将会触发一次Minor GC。
Java中的垃圾回收分为Minor GC 和 Full GC,Minor GC意为年轻代GC,由于年轻代对象大都为朝生夕死的对象,所以Minor GC 发生频率比较频繁,速度也比较快。
我们所说的Full GC 为老年代GC,但是当出现Full GC时,一般都会伴随着至少一次的Minor GC,不过这种情况并非绝对,主要还是和垃圾收集器的收集策略有关。
2.大对象直接进入老年代
大对象是指需要消耗大量连续的内存空间的Java对象,例如长度较大的字符串或者数组,大对象的出现对于虚拟机来说是不利的,但是如果我们的大对象是朝生夕死的,那么将是极为不利的情况,因为大对象的出现经常会导致内存还有大量的空间就提前触发垃圾回收动作。
3.长期存活的对象进入老年代
由于虚拟机会采用分代回收机制,所以就没有必要识别哪些对象需要放在年轻代,哪些对象需要放在老年代中,在虚拟机中会为对象定义一个年龄计数器,如果对象在Eden经历过一次垃圾回收后还存活,移动是Survivor中,则对象年龄+1,之后每经历一次垃圾回收,如果对象还存活,则将对象年龄+1,当对象年龄达到设置阈值时,将被移动到老年代,默认阈值为15,可以通过-XX:MaxTenuringThreshold来进行设置。
4.空间分配担保
在发生Minor GC之前,虚拟机会检查老年代中的最大的可用连续内存空间是否大于年轻代中所有存活对象的空间,如果可以条件成立,则Minor GC 是最安全的,因为老年代可以满足所有年轻代存活对象进入老年代的极端情况。
如果不满足,虚拟机则会查看HandlePromotionFailure 设置是否允许担保,如果允许,则检查老年代中的最大的可用连续内存空间是否大于之前每次晋升老年代对象的平均大小,如果满足就会触发一次Minor GC ,如果不满足则会触发一次Full GC。
但是这种策略在JDK6之后有了改变:老年代中的最大的可用连续内存空间如果大于之前每次晋升老年代对象的平均大小或者新生代中存活对象的总大小,则触发一次Minor GC,否则进行Full GC。
其实归根结底,Java中的内存分配和垃圾回收策略都是为了使内存得到更高效的理由,并且希望将更多的内存管理交由机器来处理从而解放程序员劳动力,是程序员可以从复杂的内存管理中抽身并减少人为管理内存中出现的错误。
标签:错误 新生代 无法找到 本地变量 技术 是否存活 相等 检查 定义
原文地址:https://www.cnblogs.com/dongmi/p/11038965.html