文章目录
java
JVM——(1)为何学习虚拟机
JVM——(2)聊聊JVM虚拟机
JVM——(3)类加载子系统
JVM——(4)运行时数据区的概述与程序计数器(PC寄存器)
JVM——(5)运行时数据区的虚拟机栈
JVM——(6)运行时数据区的本地方法栈
JVM——(7)运行时数据区的堆空间
JVM——(8)运行时数据区的方法区
JVM——(9)对象的实例化与访问定位
JVM——(10)执行引擎
JVM——(11)String Table(字符串常量池)
JVM——(12)垃圾回收概述
JVM——(13)垃圾回收相关算法
JVM——(14)垃圾回收相关概念的概述
JVM——(15)垃圾回收器详细篇
JVM——(16)Class文件结构一(描述介绍)
JVM——(17)Class文件结构二(解读字节码)
JVM——(18)Class文件结构三(JAVAP指令)
JVM——(19)字节码指令集与解析一(局部变量压栈、常量变量压栈、出栈局部变量表指令)
JVM——(20)字节码指令集与解析二(算数指令)
JVM——(21)字节码指令集与解析三(类型转换指令)
JVM——(22)字节码指令集与解析四(对象建立与访问指令)
JVM——(23)字节码指令集与解析五(方法调用指令与方法返回指令)
JVM——(24)字节码指令集与解析六(操做数栈管理指令)算法
上篇咱们讲的是垃圾回收的概述,那么从本篇开始咱们说的是垃圾回收的相关算法数组
这些算法咱们并不会手动的实现,但咱们须要知道原理是什么,背景是什么,应用场景有什么缓存
咱们没有必要底层去实现,而且自己算法也是很复杂要考虑的细节有不少并发
咱们一提到垃圾回收的算法GC,那么其实翻译成两个词:垃圾回收器、垃圾回收ide
那么咱们提到GC就是要垃圾回收,那咱们须要知道哪些是垃圾结构(对象),将它清理函数
那么怎么找到这些垃圾呢?找到以后呢怎么清理回收呢?工具
因此咱们把GC的分红二个阶段:标记阶段、清楚阶段性能
标记阶段:识别垃圾结构(对象)识别出来。涉及到算法有:引用计数算法、可达性分析算法学习
清楚阶段:释放垃圾对象所占用的内存空间。涉及到算法有:标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact)
================================
在堆里存放着几乎全部的Java对象实例在GC执行垃圾回收以前,首先须要区分出内存中哪些是存活对象,哪些是已经死亡的对象。
只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,所以这个过程咱们能够称为垃圾标记阶段
那么在JVM中到底是如何标记一个死亡对象呢?简单来讲当一个对象已经再也不被任何的存活对象继续引用时,就能够宣判为已经死亡
判断对象存活通常有两种方式:引用计数算法和可达性分析算法
================================
引用计数算法(Reference Counting)比较简单,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的状况
对于一个对象A,只要有任何一个对象引用了A 则A的引用计数器就加1;当引用失效时引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收
================================
当p的指针断开的时候内部的引用造成一个循环,计数器都还算1,没法被回收,这就是循环引用从而形成内存泄漏
接下来咱们使用示例代码来看看Java有没有使用这个引用计数算法?
public class RefCountGC { //这个成员属性惟一的做用就是占用一点内存 private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB Object reference = null; public static void main(String[] args) { RefCountGC obj1 = new RefCountGC(); RefCountGC obj2 = new RefCountGC(); obj1.reference = obj2; obj2.reference = obj1; obj1 = null; obj2 = null; }}
当咱们不当心直接把obj1.reference和obj2.reference置为null。
若咱们使用引用计数算法那么则在Java堆中的两块内存依然保持着互相引用,没法被回收
具体是否是这样呢?咱们运行起来先看看堆空间的大小
这时咱们采用如下的代码块,运行起来看看是否会被GC回收
public class RefCountGC { //这个成员属性惟一的做用就是占用一点内存 private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB Object reference = null; public static void main(String[] args) { RefCountGC obj1 = new RefCountGC(); RefCountGC obj2 = new RefCountGC(); obj1.reference = obj2; obj2.reference = obj1; obj1 = null; obj2 = null; //显式的执行垃圾回收行为 //这里发生GC,obj1和obj2可否被回收? System.gc(); }}
若咱们这时开启GC,与上面没有开启GC的堆空间大小是同样的,那就未被回收
对比未开启前的堆空间,咱们开启后的堆空间大小。这就说明他们两被回收了
================================
引用计数算法,是不少语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制
具体哪一种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提升吞吐量的尝试
Java并无选择引用计数,是由于其存在一个基本的难题,也就是很难处理循环引用关系
可达性分析算法:也能够称为根搜索算法、追踪性垃圾收集
相对于引用计数算法而言,可达性分析算法不只一样具有实现简单和执行高效等特色,更重要的是该算法能够有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生
相较于引用计数算法这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集一般也叫做追踪性垃圾收集(Tracing Garbage Collection)
================================
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所链接的目标对象是否可达
所谓"GCRoots"根集合就是一组必须活跃的引用
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接链接着,搜索所走过的路径称为引用链(Reference Chain)
若是目标对象没有任何引用链相连(Object 五、六、7)则是不可达的,就意味着该对象己经死亡,能够标记为垃圾对象
在可达性分析算法中,只有可以被根对象集合直接或者间接链接的对象(Object 一、二、三、4)才是存活对象
================================
1.虚拟机栈中引用的对象:好比各个线程被调用的方法中使用到的参数、局部变量等。
2.本地方法栈内JNI(一般说的本地方法)引用的对象
3.方法区中类静态属性引用的对象:好比:Java类的引用类型静态变量
4.方法区中常量引用的对象:好比:字符串常量池(StringTable)里的引用
5.全部被同步锁synchronized持有的对象
6.Java虚拟机内部的引用:基本数据类型对应的Class对象、一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器
7.反映java虚拟机内部状况的JMXBean、JVMTI中注册的回调、本地代码缓存等
除了这些固定的GC Roots集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不一样,还能够有其余对象“临时性”地加入,共同构成完整GC Roots集合。好比:分代收集和局部回收(PartialGC)
若是只针对Java堆中的某一块区域进行垃圾回收(好比:典型的只针对新生代),必须考虑到内存区域是虚拟机本身的实现细节,更不是孤立封闭的
这个区域的对象彻底有可能被其余区域的对象所引用,这时候就须要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
因为Root采用栈方式存放变量和指针,因此若是一个指针它保存了堆内存里面的对象,可是本身又不存放在堆内存里面,那它就是一个Root
可是也有一些须要注意的地方,好比说若是要使用可达性分析算法来判断内存是否可回收,那么分析工做必须在一个能保障一致性的快照中进行。
这点不知足的话分析结果的准确性就没法保证,这点也是致使GC进行时必须“Stop The World”的一个重要缘由。即便是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必需要停顿的
接下里咱们说关于在回收以前涉及到方法的调用逻辑,也就是Java语言提供了对象终止(finalization)机制来容许开发人员提供对象被销毁以前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象,咱们称呼为垃圾进行回收。即垃圾回收此对象以前,总会先调用这个对象的finalize()方法
咱们能够看看Object 类的finalize()方法,能够看到并无被final修饰说明可重写
finalize() 方法容许在子类中被重写,用于在对象被回收时进行资源释放。
建议永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
理由有下面三点:
从功能上来讲finalize()方法与C++中的析构函数比较类似,可是Java采用的是基于垃圾回收器的自动内存管理机制,因此finalize()方法在本质上不一样于C++中的析构函数
finalize()方法对应了一个finalize线程可是优先级比较低,即便主动调用该方法也不会所以就直接进行回收
因为finalize()方法的存在,虚拟机中的对象通常处于三种可能的状态。
若是从全部的根节点都没法访问到某个对象,说明对象己经再也不使用了。通常来讲此对象须要被回收。但事实上也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段。
一个没法触及的对象有可能在某一个条件下“复活”本身,若是这样那么对它当即进行回收就是不合理的。为此定义虚拟机中的对象可能的三种状态
不可触及的对象不可能被复活,由于finalize()只会被调用一次。以上3种状态中是因为finalize()方法的存在进行的区分。只有在对象不可触及时才能够被回收
================================
断定一个对象objA是否可回收,至少要经历两次标记过程:
若是对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被断定为不可触及的
若是对象objA重写了finalize()方法且还未执行过,那么objA会被插入到F-Queue队列中由一个虚拟机自动建立的、低优先级的Finalizer线程触发其finalize()方法执行
咱们能够运行上面的那个示例代码,看看这个Finalizer线程
finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记
若是objA在finalize()方法中与引用链上的任何一个对象创建了联系,那么在第二次标记时,objA会被移出“即将回收”集合
若是后该对象会再次出现没有引用存在的状况。在这个状况下finalize()方法则不会被再次调用,对象会直接变成不可触及的状态。也就是说一个对象的finalize()方法只会被调用一次。
接下来咱们经过一个示例代码来演示一下finalize
public class CanReliveObj { //类变量,属于 GC Root public static CanReliveObj obj; public static void main(String[] args) { try { //建立一个对象 obj = new CanReliveObj(); //本身将本身置空 obj = null; System.gc();//调用垃圾回收器 System.out.println("第1次 gc"); // 若是对象objA到GC Roots没有引用链,则进行第一次标记 // 再次进行筛选,判断此对象是否有必要执行finalize()方法 // 由于Finalizer线程优先级很低,暂停2秒,以等待它 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } } catch (InterruptedException e) { e.printStackTrace(); } }}//运行结果以下:第1次 gc obj is dead
咱们这里并无重写finalize()方法,因此当咱们没有引用链的时候,被判不可触及了
接下来咱们重写一下finalize()方法,自我拯救一下看看会判什么样呢?
public class CanReliveObj { public static CanReliveObj obj;//类变量,属于 GC Root //此方法只能被调用一次 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用当前类重写的finalize()方法"); obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj创建了联系 } public static void main(String[] args) { try { obj = new CanReliveObj(); // 对象第一次成功拯救本身 obj = null; System.gc();//调用垃圾回收器 System.out.println("第1次 gc"); Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } System.out.println("第2次 gc"); obj = null; System.gc();//调用垃圾回收器 Thread.sleep(2000); if (obj == null) { System.out.println("obj is dead"); } else { System.out.println("obj is still alive"); } } catch (InterruptedException e) { e.printStackTrace(); } }}//运行结果以下:第1次 gc 调用当前类重写的finalize()方法 obj is still alive 第2次 gc obj is dead
这时咱们就能够看到没有引用链,则进行第一次标记,可是咱们重写finalize()方法因此虚拟机进行执行而且复活回来,当咱们第二次断开引用链的时候,就不在执行finalize()方法将它判为
不可触及的状态进行回收
MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。
用于查找内存泄漏以及查看内存消耗状况,是基于Eclipse开发的是一款免费的性能分析工具
MAT是基于Eclipse开发的,是一款免费的性能分析工具,可点击:下载入口
虽然Jvisualvm很强大,可是在内存分析方面,仍是MAT更好用一些
此小节主要是为了实时分析GC Roots是哪些东西,中间须要用到一个dump的文件
那么咱们这时演示一下若是使用JVisualVM获取下面代码块的dump文件
public class GCRootsTest { public static void main(String[] args) { List<Object> numList = new ArrayList<>(); Date birth = new Date(); for (int i = 0; i < 100; i++) { numList.add(String.valueOf(i)); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("数据添加完毕,请操做:"); new Scanner(System.in).next(); numList = null; birth = null; System.out.println("numList、birth已置空,请操做:"); new Scanner(System.in).next(); System.out.println("结束"); }}
咱们这个代码块numList 和 birth 在第一次捕捉内存快照的时候,为 GC Roots
以后 numList 和 birth 置为 null ,对应的引用对象被回收,在第二次捕捉内存快照的时候,就再也不是 GC Roots
那么此时咱们将代码运行起来,捕捉第一次内存快照并使用JVisualVM工具查看dump文件
接下来咱们就可使用MAT 打开刚刚另存为的快照
若是咱们想查看当前快照的GC Roots,能够按照下图点击
固然咱们能够查看MAT的官网文档有对GC Root下的相关参数介绍:访问入口
接下来咱们能够展开Thread 查看咱们刚刚的主线程
展开咱们的主线程能够看到咱们定义的两个局部变量,类型分别为 ArrayList 和 Date
这时咱们将程序继续执行,释放引用链再另存为看看他们的快照
这时咱们再用MAT打开另外一份快照看看释放引用链后的GC Root有哪些
一样的咱们查看刚刚的主线程,看看以前定义的ArrayList 和 Date 还存在吗?
咱们当前程序当中才三十几行的代码,就出现了1700 GC Roots,如果上千行的话就更多了
因此通常开发当中,查看GC Root没有那么多的机会,不必全看
咱们只须要观看某一个引用变量的这一支 GC Root
接下来咱们能够针对刚刚程序,使用另一个JProfiler 进行 GC Roots 溯源演示
右击对象,选择 Show Selection In Heap Walker,单独的查看某个对象
咱们查看这个char[] 数组的应用看看,有哪些
点击Show Paths To GC Roots,在弹出界面中选择默认设置便可
下面咱们使用一个示例代码用JProfiler 进行OOM 异常排查
public class HeapOOM { byte[] buffer = new byte[1 * 1024 * 1024];//1MB public static void main(String[] args) { ArrayList<HeapOOM> list = new ArrayList<>(); int count = 0; try{ while(true){ list.add(new HeapOOM()); count++; } }catch (Throwable e){ System.out.println("count = " + count); e.printStackTrace(); } }}
进行堆大小设置,以及出现OOM时再目录成dump文件
这时咱们运行起来的时候,就会爆出OOM的异常
这时咱们能够查看刚刚的命令帮咱们生成的dump文件
这时咱们运用JProfiler 打开它,而且查看一下
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存
目前在JVM中比较常见的三种垃圾收集算法是
================================
标记-清除算法(Mark-Sweep)是一种很是基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。
当堆中的有效内存空间(available memory)被耗尽的时候,就会中止整个程序(也被称为stop the world),而后进行两项工做:标记、清除
这里所谓的清除并非真的置空,而是把须要清除的对象地址保存在空闲的地址列表里。下次有新对象须要加载时,判断垃圾的位置空间是否够,若是够就存放(也就是覆盖原有的地址)
关于空闲列表是在为对象分配内存的时候提过:
标记清除算法的效率不算高在进行GC的时候,须要中止整个应用程序,用户体验较差
这种方式清理出来的空闲内存是不连续的,产生内碎片,须要维护一个空闲列表
================================
为了解决标记-清除算法在垃圾收集效率方面的缺陷
M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP GarbageCollector Algorithm Using Serial Secondary Storage)”
M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中
将活着的内存空间分为两块,每次只使用其中一块在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,以后清除正在使用的内存块中的全部对象,交换两个内存的角色最后完成垃圾回收
新生代里面就用到了复制算法,Eden区和S0区存活对象总体复制到S1区
没有标记和清除过程,实现简单,运行高效
复制过去之后保证空间的连续性,不会出现“碎片”问题。
此算法的缺点也是很明显的,就是须要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动意味着GC须要维护region之间对象引用关系,无论是内存占用或者时间开销也不小
若是系统中的垃圾对象不少,复制算法须要复制的存活对象数量并不会太大,效率较高
老年代大量的对象存活,那么复制的对象将会有不少,效率会很低
在新生代对常规应用的垃圾回收,一次一般能够回收70% - 99% 的内存空间。回收性价比很高。因此如今的商业虚拟机都是用这种收集算法回收新生代
================================
复制算法的高效性是创建在存活对象少、垃圾对象多的前提下的。这种状况在新生代常常发生,可是在老年代更常见的状况是大部分对象都是存活对象
若是依然使用复制算法因为存活对象较多,复制的成本也将很高。所以基于老年代垃圾回收的特性,须要使用其余的算法。
标记-清除算法的确能够应用在老年代中,可是该算法不只执行效率低下并且在执行完内存回收后还会产生内存碎片,因此JVM的设计者须要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生
1970年先后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中人们都使用了标记-压缩算法或其改进版本
第一阶段和标记清除算法同样,从根节点开始标记全部被引用对象
第二阶段将全部的存活对象压缩到内存的一端,按顺序排放。以后,清理边界外全部的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,所以,也能够把它称为标记-清除-压缩(Mark-Sweep-Compact)算法
两者的本质差别在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策
能够看到标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可,这比维护一个空闲列表显然少了许多开销
消除了标记-清除算法当中,内存区域分散的缺点,咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可
消除了复制算法当中,内存减半的高额代价
从效率上来讲,标记-整理算法要低于复制算法
移动对象的同时,若是对象被其余对象引用,则还须要调整引用的地址(由于HotSpot虚拟机采用的不是句柄池的方式,而是直接指针)
移动过程当中,须要全程暂停用户应用程序。即:STW
================================
效率上来讲,复制算法是当之无愧的老大,可是却浪费了太多内存
而为了尽可能兼顾上面提到的三个指标,标记-整理算法相对来讲更平滑一些,可是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段
================================
难道就没有一种最优的算法吗?答:无,没有最好的算法,只有最合适的算法。具体问题具体分析
前面全部这些算法中并无一种算法能够彻底替代其余算法,它们都具备本身独特的优点和特色。
这时分代收集算法应运而生是基于这样一个事实:不一样的对象的生命周期是不同的
所以不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率,通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色使用不一样的回收算法,以提升垃圾回收的效率
在Java程序运行的过程当中会产生大量的对象,其中有些对象是与业务信息相关:
Http请求中的Session对象、线程、Socket链接这类对象跟业务直接挂钩,所以生命周期比较长
可是还有一些对象主要是程序运行过程当中生成的临时变量,这些对象生命周期会比较短
好比:String对象因为其不变类的特性,系统会产生大量的这些对象有些对象甚至只用一次便可回收
在HotSpot中,基于分代的概念GC所使用的内存回收算法必须结合年轻代和老年代各自的特色
这种状况复制算法的回收整理速度是最快的,复制算法的效率只和当前存活对象大小有关,所以很适用于年轻代的回收。而复制算法内存利用率不高的问题,经过hotspot中的两个survivor的设计获得缓解
这种状况存在大量存活率高的对象,复制算法明显变得不合适。通常是由标记-清除或者是标记-清除与标记-整理的混合实现
以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。对于碎片问题CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片致使的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理
提示:分代的思想被现有的虚拟机普遍使用。几乎全部的垃圾回收器都区分新生代和老年代
================================
上述现有的算法,在垃圾回收过程当中,应用软件将处于一种Stop the World的状态
在Stop the World状态下应用程序全部的线程都会挂起暂停一切正常的工做等待垃圾回收的完成
若是垃圾回收时间过长应用程序会被挂起好久,将严重影响用户体验或者系统的稳定性。
为了解决这个问题,即对实时垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的诞生
若是一次性将全部的垃圾进行处理,须要形成系统长时间的停顿,那么就可让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
总的来讲,增量收集算法的基础还是传统的标记-清除和复制算法。增量收集算法经过对线程间冲突的妥善处理,容许垃圾收集线程以分阶段的方式完成标记、清理或复制工做
使用这种方式因为在垃圾回收过程当中,间断性地还执行了应用程序代码因此能减小系统的停顿时间。可是由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐量的降低
================================
分区算法主要仍是针对G1收集器来讲的通常来讲在相同条件下堆空间越大,一次GC时所须要的时间就越长,有关GC产生的停顿也越长.
为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减小一次GC所产生的停顿
分代算法将按照对象的生命周期长短划分红两个部分,分区算法将整个堆空间划分红连续的不一样小区间。每个小区间都独立使用独立回收。这种算法的好处是能够控制一次回收多少个小区间
须要注意的是注意,这些只是基本的算法思路实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,而且并行和并发兼备