目录
GC的作用:
申请内存的时机和释放内存的时机
内存泄露和内存溢出
内存泄露
内存溢出
GC(垃圾回收的劣势)
GC(垃圾回收) 的工作过程
垃圾回收的过程:
第一阶段:找垃圾/判定垃圾
方案一:基于引用计数(非Java)
引用计数的缺陷
1、内存空间浪费严重(空间利用率低)
2、 会出现循环引用的问题
方案二:可达性分析(Java)
GCRoots是有哪些
一个引用置为null之后,它之前指向的对象会立刻被回收吗?
第二阶段:回收垃圾
1、标记清除
标记清除的问题:释放的内存碎片化(内存不连续),影响程序运行的效率
2、复制算法
复制算法问题: 空间利用率低(一半),开销大(垃圾少时)
3、标记整理
分代回收
GC:Garbage Clean(垃圾回收),我们在平时写代码的时候经常会进行申请内存,new操作,创建变量等等,但是内存是有限的,不断的申请会让内存耗尽,为了解决内存的消耗问题,引入了GC,这样就可以回收一些不用的内存,释放更多的空间出来。
申请内存的时机是比较好确定的,比如我们new,创建变量等等,但是什么时候不用这些变量就不容易知道。如果我们释放内存太早,但是后面还要用,那就尴尬了,如果我们释放的太晚了,内存不够后面申请了也是不行的。由于这些机制,就容易出现一些问题,常见的有内存泄露和内存溢出问题。
如果申请人在申请内存的过程中申请的内存越来越多,最后导致无内存可用的情况,这种现象就是内存泄露,垃圾回收就可以让我们程序猿不用关心内存泄露的问题,但是GC还是有一定的劣势的
内存溢出和上述问题没有必然联系,内存溢出指的是申请内存,没有足够内存提供给使用,比如一个long类型的数据申请int类型的空间大小,这就会导致内存溢出。
1、引入额外的开销(消耗的资源更多了)
2、影响程序运行的速度(并且GC还会出现STW(stop the work)问题,这也是C++不引入GC的重要原因,C++追求速度到极限)
首先JVM的内存区域划分为程序计数器,栈,堆,方法区(元数据区),其中栈中内存会自动回收,不需要GC,GC主要作用的区域就是我们的堆区,堆区存放着大量我们new出来的对象,GC要回收的对象都是些没有使用的,但是占着内存空间的对象。
这个方案就是引入一小块的内存空间,用来存放有多少个引用指向该对象,如果引用的数量为0了就代表可以进行回收。
比如:
public static void fun(){
Test t1=new Test();
Test t2=t1;
}
这个对于new Test()这个对象的引用计数就是2,当fun方法执行完毕的时候,栈上的栈帧就会消失,然后对new Test()的引用计数就会变成0,这个时候就可以GC进行回收了。
由此可见引用计数的缺陷很明显
使用引用计数,每次new一个对象的时候,都要引入一个计数器,这个计数器也是需要占据空间的,并且有时候占据的空间也不小,比如当我们的对象是4字节,计数器也是4字节的时候,这样的情况就非常的浪费空间。
通过一个例子来说明什么是循环引用:
比如我们要找宝藏:
如果这个例子不是很理解,我们用代码举例:
比如说这样一个类:
class Test{
Test test=null;
}
在测试类中创建该类实例:
public class TestDemo {
public static void main(String[] args) {
Test t1=new Test();
Test t2=new Test();
}
}
这个时候的引用对象图:
这时我们修改引用的指向:
public class TestDemo {
public static void main(String[] args) {
Test t1=new Test();
Test t2=new Test();
t1.test=t2;
t2.test=t1;
}
}
这时的引用对象指向:
直观一点:
这个时候如果我们将t1和t2置为null,这个时候这两个对象的引用计数就都会变成1,变成1之后相当于这样:
两个对象互相引用,这就导致外界没有办法访问这两个对象(和上面的寻宝藏一样),所以这两个对象永远都没有办法回收,也永远不能够使用,这不是我们想要的结果 ,还会造成内存泄露。
所以Java中不使用引用计数的方式来判定垃圾。
可达性分析就是通过一个线程来定期的扫描整个内存中的对象,扫描的过程类似于深度优先搜索 (起始位置一般为GCroots),把所有可以到达的对象都标记一遍,带有标记的对象就是可达的,没有标记的对象就是不可达的,也就是垃圾。(可以避免循环引用)
虽然说可达性分析避免了循环引用的问题,但是如果对象量比较大的情况下还是会花费大量时间进行搜索,比较消耗性能。
1、栈上的局部变量;
2、常量池当中的引用指向的变量;
3、方法区当中的静态成员指向的对象。
不会
一是因为即使一个引用置为空之后,并不代表这个对象就没有别的引用了。
二是因为可达性分析扫描是需要时间的,只有扫描过后判定是垃圾才会进行回收。
回收垃圾有三种策略:
1、标记清除
2、复制算法
3、标记整理
标记就是我们可达性分析的过程,标记完发现是垃圾的直接进行清除,释放内存即可
复制算法简单来说就是把内存一分为二,然后把正常的对象复制到另一边。然后把垃圾的那一边全部释放掉。(避免了内存碎片化)
然后把左侧的内存全部释放:
标记整理类似于数组中元素的移动,就是把不是垃圾的对象往前移动,是垃圾的往后移动,然后把垃圾一块回收。
标记整理的策略开销也是比较大的。
上述的方案都是单一的,实际上JVM中的方案不是单一的,而是结合上述方案的的策略,称为“分代回收”。
分代回收就是指对对象进行分类,按照“年龄”分成不同的类别进行回收。
对象的年龄:每熬过一轮GC扫描,年龄加1,年龄存储在对象头中
存储对象的内存区域划分为新生代和老年代
新生代中又分为了伊甸区和幸存区(幸存区有两个)
分代回收过程:
1、刚产生的对象放在伊甸区
2、熬过一轮GC,拷贝到幸存区(利用复制算法),大部分对象熬不过一轮GC
3、在后续的GC中幸存区的对象在两个幸存区来回进行拷贝(采用复制算法),进行对象的淘汰
4、经过了多轮的GC后,如果一个对象还是没有被淘汰,那么就会被放入老年代。对于老年代的对象来说,GC扫描的次数就远低于新生代了。同时,老年代当中采用的就是"标记——整理"的方式来回收。