垃圾回收
标签(空格分隔): Java
那些对象需要回收
JVM内存结构主要包括: 方法区, 堆区, 程序计数器, 本地方法区, 虚拟机栈. 其中的程序计数器, 本地方法区, 虚拟机栈这三个区域是的生命周期随线程生灭, 所以不需要过多考虑这方面的GC问题.
引用
在JDK1.2之后, Java对引用的概念进行了补充, 总体分为四类: 强引用, 软引用, 弱引用, 虚引用. 这四种引用强度逐渐减弱.
- 强引用 : 指在代码中普遍存在的, 类似
Object object = new Object();
这类的引用, 只有强引用还存在, GC就永远不会收集被引用的对象. - 软引用 : 指一些还有用但并且非必须的对象. 直到内存空间不够时(抛出
OutofMemoryError
之前), 才会被垃圾回收. 采用SoftReference
类来实现软引用. - 弱引用 : 用来描述非必须对象. 当垃圾收集器工作时就会回收掉此类对象. 采用
WeakReference
类来实现弱引用. - 虚引用 : 一个对象是否有虚引用的存在, 完全不会对其生存空间构成影响, 唯一目的就是能在这个对象被回收的时候收到一个系统通知, 采用
PhantomRenference
类实现.
如何判断对象已死
1. 引用计数法
为每一个对象的的数据结构添加一个引用计数器, 用于统计指向该对象的引用的数量, 每当多一个引用的时候, 其引用计数器就加一, 当引用不再指向该地址之后计数器减一. 一旦计数器的值为0,则表示没有引用指向该对象, 则对象已经死亡 (
<<
寻梦环游记>>
说的: 人真正的死亡是所有活着的人的都忘了你的时候).
优点:
- 效率很高.
缺点:
- 每个对象都需要在其原有的数据结构之上添加一个引用计数器的属性.
- 循环引用的话就永远不会被标记为死亡.( 对象A引用了对象B, 对象B也引用了对象A )
2. 可达性分析.
目前JVM的主流垃圾回收器采取的可达性分析算法, 这个算法的实质在于将一系列
GC Roots
作为初始的存活对象的集合(Live Set
), 然后从该集合出发, 探索所有能过被集合直接或间接引用的对象, 并且将其加入集合之中, 这个过程就是标记过程. 最终, 未被探索到的对象便是死亡, 是可以回收的.
什么是GC Roots
, 其实就是由堆外指向堆内的引用.
- JMM虚拟机栈 栈帧中的局部变量.
- JMM方法区中的静态属性, 常量引用对象.
- JMM本地方法区中的, JNI中引用的对象.
可达性分析, 可以解决循环引用问题, 但是自身也存在一些问题, 比如说在多线程的情况下, 其他线程可能会更新已经访问过的对象的引用, 造成漏删. 解决方案是 进行两次可达性分析, 如果两次某对象都被标记则进行删除.
如何删除无效对象.
1. 标记清除算法
由名可得: 标记
->
清除. 得到需要清除的对象之后就直接进行清除.
优点: 速度快
缺点: 多次GC之后, 造成大量的碎片空间. 对于需要连续存储的较大对象无法存储, OutofMemoryError
2. 标记整理算法
由名可得: 标记
->
清除->
移动整理. 对标记清除算法
的一次改进, 但是因为移动操作, 所以时间成本较高.
优点: 没有内存碎片.
缺点: 时间成本较高.
3. 复制算法.
将可用的内存按容量分为大小两块, 每次只是用其中一块, 当这一块的内存用完了, 就将还存活着的对象复制到另一块内存上, 然后再把已使用的内存空间清理掉.
每次当内存分配的时候空间不够的时候, 都进行复制算法进行内存整理.
优点: 实现简单, 效率高. 解决了标记清除算法导致的内存碎片问题.
缺点: 代价太大, 将内存缩小了一半. 效率随对象的存活率升高而降低.
3.1 HotSpot虚拟机的改良算法
- 弱代理论
- 分代垃圾收集基于弱代理论, 具体描述如下:
- 大多分配了内存的对象并不会存活太长时间, 在处于年轻时代的时候就会死掉.
- 很少有对象会从老年代变为年轻代.
- 其中IBM研究表明: 新生代中的98%的对象都是 朝生夕死 ; 所以不需要按照1:1比例来划分内存.
- Hotspot虚拟机新生代内存布局以及算法.
- 新生代内存分配一块较大的
Eden
和两块较小的Survivor
空间. - 每次使用
Eden
和其中Survivor
空间. - 回收时将
Eden
和Survivor
空间中存活的对象一次性的复制到另一块Survivor
空间上. - 最后清理掉
Eden
和使用过的Survivor
空间.
Hotspot
默认Eden
和Survivor
的大小比例是8:1 .
分配担保:
如果另一块Survivor
空间没有足够的内存来存放上一次新生代收集下来的存活对象, 那么这些对象则直接通过担保机制进入老年代.
4. 分代收集算法
当前商业虚拟机的垃圾收集器都是采用分代收集算法, 根据对象存活周期的不同将内存划分为几块. 一般把
Java
堆分为新生代, 老年代. JVM根据各个年代的特点采用不同的手机算法.
- 新生代中, 每次进行垃圾回收都会发现大量对象死去, 只有少量存活, 因此比较适合复制算法. 只需要付出少量的存活对象的复制成本就可以完成收集.
- 老年代中, 因为对象的存活率比较高, 没有额外的空间进行分配担保, 所以比较适合标记-清理, 标记-整理算法来进行回收.
Java堆是JVM所管理的内存的最大的一块, 也是GC主要的工作区. 其主要分为两个区
年轻代
和老年代
, 其中年轻代又分为Eden
和Survivor
区, 其中Survivor
区又分为FROM
和To
两个区. 可能这个时候大家又会有疑问, 为什么需要Survivor
区, 为什么Survivor
还要分为两个区?
Eden
区 : IBM表示有98%的对象是朝生夕死, 所以针对这一现状, 大多数情况下, 对象会在新生代Eden
区中进行分配, 当Eden
区没有足够的空间进行分配的时候, 虚拟机会发起一次GC
. 通过这次GC
之后,Eden
会被清空,Eden
区中绝大部分的对象会被回收, 而那些无需回收的存货对象, 将会进到Survivor
的FROM
区(若FROM
不够, 则直接进入Old
区).
Survivor
区: 相当于是Eden
区和Old
区的一个缓冲, 类似于我们交通中的黄灯.Survivor
又分为两个区, 一个为From
区, 一个是To
区. 每次执行Minor GC
会将Eden
区和FROM
存活的对象放到Survivor
的To
区(如果To
则直接进入Old
区).
不是新生代到老年代么, 直接中
Eden
到Old
不好了么,为什么要这么复杂. 如果没有Survivor
区,Eden
区每一次GC
, 存活的对象就会被送到老年代, 老年代很快就会被填满, 而虽然有很多对象虽然一次没有被消灭掉,但是也存活不了太多次, 这个时候将其移入Old
区会很快的将其填满.
所以
Survivor
的存在意义就是减少被送到老年代的对象, 进而减少GC
的发生.Survivor
的筛选保证, 只有经历16此的GC还能再新生代存活的对象,才会被送到老年代.
Old区占据着
2/3
的堆内存空间,只有在Marjor GC
的时候才会进行清理, 每次GC
都会出发stop-the-world
. 内存越大,SWT
的时间也越长, 所以内存也不仅仅是越大越好, 由于复制算法的对象存活率较高的老年代会进行很多次的复制操作, 效率很低, 所以老年代这里采用的是 标记整理算法.
除了上述所说, 在内存担保机制的情况下, 无法安置的对象也会直接进入老年代, 以下几种情况也会进入老年代.
- 大对象: 指需要大量连续内存空间的对象, 这部分对象不论是不是"朝生夕死", 都会直接进入到老年代, 这样做主要是为了避免在
Eden
区以及两个Survivor
区之间发生大量的内存复制, 当你的系统有非常多的"朝生夕死"的大对象的时候, 就值得注意了. - 长期存活对象: 虚拟机给每个对象定义了一个对象年龄(Age)计数器. 正常情况下的对象会不断的在
Survivor
的FROM
和To
之间进行移动, 对象在每经历一次Minor GC
, 年龄就会自增1, 当年龄增加到15岁的时候, 就会被移入老年代. 这里的15是可以进行自定义的. - 动态对象年龄: 虚拟机并不重视对象的年龄必须大于15岁, 才会放入老年区, 如果
Survivor
空间中相同年龄所有对象的总和大于Survivor
空间的一般, 年龄大于等于该年龄的对象就可以直接进入老年区.