前言
其实关于虚拟机的知识我之前写过几篇公众号的文章,但是关于jvm的相关知识,我还是没能印刻在脑子里,所以年末我想要再认真的梳理一次,这篇文章我将从头到尾的来梳理整个JVM知识体系。
一、内存分布
写虚拟机的文章,我觉得必须要从内存分布开始,如果连虚拟机的内存分布都不弄清楚,那么整个JVM知识体系的构建是很空的。
1.程序计数器
程序计数器是一块较小的内存空间,他的主要功能是记录程序执行的字节码行号,这里唯一点需要注意的是java的多线程是是通过线程轮流切换实现的,所以每一个线程都有一个独立的程序计数器。
2.java虚拟机栈
与程序计数器一样,java虚拟机栈也是线程私有的,虚拟机栈描述的是方法执行的内存模型,即每个方法执行时都会创建一个栈,它里面主要存储的是:局部变量、操作数栈、动态链接、方法出口等信息。
3.本地方法栈
本地方法栈和虚拟机栈的功能非常像似,他们的区别在于,本地方法栈是服务本地方法的。
4.堆
对于大多数应用来说,java堆是虚拟机管理的内存中最大的一块,java堆是被所有线程共享的,它里面存放的是对象实例和数组。GC的只要作用对象也即是堆。
5.方法区
与java堆一样方法区也是被所有线程共享的一块区域,他里面主要是一些类信息、常量和静态变量,方法区里还有一块特定的区域叫常量池,主要用于存放运行时常量的,方法区也被成为永久代。
二、可达性分析算法
在明白内存分布后,我们来思考一下哪些内存需要回收?以及如何判断哪些该被回收了?上诉内存分布的程序计数器、栈都是线程私有的,线程结束,内存自当释放,而方法区里面大部分是类相关的信息,这些信息则是不能随便清理的,那么堆里面存放的是对象,我们知道对于Java来说,对象无处不在,这些对象的信息如果一直不被清理,将会越来越大,并且有些对象并不是一直被程序使用的,所以对象是可以被优化的,所以GC的主要工作是清理堆内存。
判断一个对象是否需要被清理的方法是可达性分析算法来实现的,他的原理是由根节点向下搜索,如果发现一个对象没有通过任何直接或者间接的方式到根节点可达,那么则认为该对象可以被回收
- 不使用引用计数器的原因是可以避免两个空引用相互引用逃离GC
- 可以作为根节点的:虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中静态变量引用的对象、方法区中常量引用的对象
垃圾回收算法
1.标记-清除
标记清除算法是垃圾回收算法中最基础的,他的原理如图名称一样,主要分为两个过程,标记和清除,此算法有两个缺点,第一是效率,第二是会产生大量的内存碎片,当剩余内存很多却不连续的时候无法给大对象分配内存。
2.复制算法
为了解决效率的问题,复制算法应运而生,他的原理是将内存分为大小相等的两块内存,每次都只使用一半,内存回收的时候讲存活的内存复制到另一块,然后将之前的另一半全部清除,复制算法完美的解决了效率和内存不连续的问题,但是将可使用的内存降低了原来的一半,代价有点大,但是由于大部分对象朝生夕死,如果将内存合理配比也是可以的,很多新生代的垃圾回收器都是采用的此算法,比如新生代的Eden 和 survival采用8:1的比例就是很好的
3.标记整理算法
由于复制算法对于大多数对象都存活的极端情况下不是很适用,比如老年代,于是标记-整理算法出现了,标记-整理算法和标记-清理算法即为类似,只是在最后将所有存活的对象向一端移动,然后直接清理端界以外的对象。
4.分代算法
分代算法的精髓在于根据对象存活的周期不同,将对象分为新生代和老年代,这样就可以根据各年代的特点采用不同的收集算法,比如新生代采用复制算法,老年代采用标记-清理或者标记-整理算法。
三、垃圾收集器
1.Serial收集器
Serial收集器是最基本的也是最古老的收集器,在1.3版本以前Serial是唯一的年轻代垃圾收集器,Serial收集器最大的弊端是STW(stop the world),你可以理解为这是一个单线程的收集器,并且在进行垃圾回收的时候,所有的工作线程必须停掉,Serial收集器的唯一用处是可以在客户端应用中使用。
2.ParNew收集器
ParNew收集器其实是Serial收集器的多线程版本,但是ParNew收集器可以作为Server模式的新生代收集器,一个重要原因是他可以和老年代CMS收集器配合使用
3.Parallel Scavenge 收集器
parallel Scavenge收集器也是新生代收集器,采用的是并行的方式,parallel Scavenge收集器的优点是,他可以提高程序的吞吐量,可以通过参数设置程序最大的停顿时间和吞吐量的大小。
吞吐量 = 程序运行的时间 / (程序运行的时间 + GC的时间)
所以parallel Scavenge