jvm gc
一. 小例子说明
1 | void method() { |
对于以上代码,会生成2部分内存区域:
- obj 这个引用变量,会放到jvm stack 里面。
- 真正的object的class实例对象,会放到 heap 里面
当方法执行结束以后,stack里面的变量会马上被回收,而heap上的class实例,则会等待下一次gc的时候被回收 。
二. 垃圾的判断
哪些对象是垃依据什么进行判断的 ?
- 引用计数算法
给对象添加一个计数器 每一次引用 计数器加1,每一次引用失效,计数器减1
当计数器为0的时候,那么就可以被回收掉
一个很重要的缺陷就是 ,引用计数算法无法解决循环引用的问题 - 根搜索算法
使用根搜索算法判断对象是否存活,通过一系列被称为‘GC roots’ 的根为起点进行向下搜索,当一个对象到 gc roots 没有任何引用链相连,曾明此对象不可用。
gc roots 包括:- vm栈中(帧的本地变量)的引用 。
- 方法区的静态引用 。 jvm不要求实现,商业垃圾jvm基本都会实现。回收无用类和废弃常量 。
类的回收需满足如下条件 : 所有的实例都已经被回收 ,
加载该类的classloader已经被回收 ,
加载该类的 java.lang.Class对象已经没有在任何对象 被引用,包括反射 - jni本地方法引用
三. 常见 GC 算法
标记清除
分为【标记】和【清除】 2个阶段,先标记可以被回收的对象,然后再去逐一清除。
缺点:- 效率低下 2. 会产生不连续的的内存碎片 , 可能会导致后续操作无法找到足够的内存空间而触发二次的gc 。 3. 扫描所有对象,堆越大,时间越长。 gc次数越多,碎片化越严重。
标记整理
标记过程和之前是一样的,而后续步骤不是直接进行清理,而是将所以有存活对象一端移动,然后清理掉这这端边界以外的内存。
相比标记清除,不会产生碎片,但是所需时间更长。复制搜集算法
将可用的内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅仅将存活的对象赋值到另一块上面,然后将原来的空间一次性清除掉
优点:
实现简单,高效 不会产生碎片。
缺点:
将原来的内存空间缩小为原来的一半,带价也是挺高昂的。在对象存活高的时候,效率下降在现代商业vm中常用来回收新生代 。 新生代中划分为 Eden 和 2块较小的survivor ,每次使用 Eden和其中的一块survivor ,当回收时,将Eden和survivor中的存活对象,赋值到另一块啊 survivor中,然后清理 Eden和使用过的survivor。 hotspot中 eden和survivor 默认比例为 8 : 1
分代算法
当前商业虚拟机都是采用分代算法来实现,根据对象不同一般划分为如下区域:- java堆分为新生代和老年代,根据各个年代的特点采用不同的收集算法 。
譬如新生带每次gc都只有少量存活,那么选用复制算法 。 - 老年代采用 标记清除或者标记整理算法
新生成的对象被存放在年轻代 , 年轻代采用复制算法 算法(理论上年轻代的对象生命周期都非常短,适合复制算法)
年轻代分为三个区域 Eden 和 两个 survivor 区 。对象在Eden生成,当Eden区满了时候,此区域的存活对象会被复制到其中的一个 survivor区域,,当这个survivor区域也满了的时候,会存活的对象复制到另外一个survivor区域,。当第二个survivor也满了的时候,从第一个survivor复制过来的还存活的对象会被放到老年代 。
老年代存放经过一次或者多次gc还存活的对象,采用 标记清除或者标记整理算法进行gc
永久代,在jdk 8 中已经被取消。 它并不属于堆,gc也会涉及到这个区域 。它存放了每个class的结构信息,包括常量池,字段描述,方法描述 。
- java堆分为新生代和老年代,根据各个年代的特点采用不同的收集算法 。
内存分配:
- 堆上分配: 大多数情况下,会在Eden生进行分配,偶尔会分配到old上
- 栈上分配: 原子类型的局部变量
内存回收:
在 full gc 的时候会对reference类型的引用进行特殊处理
- soft: 内存不够时一定会被gc ,长期不用也会被gc
- weak: 一定会被gc,当被mark为dead
,会在referencequeue中通知 - phantom: 本来就没引用,当jvm heap释放 会通知
垃圾收集算法:
- young generation: serial, parnewe ,parallel scavenge
- old generation: cms , serial old , parallel old
minor gc:
时间短,频繁,复制算法执行效率高,当 Eden满了会触发
full gc:
对整个jvm进行整理。触发的实际: old满了,perm满了,system.gc。 他的效率很低,应当尽量避免
垃圾回收器的并行和并发:
并行: 多个收集器同事同做,但用户线程处于等待状态
并发: 在收集器工作的时候,用户线程也可以工作。但是在关键的步骤还是要暂停,比如标记阶段。 在清理阶段用户线程和gc线程可以同时工作 。
serial收集器:会暂停用户线程 , 默认的新生代收集器, 单线程实现。 在新生代采用 复制算法,老年代采用标记算法 。
parNew收集器: serial的多线程版本,使用多线程。其余的行为(对象份额 ,回收策略等)和serial一样
parallel scavenge 收集器: 多线程, 复制算法,吞吐量最大化 允许较长时间的stop the world。
parallel old : jvm 1.6 , 多线程,标记整理算法,注重吞吐量,gc停顿不太理想。