java 虚拟机原理
什么是jvm
jvm是java virtual machine(java虚拟机)的缩写,是一个虚构出来的计算机,它屏蔽了与具体操作系统平台相关的信息,使得java程序只需生成在java虚拟机上运行的目标代码(字节码,bytecode), 就可以在多种平台上不加修改地运行。这背后其实就是jvm把字节码翻译成具体平台上的机器指令,从而实现“一次编写,到处运行(write once, run anywhere)”。
java为什么能够跨平台?
java引入了字节码的概念,jvm 只能认识字节码,并将它们解释到系统的api调用。针对不同的系统有不同的jvm实现,有 linux 版本的 jvm 实现,也有 windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。在不同的系统平台上运行是通过java解释器将字节码解释为不同平台的机器码,在不同的 jvm 实现上会映射到不同系统的 api 调用,从而实现代码的不加修改即可跨平台运行。
jvm、jre、jdk的关系
- jre(java runtime environment,java运行环境),面向java程序的使用者,而不是开发者。jre是运行java程序所必须环境的集合,包含jvm标准实现及 java核心类库。它包括java虚拟机、java平台核心类和支持文件
- jdk(java development kit,java开发工具包),包括了java运行环境(jre),并提供了一堆java工具tools.jar和java标准类库 (rt.jar)
三者的关系是:jdk>jre>jvm
java虚拟机运行原理
按照阶段分为两个阶段:
编译阶段:当我们将一个.java的文件进行编译,编译程序会生成一个相同名字而后缀为.class的文件。
运行阶段主要分为以下步骤:
- 加载
- 通过一个类的全限定名来获取该类的二进制字节流
- 将这个字节流的静态存储结构转化为方法区运行时数据结构
- 在内存堆中生成一个代表该类的java.lang.class对象,作为该类数据的访问入口
-
验证
验证、准备、解析这三步可以看做是一个连接的过程,将类的字节码连接到jvm的运行状态之中
验证是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,不会威胁到jvm的安全,主要包括以下几个方面的验证:- 文件格式的验证,验证字节流是否符合class文件的规范,是否能被当前版本的虚拟机处理
- 元数据验证,对字节码描述的信息进行语义分析,确保符合java语言规范
- 字节码验证 通过数据流和控制流分析,确定语义是合法的,符合逻辑的
- 符号引用验证 这个校验在解析阶段发生
- 准备
为类的静态变量分配内存,初始化为系统的初始值。对于final static修饰的变量,直接赋值为用户的定义值。如下面的例子:这里在准备阶段过后的初始值为0,而不是7java public static int a=7
-
解析
解析是将常量池内的符号引用转为直接引用(如物理内存地址指针)
-
初始化
到了初始化阶段,jvm才真正开始执行类中定义的java代码
1)初始化阶段是执行类构造器()方法的过程。类构造器 ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
2)当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化。
3)虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
jvm内存分区
-
程序计数器
程序计数器(progarm counter register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码行号指示器。在jvm中,通过程序计数器来记录某个线程的字节码执行位置,或者说记录下一条要运行的指令。程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器,互不影响,是一块线程私有的内存空间。
如果当前正在执行的是一个java方法,程序计数器会记录正在执行的java字节码地址;如果正在执行的是native方法,则程序计数器为空。 -
java虚拟机栈
java虚拟机栈是线程私有的内存空间,它用来保存方法的局部变量、部分结果,并参与方法的调用和返回。
虚拟机栈在运营师采用栈帧来保存数据,栈帧中主要有局部变量表、操作数栈、动态链接地址、返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作,相应的,方法的返回则对应着栈帧的出战操作。
和java栈相关的两个异常:- *error
在线程的计算过程中,如果请求的栈的深度大于最大可用的栈深度,则抛出改异常。
- outofmemoryerror
如果java的栈可以扩展,在程序运行过程中,没有足够的内存来支撑程序的扩展,则抛出该异常。
- *error
本地方法栈
本地方法栈和java虚拟机栈功能类似,本地方法栈主要管理本地方法栈的调用,一般是指有c实现的。和java虚拟机栈一样会抛出*error和outofmemoryerror异常-
方法区
方法区是java内存区域中比较重要的一部分,主要保存的信息是元数据。其中最为重要的是类的类型信息、常量池、域信息、方法信息。
java堆
java堆可以说是java运行时内存中最为重要的一部分,几乎所有的对象和数据都是在堆中分配空间的。java堆分为新生代和老年代两个部分,新生代用于存放刚刚产生的对象,如果对象一直没有被回收,生存的足够长,老年对象就会被移入老年代。
新生代又可以细分为eden、surivor space0(s0或者from space)和surivor space1(s1或者to space)。eden存放刚刚创建的对象,s0和s1存放的对象至少经历了一次垃圾回收,等幸存下来。如果幸存去的对象到了指定年龄仍未被回收,就会进入老年代。
持久代:permanent generation。在sun的jvm中就是方法区的意思,尽管有些jvm大多没有这一代。主要存放常量及类的一些信息默认最小值为16mb,最大值为64mb
垃圾收集算法
- mark-sweep(标记-清除)算法
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。- 缺点:空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作;
- 优点:简单快速
- copying(复制)算法
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。- 缺点:内存使用率只有一半
- 优点:不会产生碎片
mark-compact(标记-整理)算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(generational collection)算法,根据对象存活周期的不同将内存划分为几块并采用不用的垃圾收集算法。
一般是把 java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
垃圾收集器
- serial收集器
新生代收集器,使用停止复制算法,使用一个线程进行gc,串行,其它工作线程暂停。 - parnew收集器
新生代收集器,使用停止复制算法,serial收集器的多线程版,用多个线程进行gc,并行,其它工作线程暂停,关注缩短垃圾收集时间。 - parallel scavenge 收集器
新生代收集器,使用停止复制算法,关注cpu吞吐量,即运行用户代码的时间/总时间,比如:jvm运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用cpu,适合运行后台运算(关注缩短垃圾收集时间的收集器,如cms,等待时间很少,所以适 合用户交互,提高用户体验)。 -
serial old收集器
老年代收集器,单线程收集器,串行,使用标记整理(整理的方法是sweep(清理)和compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行gc,其它工作线程暂停(注意,在老年代中进行标记整理算法清理,也需要暂停其它线程),在jdk1.5之前,serial old收集器与parallelscavenge搭配使用。 - parallel old收集器
老年代收集器,多线程,并行,多线程机制与parallel scavenge差不错,使用标记整理(与serial old不同,这里的整理是summary(汇总)和compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像sweep(清理)那样清理废弃的对象)算法,在parallel old执行时,仍然需要暂停其它线程。parallel old在多核计算中很有用。parallel old出现后(jdk 1.6),与parallel scavenge配合有很好的效果,充分体现parallel scavenge收集器吞吐量优先的效果。 -
cms(concurrent mark sweep)收集器
老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和gc线程同时工作),停顿小。使用-xx:+useconcmarksweepgc进行parnew+cms+serial old进行内存回收,优先使用parnew+cms(原因见后面),当用户线程内存不足时,采用备用方案serial old收集。 - g1收集器
初始标记阶段仅仅只是标记一下 gc roots 能直接关联到的对象,并且修改 tams(next top at mark start)的值,让下一阶段用户程序并发运行时,能在正确可用的 region 中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记阶段是从 gc root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 remembered set logs 里面,最终标记阶段需要把 remembered set logs 的数据合并到 remembered set 中,这阶段需要停顿线程,但是可并行执行。
最后在筛选回收阶段首先对各个 region 的回收价值和成本进行排序,根据用户所期望的 gc 停顿时间来制定回收计划,从sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。通过下图可以比较清楚地看到g1收集器的运作步骤中并发和需要停顿的阶段。