Java的垃圾回收是不需要程序员去手动操控的,而是由JVM自己完成
首先我们要确定哪些对象是需要回收的?
- 引用计数法
在对象中添加一个引用计数器,新加一个引用时,计数器就加1,当引用失效时,计数器就减1。任何时刻只要计数器为0就代表对象没有引用可以被回收。
缺点:无法解决循环引用问题、额外内存来计数、额外开销
- 可达性分析
目前主流的算法
为了解决引用计数法的循环引用问题
基本思路:
通过一系列被称为GC Roots的根对象作为起始节点集,从这些结点开始,通过引用关系向下搜寻,搜寻路径成为引用链,如果某个对象到GC Roots没有任何引用链相连,就说明不可达,即可以被回收。
1、什么是对象可达?
双方存在直接或间接引用关系
根可达:对象到根存在直接或间接关系
2、GC Roots是什么?
垃圾回收时,JVM首先要找到所有的GC Roots,这个过程枚举根节点
----需要在safepoint时(堆对象状态是确定一致的)停止用户线程,即触发STW,然后从根节点向下寻,不可达就回收。
GC Roots就是对象,而且是JVM当前绝对不能被回收的对象
3、哪些对象可作为GC Roots?
全局对象
1)方法区静态属性引用的对象
Class对象很难被回收,只要Class对象不被回收,静态成员就不能被回收
2)方法区常量池引用的对象
例如:字符串常量池,本身初始化后不会在改变
执行上下文
1)方法栈中栈帧本地变量表引用的对象
线程在执行方法时,会将方法打包成一个栈桢入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中,只要方法还在运行,还没出栈,就意味着本地表量表还会被访问,可作为根节点
2)JNI本地方法栈中引用的对象
同上,无非一个是Java方法栈中的引用变量,一个是native方法栈中的变量引用
3)被同步锁持有的对象
被synchronized锁住的对象也是不能回收的
什么时候触发垃圾回收?
针对新生代:
当Eden满了,会触发minor GC
针对老年代:触发major GC 也成为Full GC
1)调用system.gc
2)老年代空间不足
只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则会触发Full GC
3)方法区空间不足
典型的垃圾回收算法?
- 标记-清除算法(Mark-Sweep)
标记阶段:标记出所有需要回收的对象
清除阶段:回收被标记的对象所占用的空间
缺点:内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
- 复制算法(Copying)
为了解决内存碎片化严重问题
将内存容量一分为二,每次使用其中一块,当这一块内存存满后,将尚存活的对象复制到另一块,把不可达的清除。
缺点:内存只有原来一半、存活对象增多的话,copying算法效率会降低
商业虚拟机的分配担保机制:
将内存分为一块较大的 eden 空间和两块较小的 survivor 空间,默认比例是 8:1:1,即每次新生代中可用内存空间为整个新生代容量的 90%,每次使用 eden 和其中一个 survivour。当回收时,将 eden 和 survivor 中还存活的对象一次性复制到另外一块 survivor 上,最后清理掉 eden 和刚才用过的 survivor
若另外一块 survivor 空间没有足够内存空间存放上次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
- 标记-整理算法(Mark-Compact)
标记阶段:标记出所有需要回收的对象
但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点:这类算法通常需要多次遍历堆空间
从效率上来说,标记-整理算法要低于复制算法
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
移动过程需要STW
- 分代收集算法(Generational Collection)
目前,大部分JVM采用的方法。
核心思想:
根据对象的存活的不同生命周期将内存划分为不同的域
新生代:每次GC都有大量垃圾需要被回收
朝生夕灭的对象(例如:方法的局部变量引用的对象等)。
老生代:每次GC只有少量对象需要被回收
存活得比较久,但还是要死的对象(例如:缓存对象、单例对象等)
永久代:
对象生成后几乎不灭的对象(例如:加载过的类信息)。在方法区中
目前,大部分JVM对于新生代采取copying算法
新生代按8:1:1区分为下面
每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
对象的内存分配执行过程:
1)主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。
2)当新生代的Eden Space和From Space空间不足时就会发生一次minor GC
3)GC后,Eden Space和From Space区的存活对象会被移动到To Space,然后将Eden Space和From Space进行清理-----如果To Space无法足够存储某个对象,则将这个对象存储到老生代。
4)在进行GC后,使用的便是Eden Space和To Space了
5)往复循环
6)当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。
老生代因为每次只回收少量对象,因而采用Mark-Compact算法。
区域较大,对像存活率高。
永久代指的是虚拟机内存中的方法区,采用Mark-Compact算法
永久代垃圾回收比较少,效率也比较低,但也必须进行垃圾回收,否则永久代内存不够用时仍然会抛出OutOfMemoryError异常
在jdk 7之后,原先位于方法区里的字符串常量池已被移动到了java堆中。
jdk8以后,使用元空间替代了永久代–元空间使用本地内存,而永久代使用的是JVM的内存