如何判定是否可回收
引用计数法:当对象被引用则+1,但对象引用失败则-1。当计数器为0时,说明对象不再被引用,可以被可回收 引用计数法最明显的缺点就是:如果对象存在循环依赖,那就无法定位该对象是否应该被回收(A依赖B,B依赖A)
可达性分析算法: 从「GC Roots」开始向下搜索,当对象到「GC Roots」都没有任何引用相连时,说明对象是不可用的,可以被回收。「GC Roots」是一组必须「活跃」的引用。从「GC Root」出发,程序通过直接引用或者间接引用,能够找到可能正在被使用的对象
可作为GCRoot的点:
- 1 方法区中静态属性 和 常量 引用的对象。
- 2 虚拟机栈 和 本地方法栈中引用的对象
- 3 活跃线程的成员变量
如何回收
既然内存可以看成一个表格,那么怎么回收里面无用的格子呢,大概有两种策略:
- 1 先标记无用的,然后把无用的全部清理;
- 2 先标记有用的,然后把有用的复制下来,再一次清理整个格子。
可以细分为下面3种方法:
-
1 标记-清理 先从上到下从左到右扫描整个内存块,将无用的内存进行标记,然后再清理所有被标记的内存格,这样有两个缺点;1 如果回收的内存不连贯,就会造成大量的内存碎片 2 如果回收的内存比较多,则效率底,比如共有100个格子,结果其中99个格子都被标记了,那就需要回收99个格子,太慢了,效率低。
-
2 标记-整理 先从上到下从左到右扫描整个内存快,将无用内存进行标记,然后将无用内存都移动到一端,有用内存就在另一端,然后直接对无用内存的一端清除即可。优点就是避免了内存碎片,缺点还是效率低,回收的越多越慢。
-
3 复制算法 将内存分为大小相同的A、B两块,每次使用其中一块,比如A(我们将A简称为使用块,将B简称为存活块),满了则对A进行扫描,然后将A上面存活的对象复制到B上,然后再将A全部清除,然后交换A、B的角色。优点就是快,不用遍历回收,而是一次直接清空A,缺点就是浪费内存,一次只能用一半内存,这样变相提高了gc()的频率,不划算。假如我们知道每次回收都能回收很多的对象,比如回收4/5, 也就是只有1/5存活,那么我们就可以将A、B的比列调整为4:1,也就是4/5给A,用来放置创建的对象,1/5给B,用来放置回收后剩下的对象,那么万一有几次存活的对象超过了1/5怎么办呢,我们就需要内存担保来处理这种情况了,这就涉及到分代策略了。
如何分配内存
我们先来看对象的分配流程(现在假设新生代和老年代都是空的):
- 1 创建一个对象User user = new User();
- 2 新创建的new User()对象首先会分配在新生代,如果新生代能放得下的话。
- 3 如果新生代放不下,则尝试对新生代进行一次gc(),称为MinorGC,此次gc()过后,所有存活的对象的年龄都+1,如果对象+1后年龄达到了15,则这个对象会直接移到老年代,我们可以简称为"年龄达标"。
- 4 如果此次gc()后,新生代还是放不下new User()对象,则直接放入老年代,我们可以简称称为"体积达标"。
- 5 如果老年代也放不下的话,则会对老年代进行一次gc(),称为MajorGC。
- 6 如果gc()后,还放不下,则进行二次gc(),也就是回收软引用的过程。
- 7 如果还是放不下,则抛出OOM异常。
综上,有两个条件可以进入老年代:
- 1 年龄达标: 年龄达到15的对象(gc发生了15次还存活下来的对象)被放到老年代。这是属于时间层面的。
- 2 体积达标: 新生代在MinorGC后还放不下,则直接进入老年代。这是属于空间层面的