Java 和 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的"高强",墙外面的人想进去,墙里面的人却想出来 ————《深刻理解Java虚拟机》html
文章,笔记,源码地址:github.com/leosanqing/…java
Java 因为JVM自带垃圾回收的机制,因此对于不少初中级的程序猿很是方便,不须要本身写语句控制垃圾的回收,只须要关注业务逻辑便可。尤为是本身写demo或者练手的项目,基本不用担忧垃圾回收的事情git
可是,对于线上的项目,一旦出现垃圾回收的问题,每每是很是致命的。因此不管是校招仍是社招基本都会问到JVM的垃圾回收机制以及算法github
这个文章的结构以下算法
主要分为两个部分安全
若是你知道 JVM运行时数据区,那么应该知道上面的两部分恰好是按照线程共不共享来划分的。这样也方便记数据结构
若是不了解的话,能够参考个人这篇 JVM运行时数据区的文章spa
刚刚也提到了,若是了解JVM运行时数据区的话,第一部分的线程是不共享的。他们都随着线程生而生,线程灭而灭。线程
这三个区域的内存分配和回收都具有肯定性,没必要过多考虑回收问题,在方法结束或者线程结束时,内存天然而然就跟着回收了3d
基本是运行时才已知,且线程共享,基本上考虑垃圾回收算法以及回收断定都是这两个部分。
在方法区的回收垃圾的性价比通常是比较低的
在JDK1.7或者1.8以前,不少人将方法区和Hotspot中的永久代
等同。其实两者也是有区别的前者是 JVM 的规范,然后者则是 JVM 规范的一种实现
**注意:**只有 Hotspot中才有永久代(PermGen space)的概念,其余的JVM好比Oracle的JRocket和IBM的J9并无这个概念。 并且永久代从JDK1.7以后就开始逐步移除了,1.8以后就已经彻底移除,转为元空间
具体能够参考这篇博文,永久代和元空间
他主要回收的内容是废弃常量和无用的类
以回收常量池中的字面量为例,若是一个字符串"abcd"已经加入了常量池,可是并无任何String对象引用常量池中的 "abcd"常量
常量池中其余类(接口)、方法、字段的符号引用也与此相似
回收Java堆中的方式也与此相似
断定无用的类条件比较严苛,须要同时知足下列三个条件
java.lang.Class
对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法注意这里仅仅是"能够",而并非和对象同样,不使用了就必然会回收
固然是对象已经"死掉"的时候,主要有如下两种方法
这个逻辑比较简单
可是这个方法有一个缺点:互相引用的对象不会被回收
好比有下面代码(除此以外没有其余引用,这两个对象也不会被访问,应该回收)
obj1.instance = obj2;
obj2.instance = obj1;
复制代码
对于使用 这种断定算法的虚拟机,并不会收到回收他们通知,就不会回收他们两个
引用链
即使是不可达的对象也不是当即进行回收,他会经历两次标记的过程
不管是可达性分析算法仍是引用计数法,要断定对象是否"死亡",都须要根据引用来断定
在Java中一共有四种引用(JDK1.2以后),强度依次递减
Strong Reference
在代码中广泛存在的,只要强引用存在,JVM就必定不会回收。
好比Object obj = new Object();
Soft Reference
发生内存溢出异常以前,将这些软引用链接的对象列近回收范围之中的第二次回收。若是此次回收以后尚未足够的内存,才会抛出内存溢出异常
Weak Reference
只能活到下一次垃圾回收发生以前。不管内存是否足够,都会将软引用链接的对象回收
Phantom Reference
,他是最弱的一种引用关系。一个对象是否含有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例
"为一个对象设置虚引用关联的惟一目的就是能在这个对象被回收时收到一个系统通知” ————《深刻理解Java虚拟机》
若是想多了解java四种引用的应用说明,能够参考下面这篇博文;
垃圾收集算法主要有四种:
顾名思义,标记须要清除的对象而后清除。他是最最基础的,以后的都是经过他改进的
将内存分为两块,每次只使用其中一块。每次垃圾回收时,将存活的对象复制到另外一块,而后清理那块所有空间。
比较适合能够大量进行垃圾回收的新生代
为了解决这个问题,JVM将新生代的 Eden区和其中一个survivor区域划分比例调整为8:1.而survivor共有两个,每次只用其中一个,另外一个用来复制,存放活着的对象
标记过程同样,就是清除的时候,将全部活着的对象移到一端。而后清理剩下的区域
因此通常用在老年代的回收
主流的虚拟机都采用这种算法
主要是根据不一样代的特色,选择相对合适的上述算法。
好比Java新生代对象存活率比较低,有大批的对象死去,因此采用标记-复制
算法,而老年代的对象相对比较稳定,存活率较高并且对象较少,也没有额外的空间对他进行分配担保,因此就采用标记-整理
算法
以前也说到了,JVM使用的是可达性分析法。可是可达性分析算法也有他的缺点:
耗费时间是由于使用可达性分析算法的时候寻找GC Root的时候。要扫描整个区域,可是仅仅方法区通常都数百兆。
上文提到可做为GC Root节点的对象有全局性的引用(常量和静态属性),执行上下文(如栈帧中的本地变量表)。
GC停顿是指在进行可达性分析的时候,这项工做必须在一个能确保一致性快照中进行。一致性是指在那一段时间内对象关系不能再反复发生变化
那么怎么缩短这个时间呢?
这个要得益于Hotspot虚拟机的准确式内存管理。由于有他,因此在Hotspot中,是使用一组称为OopMap
的数据结构。在类加载完成的时候,Hotspot就把对象内什么偏移量上是什么类型计算出来,在JIT编译过程当中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
这样的话,就不须要进行全盘扫描,只须要扫描OopMap就能够完成GC Root的枚举
使用OopMap貌似是解决了时间的问题,可是若是为每一条指令都生成OopMap,那么空间的开销很是大。由于OopMap内容变化的指令很是多。
因此安全点的做用其实是解决OopMap空间开销的问题
那么什么地方的节点称为"安全点"
呢?只在特定的位置记录指令信息,这些位置叫安全点。
为啥叫安全点呢?由于程序执行时并不是在全部地方都能停顿下来开始GC,只有到达特定节点——安全点才能暂停。由于他们已经"安全“了,能够放心的进行分析了
问题又来了,那么以什么标准来选取呢?选取的不能太少,省得GC等待时间长;也不能太多,省得频繁进行。
因此选取的标准是:是否具备让程序长时间执行的特征。
最明显的特征就是——指令复用
那么怎么让线程跑到安全点而后让他们停下来呢?
有两种实现方式
抢先式中断:不须要线程主动配合,发生GC时,全部的线程中断,若是发现有的线程没有跑到安全点,就让他再运行,跑到安全点再中断
主动式中断:当GC须要中断线程的时候,不须要操做线程。仅仅设置一个标志位,而后让线程主动去轮询他,发现标志位为真,就中断。轮询标志位和安全点是重合的
安全点彷佛已经解决了枚举根节点时的空间效率问题,可是还存在一个缺陷:当个人线程没有执行的时候咋办?好比个人线程这个时候正在sleep状态
或者block状态
,那我不可能让他们本身走到相应的安全点。针对这个状况,Hotspot中提出了一个新的解决方法——安全区域
清楚了上面安全点的概念,那么你就能够把安全区域当成是他的拓展,这里一大片的地方都是安全的——引用关系都不会发生变化
当线程执行到安全区域
中的代码时,就先标示本身已经进入到了安全区域,这样的话,当在这段时间JVM要发起GC的时候,就不用管标示为安全区域的线程了。
当线程要离开安全区域时,他要判断是否已经完成了根节点的枚举,若是完成了那就继续执行,没有完成的话那就只能等待直到收到能够离开安全区的信号为止
JVM采用可达性分析方法找出须要回收的对象
须要回收的对象主要是
java中的四种引用
Hotspot采用分代回收算法
Hotspot枚举根节点的时候算法实现