浅读Java虚拟机
Posted 席飞剑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅读Java虚拟机相关的知识,希望对你有一定的参考价值。
理解Java虚拟机结构是Java从业人员必备技能,下面描述Java虚拟机结构、垃圾收集器与内存分配策略,在排查java性能问题以及性能优化时大有帮助,掌握Java虚拟机20%的知识能解决工作中80%的Java性能诊断及优化(与JVM相关)问题。更深入的知识请自行研究源码。
一、Java虚拟机结构
1、程序计数器:是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
2、Java虚拟机栈:
描述的是java 方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型(boolean
、byte
、char
、short
、int
、float
、long
、double
)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3、本地方法栈:与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。在堆、方法区等占用内存之后,剩下的内存则由虚拟机栈和本地方法栈来瓜分,每个线程分配到的栈容量越大,那么可以建立的线程数量自然就越少,需要防范方法调用(包括递归)、定义了大量本地变量等引起栈溢出问题。
4、堆:
也叫做java 堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。其大小通过-Xms
(最小值)和-Xmx
(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx
为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx
指定的大小,可通过-XX:MinHeapFreeRation=
来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=
来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。
由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
5、方法区:俗称”永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize
和 -XX:MaxPermSize
参数限制方法区的大小。
6、运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
7、直接内存:直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这个堆外内存就是本机内存,不会影响到堆内存的大小。DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与-Xmx一样,如果DirectMemory溢出,一个明显的特征是在Heap Dump文件中不公看到明显的异常,如果发现OOM之后Dump文件很小,而程序中直接或间接使用了NIO,可以怀疑一下这方面的原因。
二、圈对象
如何判断一个对象是否存活?
1、引用计数算法:给对象中添加一个引用计数器,每当有引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用。
Java虚拟机里面没有选用引用计数算法来管理内存,其中主要原因是他很难解决对象之间相互引用的问题。例如:对象objA和objB都有字段instance字段,且互相赋值,但实际上这两个对象已经不可能被访问了,但因为他们互相引用着对方,导致他们的引用计数都不为0,于是导致他们无法回收。
public class ReferenceGC
public Object instance = null;
public static void testGC()
ReferenceGC objA = new ReferenceGC();
ReferenceGC objB = new ReferenceGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
2、可达性分析算法:通过一系列的称为"GC Root"的对象作为起点,从这些节点开始向下搜索,搜素所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何的引用链相连,就判定对象可以被回收。Java, C#等语言都是使用这种算法判读对象的存活的。
GC Root的对象包括下面几种:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(Native方法)引用的变量
3、再谈引用:Java1.2后对引用进行扩充,分为:
1)强引用:类似 Object obj = new Object()这类引用,垃圾收集器永远不会进行回收
2)软引用:用来描述一些还有用但非必须的对象。系统在内存溢出异常之前,将会把这些对象列入回收范围之中进行。二次回收。如果这次回收还没有足够的内存,才会抛出内存异常。SoftReference类实现。
3)弱引用:比软引用更弱一些,也是用来描述非必须对象。回收发生在下次垃圾收集器回收时。WeakReference类实现。
4)虚引用:最弱一种引用,虚引用无法影响对象其生存时间,也无法通过虚引用获得对象,唯一目的就是对象被垃圾收集器回收时,收到一个系统通知。PhantomReference类实现。
4、对象生存还是死亡
如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,那么它将会被第一次标记并且进行一次筛选,筛选条件是该对象是否覆盖finalize()方法,是否执行过finalize()方法。如果为覆盖或执行过则对象死亡。
如果覆盖finalize()且有必要执行finalize(),则对象放入F-Queue的队列中,之后由虚拟机自动建立,低优先级的Finalizer线程去执行。这里的执行是指虚拟机会触发这个方法,但不承诺会等待它运行结束。(该方法不建议使用)
三、HotSpot算法实现与垃圾收集
垃圾收集算法与垃圾收集器是基础概念,主要有:
垃圾收集算法:常见的复制算法、标记-清除算法,标记-整理算法。
垃圾收集器:Serial、Parallel、CMS、G1
这里不打算细讲,参考另一篇文章:GC基础知识
1、枚举根结点:可达性分析中从GC Roots节点主要在全局性引用与执行上下文中,如果要逐个检查里在的引用,必然会消耗很多时间。
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行,这里“一致性”是指在整体分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这是导致GC进行时必须STW的一个重要原因,即使在号称几乎不会发生停顿的CMS中,检举根节点时也是要STW的。
2、安全点(safepoint):如何在GC时让所有线程都“跑”到最近的安全点上再停顿下来,一般采用主动式中断的思想,当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
3、空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一下Minor GC(尽管此次Minor GC是有风险的),如果小于或者HandlePromotionFailure设置为false不允许冒险,则此时改为一次FGC。大部分情况下还是会将HandlePromotionFailure开关打开,避免FGC过于频繁。
后记:对于JVM分析常用的工具或命令,例如:Jmap、Jstack、Jstat、MAT、Jvisualvm等都是对理论的一种包装,理清原理后上手都是极快的,此处不详细再说,我博客文章之前都有讲解的。
以上是关于浅读Java虚拟机的主要内容,如果未能解决你的问题,请参考以下文章