深入理解java虚拟机(2)-----Java 垃圾收集器和内存分配策略

时间:2022-12-27 12:34:11

Java 垃圾收集器和内存分配策略

<深入理解java虚拟机>读书笔记
java和C++之间有一堵有动态内存分配和垃圾回收所构成的高墙,墙内的人想出去,强外的人想进来 —— 作者的这句话很经典


主要围绕三个问题进行分析
- 那些内存需要回收?
- 什么时候回收?
- 如何回收?


那些内存需要回收?

java内存模型http://blog.csdn.net/chenqianleo/article/details/77596409
上个博客我们了解了java的内存模型,java内存模型中的那些区域是需要进行内存回收的。我们主要关注的是堆内存,其次是方法区内存

    其中程序计数器,虚拟机栈,本地方法栈都随着线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出进行压栈和出栈的工作,每一个栈帧分配多少内存在类结构确定是都是已知的,这几个区域的内存分配和回收都具有确定性,当方法结束或者线程结束时内存自然就回收了
但是java堆和方法区说我不一样,一个接口中的多个实现类内存可能不一样,一个方法中的多个分支内存也可能不一样只有运行期间才知道要创建那些对象,内存的分配和回收都是动态的,所以gc主要是针对这一部分内存

什么时候回收?

在堆里面存放着所有的对象实例,在垃圾收集器堆堆进行回收时,要确定对象是否存活,gc对死亡的对象进行回收

如何判断对象死亡

  1. 引用计数法
    核心思想: 给对象添加一个引用计数器,每当一个地方引用它,计数器值就加1,当引用失效时,计数器值就减1,当计数器的值为0就是不可能被使用的
    特点: 实现简单,判断效率也很高,但是很难解决循环引用的问题
  2. 可达性分析算法
    核心思想: 通过一系列被称为“Gc Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径称为引用链(Reference Chain)当一个对象到GcRoots没有任何引用链时就证明对象是不可用的
Java语言中,可作为GC Roots的对象包括下面几种
虚拟机栈(栈帧的本地变量表)中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法中

对象的死忙与重生

执行结果
# in finalize method executed!
# i am alive :)
# i am dead :(
public class FinalGc {
public static FinalGc instance = null;
public void isAlive(){
System.out.println("i am alive :)");
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("in finalize method executed!");
instance = this;
}

public static void main(String[] args) throws InterruptedException {
instance = new FinalGc();

//第一次
instance = null;
System.gc();
//休眠是因为执行finalize()的Finalizer线程优先级比较低,等待去自救
Thread.sleep(500);
if(instance != null){
instance.isAlive();
}else {
System.out.print("i am dead :(");
}

//第二次,因为第一次自救成功了,第二次无法执行finalize()所以死忙了
instance = null;
System.gc();
Thread.sleep(500);
if(instance != null){
instance.isAlive();
}else {
System.out.print("i am dead :(");
}

}
}

在对象的可达性分析不可达时,对象也不是必须死,这时相当于缓刑阶段,对象并没有立即死忙,对象的死忙要经历两次标记过程。
在可达性分析完成后如果没有与GC roots连接的引用链,对象将被第一次标记并进行一次筛选,当没有finalize()方法或者已经执行过一次了那么就真的被回收了,如果有复写了finalize()方法,可以在方法内进行自救,只要是自己重新与引用链的对象就行管链就可以了,这时就会移除即将回收的对象集合

方法区的回收

方法区的回收主要是无用的类和常量,判断一个类是否“废弃”比较简单,但是判断一个无用的类比较困难,有下面三个条件

  • 该类的所有实例已经回收
  • 加载该类的classLoader已经回收
  • 该类的java.lang.Class对象在任何地方没有被引用,无法通过反射访问到该类的方法

如何回收?

垃圾收集算法

具体的画图就不做了,在网上找了一篇博客http://www.cnblogs.com/xiaoxi/p/6486852.html是对<深入理解java虚拟机>这一部分的总结,我这里直接copy该博主的照片和文字

1.标记清楚算法
标记-清除算法就如同它的名字样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。
深入理解java虚拟机(2)-----Java 垃圾收集器和内存分配策略

2、复制(Copying)算法
复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。复制算法的执行过程如图:
深入理解java虚拟机(2)-----Java 垃圾收集器和内存分配策略
不过这种算法有个缺点,内存缩小为了原来的一半,这样代价太高了。现在的商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

3、标记-整理(Mark-Compact)算法
复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。标记-整理算法的工作过程如图:
深入理解java虚拟机(2)-----Java 垃圾收集器和内存分配策略

4.总结

现在的虚拟机都采用分代收集算法,就是上面算法的结合,在大批对象死去,少量对象存活的新生代采用复制算法,在存活率高,没有额外空间进行担保的老生代采用标记-清除或者标记-整理算法
深入理解java虚拟机(2)-----Java 垃圾收集器和内存分配策略


分析GC日志

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 

1.日志开头的有GC ,Full GC ,Major GC , Minor Gc,棋子Minor Gc是发生在新生代的垃圾回收动作,比较频繁,回收速度也快;FullGc ,Major Gc都是老年代的Gc速度是Minor Gc的10倍以上;Full GC的写法是“Full GC(System)”,这说明是调用System.gc()方法所触发的GC
2.“[DefNew”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的
3.后面方括号内部的“310K->194K(2368K)”、“2242K->0K(2368K)”,指的是该区域已使用的容量->GC后该内存区域已使用的容量(该内存区总容量)。方括号外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”则指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆总容量)
4、再往后“0.0269163 secs”表示该内存区域GC所占用的时间,单位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”则更具体了,user表示用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束经过的墙钟时间。后面两个的区别是,墙钟时间包括各种非运算的等待消耗,比如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以如果看到user或sys时间超过real时间是完全正常的。
5、“Heap”后面就列举出堆内存目前各个年代的区域的内存情况。

内存分配策略

在下面提到的各种情况有好多可以通过虚拟机参数进行设置,时间有限,要把时间用在最有价值的东西上就不进行记录了,所以想要深入理解的请阅读书籍
1.对象优先在Eden分配
大多数情况下,对象在新生代Eden中分配,当Eden中没有足够的空间进行分配时,虚拟机发起Minor Gc
2.大对象直接进入老年代
大对象是指需要大量连续内存空间的java对象,就是字符串和数组,因为大对象在新生代的Eden和Survivor的复制工作很大,并且可能还需要老生代进行担保,所以直接进入老年代
3.长期存活的对象进入老年代
4.长期存活的对象中对象年龄的动态判断
5.空间分配担保,在发送Minor Gc之前,虚拟机会检查老年代中最大可用的连续内存是否大于新生代所以对象之和,如果条件成立,那么这次Minor Gc是安全的,否则要进行设置运行担保失败