JVM(复习)垃圾收集器和内存分配策略
文章目录
一,垃圾回收
关于垃圾回收我们需要总结的点有三个:
- 哪些内存需要回收
- 什么时候回收
- 怎样回收
1.1哪些内存需要回收
程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭
- 栈中的栈桢随着方法的进入和退出而有条不紊的执行者出栈和入栈操作,每一个栈桢中分配多少内存基本可以在编译期确定,随着方法或线程结束,该部分内存也会随着被回收
java堆和方法区则不一样,一个接口的多个实现类需要的内存可能不一样,需要在运行期才会知道会创建哪些对象,这部分内存分配和回收都是动态的
而且,堆里几乎存放着所有对象实例,所以这一部分才是垃圾收集关注的地方
1.2什么时候回收
1.2.1引用计数算法
算法原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻,计数器为0的对象就是不可能再被使用的
但是该算法无法解决循环引用问题:
public class Test{
static class A{
private byte[] bytes = new byte[2 * 1024];
private Object object;
}
static class B{
private byte[] bytes = new byte[2 * 1024 * 1024];
private Object object;
}
public static void main(String[] args) {
A a = new A();
B b = new B();
//相互引用
a.object = b; //引用b,b的引用计数器加一
b.object = a; //引用a,a的引用计数器加一
//引用失效,引用计数置为0
a = null;
b = null;
//垃圾回收
System.gc();
}
}
可以看到,jvm并没有因为两个对象互相引用就不回收他们,可以看出jvm判断对象是否可以回收采取的不是引用计数法
1.2.2可达性分析算法
判断对象是否存活都是与引用有关
java中的引用
-
强引用:
强引用非常常见,比如:
Object a = new Object();
这类引用就是强引用
只要强引用还在,垃圾收集器永远不会回收他们,哪怕内存不足仍不会回收,但会抛出OOM
-
软引用:
软引用用来描述一些还有用但并非必需的对象,对于软引用关联的对象,在内存充足时,不会回收,但是在内存不足时会回收,如果回收了软引用还是收集不到足够的内存进行分配,则会抛出OOM,利用SoftReference实现弱引用
//软引用实现 String string= ""; //构造方法传入引用 SoftReference<String> softReference = new SoftReference<String>(string); //此时string便是一个软引用 string = softReference.get();
-
弱引用:
描述非必需对象,无论内存是否足够,只要gc,便会回收
//弱引用实现 String s = ""; WeakReference<String> weakReference = new WeakReference<>(s); s = weakReference.get();
-
虚引用:
一个对象是否有虚引用的存在完全不会对其生存时间构成影响,无法通过虚引用来获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
再谈gc root
其实可达性分析算法是基于gc root引用链实现的,通过GC Root对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Root没有任何引用链和GC Root相连,则此对象无用,该对象可以回收。
哪些可作为GC Root:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈引用的对象
凡是被常量,静态变量,全局变量,运行时方法中的变量直接引用的对象,原则上不能被gc
即使在可达性分析算法中不可达的对象,也并非是非死不可。的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程
-
如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
-
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。finalize() 方法只会被系统自动调用一次。
回收方法区(HotSpot中的永久代)
在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。
永久代垃圾回收主要两部分内容:废弃的常量和无用的类。
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足
- 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法
1.3怎样回收
1.3.1标记-清除算法
先标记出所有需要回收的对象,标记完成后,统一回收所以被标记的对象
缺陷:
-
效率问题
- 标记和清除两个过程的效率并不高
-
空间问题
- 标记清除之后会产生大量不连续的内存碎片,当需要分配一个大对象时,就无法找到足够的连续内存而不得不触发一次垃圾回收
1.3.2复制算法
复制算法解决了标记-清除算法的效率问题
他将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上,然后将已使用过的内存空间一次清理掉
这种算法的代价是将内存缩小为原来的一半,且在对象存活率较高时,需要进行多次复制操作,此时效率会降低
1.3.3标记-整理算法
标记整理算法和标记清除的第一阶段标记相同,先标记出所有可回收的对象,接下来,标记整理不是对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理端边界以外的内存。
1.3.4分代收集算法
根据对象存活周期的不用将内存划分为几块,堆分为新生代和老年代,根据各个年代的特点采用最适当的算法
- 新生代:
- 新生代每次垃圾收集都有大批的对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
- 老年代:
- 老年代对象成活率较高,没有额外的空间为其分配担保,必须采用标记-清除或者标记-整理算法
1.4垃圾收集器
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
自己弄的思维导图:
二,内存分配
对象内存分配,大方向来说,就是在堆上如何分配
java堆分代:
对象优先在Eden分配
大多数情况下,对象在新生代Eden区分配,当Eden区没有足够的内存空间进行分配时,虚拟机将发起一次Minor GC
大对象直接进入老年代
大对象(需要大量连续内存空间的java对象,数组或者长字符串)将直接进入老年代
长期存活的对象将进入老年代
每个对象都有一个年龄计数器,对象在Eden区经过第一次minor gc后仍然存活,并且能被Survivor容纳的话,将移动到Survivor分区,并且将该对象年龄置为1,以后,在survivor每经历一次minor gc后仍能存活,年龄 + 1;当年龄到达一定程度(默认15岁)则进入老年代,但也不一定说必须达到这个年龄才可以进入老年代,如果,survivor空间中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代