垃圾回收之前,先来了解一下运行时数据是怎么存的。
先来一个最简单的例子。
public class Demo{
public static void main(String[] args){
System.out.println("Hello world");
}
private void methodA(){
int x = 5;
int y = 8;
int z = x + y;
}
private void methodB(){
}
}
这个demo,从运行到结束,都经历了什么流程,里面的数据都怎么处理了?其实,程序代码,都是由一个个线程跑起来的,main方法也不例外,启动一个线程,就会产生一个虚拟机栈。
动态链接,最常见的就是多态。
返回地址,就是一个方法执行完,要返回到哪里继续执行。
要注意,局部变量表存储的是8种基本类型,那对象类型呢?存哪里。
private void methodB(){
Object o = new Object();
}
其实方法里的对象变量,是存在堆里面的,但是局部变量表会有一个引用,指向堆里面的这个对象。
排除在堆中的对象变量,上面的一系列东西,都是跟随线程的生命周期的,就是线程私有,一个线程有一份,是个线程有十分。
接下来,我们看共享的部分,方法区和堆。这里就是线程共享的东西。这部分的数据,是共享的,跟线程的生命周期无关。
放方法区的数据:字节码、静态变量、常量
放堆的数据:对象
既然这部分的数据跟线程的生命周期无关,也就是说,当线程全部逻辑结束了,方法区和堆里面的数据,也不会立刻被销毁。同时又因为这是共享的数据,如果要销毁回收,一定要先知道还有没有其他地方在使用。而这里就需要了解内存的分代思想了,也就是JMM。
image.png 这就是JMM的内存模型。方法区:绿色的部分,jdk1.8以前是永久代,1.8以后是元空间,区别是什么呢?其实就是1.8以后,这一部分允许内存空间不连贯了。
堆:2个蓝色的部分,即新生代+老年代,而新生代又分为3个,Eden区和2个交换区S0和S1,他们的大小比例是8:1:1。
存放对象
当我们存放对象的时候,一般会遵循一些规则:
①先存放到Eden区
②如果新对象的大小>Eden区的大小,可以直接存到老年代
③长期存活的对象进入老年代(经历15次垃圾回收)
④担保原则
记住无论经过多少次垃圾回收,都只会在新生代和老年代这里,不会去到永久代或者元空间。元空间存放的是字节码、常量、静态变量的
GC垃圾回收
可达性分析算法:GCRoot(对象)、常量、静态变量
检查引用链,判断对象的存活。
如果对象存活,则不回收。而垃圾回收的过程,就是利用算法,对堆的清理和整理。
复制回收算法
image.png 把内存分为2个区,一个正在使用的,一个预留的,如果触发回收,则把使用区里面的不可回收对象,找出来并逐一放到预留区,然后把使用区擦除清空。(这时候,使用区和预留区就对换了,左边变成了预留区,右边变成了使用区)优点:简单高效,不会存在内存碎片,典型的空间换时间例子。
缺点:内存利用率低,永远只有一半在使用,当存活的对象越来越多,效率就越来越低。
标记清除算法
image.png 标记清除也很好理解,分2步,标记和清除。①扫描内存区,把可回收的对象标记
②把标记的对象进行回收
优点:存活对象很多时,效率高
缺点:碎片化,如果再有一个大的对象新建,由于没有足够的连续空间分配,会提前触发GC。
标记整理算法
image.png 标记整理则是:①把不可回收的对象标记
②把不可回收的对象进行整理,排在内存的一端连续空间内
③调整边界的指针,然后把边界外的空间进行清理
优点:标记清理算法的优化,避免了碎片的产生
缺点:效率会相对慢
那各个区分别用什么回收算法呢?
Minor GC
对新生代进行回收,不影响老年代。用的算法是复制回收算法,因为Eden区的对象,大部分都是生命周期很短的对象,用效率最快的算法。
Full GC
对整个堆进行回收,而其中的老年代和元空间,用的是标记清除和标记整理算法。由于是对整个堆进行回收整理,所以效率肯定相对慢,所以要尽量减少Full GC的次数。
总结
当我们不停地创建对象,就会先放到堆中的Eden区;
①如果Eden满了,就把存活的对象放到s0中;
②Eden和S0都满了,就触发Minor GC,利用复制回收算法,把存活对象整理到S1;
③交换区的S0和S1中的数据,每经历一次回收,计数+1,当经历过15次回收后还是存活,则会把这些对象转移到老年代。
④老年代被写满、主动调用System.gc则会触发Full GC,利用标记清除+标记整理算法进行回收。
另外,如果一开始就用存活的对象,塞满Eden和S0,那就直接全部转到老年代,然后把新生代的空间都回收了。
网友评论