JVM 之 GC(Garbage Collection)基础
《深入理解Java虚拟机》学习笔记
java与C++的垃圾管理
我们都知道Java与C++最大的区别就在于对内存的管理。
C++有析构函数,所以程序员能很明确的清除类所占用的空间。
Java有自己的垃圾回收机制,所以一般不需要程序员自己主动去释放空间。但是为了适应C++程序员的转型,Java增加了finalize()方法,在GC回收之前调用,可以让程序员主动去清除类所占据的内存空间。但是finalize()的执行又具有不准确性。
public class Main {
public static Main SAVE_TEST=null;
public void isAlive(){
System.out.println("我还活着!");
}
@Override
protected void finalize()throws Throwable{
super.finalize();
System.out.println("内存清除方法运行");
Main.SAVE_TEST=this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_TEST=new Main();
int n=3;
while(true) {
if (n == 0) {
break;
}
SAVE_TEST = null;
System.gc();//主动清理内存
//因为finalize()方法优先级很低,所以暂停一会儿等他先运行
Thread.sleep(500);
if (SAVE_TEST != null) {
SAVE_TEST.isAlive();
} else {
System.out.println("我没了");
}
n--;
}
}
}
/**
*内存清除方法运行
*我还活着!
*我没了
*我没了
*/
明明我们第一次已经调用System.gc();但是我们看到清除方法执行之后,Main类又复活过来了。
这里就体现了finalize()的方法执行原理,以下部分引至《深入理解Java虚拟机》第三章的部分文字:
如果一个对象被判定为有必要执行finalize()方法,那么这个对象将被放置在一个叫做F-Queue的队列中,
并在稍后由一个由虚拟机自动建立的、 低优先级 的FInalizer线程去执行它,
这里的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。
这里得出两点:
- Java虚拟机不保证finalize方法会被及时调用(低优先级)
- 不会保证它会被完全执行,甚至不保证其执行
而且finalize方法至多由GC执行一次,第一次标记时将其加入F-Queue队列。第二次再标记到,就直接将其加入“即将回收”的集合。这也是为什么之前程序,第一次Main活力,后面两次都死了。所以,在垃圾回收的时候,更多的应该依赖各个“代”的回收算法,而不是finalize()甚至书中建议,大家忘了有finalize()这个方法。
清除前的准备 判断对象是否存活
引用计数法
之前我老师上课一直讲的Java的判断对象是否存活的方法就是这个方法。
原理
引自《深入理解JAVA虚拟机》3.2
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;
当失去引用时,计数器值就减1;任何时刻该计数器为0的对象就是不可能再被使用的。
但是,Java并不是这样做的。因为引用计数法不能解决两个对象互相引用的问题。
public class Main {
public Object instance=null;
//加入这个为了方便观察内存中的变化
private byte[] bigSize=new byte[2*1024*1024];
public static void main(String[] args) throws InterruptedException {
Main a=new Main();
Main b=new Main();
a.instance=b;
b.instance=a;
a=null;
b=null;
System.gc();
}
}
//没有a=null;b=null时GC情况
//[GC (System.gc()) [PSYoungGen: 8030K->4888K(57344K)] 8030K->4896K(188416K) 4M多
//a,b置null
//[GC (System.gc()) [PSYoungGen: 8030K->808K(57344K)] 8030K->816K(188416K) 816K
上面的例子,我们看到对象a和b的instance属性都互相引用对方的Main实例。当主动将对象a和b的引用置null的时候,其在堆中的Main实例引用还被对方的instance所用。即计数器为1,按理GC的时候,这两个Mian实例并不会被释放。然而却被释放了,堆中并没有。所以证明Java并不是用这个方式判断对象是否存活。
可达性分析算法
主流的Java虚拟机都是用可达性分析来判断Java对象是否还存活的。
原理
通过一系列的“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链的时候,证明这个对象不可用(死了)。
能作为GC Roots的对象包括
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JIN引用的对象
准备清除 GC算法
主流虚拟机采用分代搜集法来处理垃圾回收。大体分为新生代、存活代、老年代。
存活代存活在新生代内存中(自己理解的不知道对不对)。
标记-清除算法
最基础的回收算法。是后面所有算法的基础。虽然不怎么用。
原理
在堆中将内存划分为等大的块。每个块存中三种可能:对象存活、对象可回收、未使用。
首先虚拟机对所以需要回收的块标记(前面所的两次标记法)。再标记完成后统一回收所被标记的对象。
回收前:(空表示未使用)
存活 | 回收 | |
---|---|---|
回收 | 回收 | 存活 |
回收 | 回收 | |
回收 | 存活 | 回收 |
回收后:
存活 | ||
---|---|---|
存活 | ||
存活 |
这样的方法存在两个 问题:
- 标记和清除的效率都不高
- 如果运气不好只有一个各自未使用,又来了要两个格子的对象。不得不再GC一次。
复制算法
新生代、存活代处理回收算法。
原理
**最开始的复制算法 **:将内存分为大小相等的两块A,B。如果其中一块(A)内存用完了。就把上面存活的对象复制到另一半的内存(B)中,然后清空当前这内存块(A),然后使用另外一半内存块(B)。一直循环。因为只需要复制并且是按顺序分配内存,并且也不用担心内存连续问题。效率高了很多,但是却一直要空着一半的内存。花销太大。
改良后的复制算法 :IBM公司研究表明新生代有98%的对象都是经常死亡的对象。所以完全可以不用浪费一半。将新生代内存按8:1:1的大小分别分配给新生代、存活代A、存活代B。A,B之间还是用最初的复制算法循环使用。当回收时,直接将 新生代 和 存活代 中的 存活对象 全部复制到另一个存活代中。
理论上理想情况下改良后的复制算法已经足以支撑所有的垃圾回收。但是如果运气不好,存活的对象超过存活代的大小。就需要依赖其他的内存来分担压力了。所以有了老年代。
标记-整理算法
复制算法,在对象存活率较高的情况下,会进行很多的复制操作。而到了老年代的对象。进过多次回收都还坚挺。其上的对象死亡率会很低。所以有了标记整理算法。
原理
标记依旧。当回收的时候,直接将存活的对象向一端移动。然后清空最后一个存活对象后面的空间。