虚拟机
何为虚拟机呢?虚拟机是模拟执行某种指令集体系结构(ISA)的软件,是对操做系统和硬件的一种抽象。其软件模型以下图所示:java

计算机系统的这种抽象相似于面向对象编程(OOP)中的针对接口编程泛型(或者是依赖倒转原则),经过一层抽象提取底层实现中共性的部分,底层实现这个抽象并完成本身个性的部分。也就是说经过一个抽象层次来隔离底层的不一样实现。虚拟机规范定义了这个虚拟机要完成的功能(也就是接口),底层的操做系统和硬件利用本身提供的功能来实现虚拟机须要完成的功能(实现)。经过运行在虚拟机之上,Java才具备很好跨平台特性。python

Java虚拟机
Java虚拟机(JVM)是由Java虚拟机规范定义的,其上运行的是字节码指令集。这种字节码指令集包含一个字节的操做码(opcode),零至多个操做数(oprand),虚拟机规范明肯定义了每种字节码指令完成的功能是什么以及须要多少个操做数。Java虚拟机上运行的class文件,这个文件中包含字节码指令流以及类定义的信息,因此Java虚拟机规范还定义了class文件的格式(精确到每一个字节)。因此实现Java虚拟机的两个要素是字节码指令集和class文件格式,Java虚拟机的实现者只要以正确方式读取class文件中的每一条字节码指令,并按照要求实现字节码指令的功能就能够实现JVM。程序员
目前经常使用的商用JVM主要有:Sun HotSpot,BEA JRocket以及IBM J9。其中因为BEA和Sun已经被Oracle收购,因此Oracle拥有当今世界上最流行的两个JVM,并有传言说Oracle将在Java8时将两个虚拟机合并,各取所需,取长补短,打造一个更加精湛的JVM。HotSpot会以解释+即时编译执行代码,HotSpot在解释执行字节码的时候,会探测热点(hotspot)代码,而后将这部分代码编译为本地代码,以后将直接运行本地代码,而不是解释,这样会有效提升虚拟机性能。JRocket主要是定位于服务器应用,因此不关注虚拟机的启动速度,它会将全部代码即时编译为本地代码执行,JRocket的垃圾收集器具备很高的收集效率。J9定位与HotSpot相似,专一于桌面应用和服务器应用,主要是针对IBM的各类Java产品。算法
Java语言与Java虚拟机
咱们知道Java源代码,即.java文件,经过javac编译为.class文件。.class文件能够运行在JVM上,JVM底层会经过字节码解释器或者即时编译器(JIT Compiler)执行.class文件中的字节码指令。JVM是运行在操做系统之上的,操做系统又经过指令集调用底层硬件服务执行其上的各类软件。编程

能够看到Java是运行在JVM之上的。可是Java语言和JVM没有必然的联系。Java语言并非只能运行在JVM之上,只要实现了相应的编译器Java语言就能够运行在任何平台之上(好比J++),也能够被编译为本地代码直接运行在操做系统之上,好比,Linux上的GCJ(GNU Compiler for Java)就能够把Java语言编译为本地代码直接执行。一样的,JVM上也不是只能执行Java语言,只要实现了适当的编译器,将其余语言编译为JVM上的字节码,就能够在JVM上运行。好比,JRuby,Jython以及Groovy等其余JVM语言,都会经过相应的编译器或是解释器转化为.class,而后再JVM上运行。因为JVM并不关心.class文件是由Java、JRuby、Jython等转化而来,只要这个文件结构正确并能经过class文件校验。所以,因为.class文件屏蔽了Java、JRuby等上层语言的差别,因此Java、Groovy等能够相互调用。windows
JVM生命周期
当启动一个Java程序时,一个虚拟机实例就诞生了,当程序关闭退出时,这个虚拟机实例随之消亡。JVM实例经过main()方法来运行一个Java程序。而这个main()方法必须是共有的(public)、静态的(static)、返回void,而且接收一个字符串数组为参数。Java程序初始类中的main()方法,将做为改程序初始线程的起点,任何其余线程都是由这个初试线程启动的。数组
JVM内部有两种线程:守护线程与非守护线程。守护线程一般是由虚拟机本身使用的,好比垃圾回收线程。当该程序全部的非守护线程都终止时,JVM实例将自动退出。安全
Java虚拟机体系结构
JVM由类加载器子系统,运行时数据区,执行引擎以及本地方法接口组成。服务器

类加载器子系统数据结构
类加载器子系统主要用于定位类定义的二进制信息,而后将这些信息解析并加载至虚拟机,转化为虚拟机内部的类型信息的数据结构。类加载器子系统还承担着安全性的责任,而且是JVM的动态连接和动态加载的基础。将二进制信息=>类型信息的数据结构,中间须要通过不少步骤。首先类加载器是JVM安全沙箱的第一道防线,可以防止非信任类破坏虚拟机。每个被加载的class文件须要通过四次校验才能被加载。校验经过后,类加载器的命名空间和运行时包的特性可以防止非信任类假装成信任类来破坏虚拟机。类加载器在方法区构造具备这个类的信息的数据结构后,会在堆上建立一个Class对象做为访问这个数据结构的接口。同时,类加载还须要初始化类的静态数据,也就是调用类的方法。以上就是一个类的加载、连接及初始化的过程。
运行时数据区
运行时数据区是JVM运行时的内存空间的组织,逻辑上又划分为多个区,这些区的生命周期和它是否线程共享有关,它们分别是:
堆
用于存放对象或数组实例,也就是运行期间new出来的对象。堆的生命周期与JVM相同,而且在线程之间共享访问。因为多线程并发访问,因此须要考虑线程安全的问题,有两种方法。第一种是,加锁进行互斥访问。第二种是线程本地分配缓冲(Thread Local Allocate Buffer, TLAB),在线程建立时预先给每一个线程分配一块区域,这块区域是线程私有的,对其余线程是不可见,也就不会被共享。JVM规范规定在申请不到足够的内存时,堆会抛出OutOfMemoryException。
方法区
存放类型信息和运行时常量池(Runtime Constant Pool)。每一个被类加载器加载的类都会在方法区中造成一个与子对应的类型信息的数据结构,包括:这个类的类名、直接超类、实现的接口列表、字段列表、方法列表等。运行时常量池是class文件中的常量池列表(Constant Pool List)在运行时的一种体现,其中存储各类基本数据类型及String类型的常量以及其余类、方法、字段的符号引用。方法区的生命周期与JVM相同,被多个线程共享,因此要考虑并发访问的安全性的问题。JVM规范规定在须要的内存得不到知足的状况下,方法区会抛出OutOfMemoryException。
PC(Program Counter)
线程私有的,生命周期与线程相同,是对CPU中PC的一种模拟。若是线程正在执行的是Java方法,则该线程的PC中存放的下一条字节码指令的地址。在进行Java方法的调用和返回时,须要更新PC以保存当前方法(Current Method)正在执行的字节码指令的地址。PC是JVM规范中惟一没有规定会抛出异常的存储区。
JVM栈
线程私有,生命周期与线程相同,是对传统语言(好比C)中的方法调用栈的一种模拟。JVM栈中存放栈帧(Frame)用于进行方法调用和返回、存储局部变量以及计算的中间结果。JVM规范规定栈能够抛出两种异常:(1)StackOverflowException,在栈的深度大于某个规定值的状况下抛出。(2)OutOfMemoryException,在为新栈帧分配内存或者是为线程分配栈的内存时,申请不到足够的内存的状况下抛出。
JVM栈中存放的是栈帧,每一个栈帧对应着一次方法调用。每一时刻,JVM线程只能执行一个方法(Current Method),该方法的栈帧是JVM栈的栈顶的元素(叫作当前栈帧,Current Frame),当调用一个方法时,会初始化一个栈帧压入JVM栈;当方法调用返回或者抛出异常没有被处理的状况下,JVM栈会弹出该方法对应的栈帧。每个栈帧中存放局部变量表(Local Variable Table)、操做数栈(Oprand Stack)以及其余栈帧信息。栈帧的大小在编译时就肯定了,编译器会把局部变量表和操做数栈的大小记录在class文件中method_info的属性表中。局部变量表相似于数组存放局部变量和方法参数。因为JVM采用的是基于栈的指令集体系结构,而不是基于寄存器,因此JVM上的全部计算都是在操做数栈上进行的(好比,算术运算、方法调用、内存访问等)。
本地方法栈
用于支持本地方法调用,抛出的异常与JVM栈相同。
执行引擎
执行引擎用于执行JVM字节码指令,主要由两种实现方式:
(1)将输入的字节码指令在加载时或执行时翻译成另一种虚拟机指令;
(2)将输入的字节码指令在加载时或执行时翻译成宿主主机本地CPU的指令集。这两种方式对应着字节码的解释执行和即时编译。好比在HotSpot VM中执行引擎的实现是一种解释-编译的层次结构:
(1)解释执行:解释执行字节码,并以方法为单位收集“热点(HotSpot)代码”的信息,将“热点代码”执行C0编译。
(2)C0编译:将收集的“热点代码”编译成本地代码,并进行一些简单的优化。继续收集运行时信息,将一些频繁执行的本地代码进行C1编译。
(3)C1编译:将C0阶段的本地代码,进行一些比较激进的优化。若是某些优化致使本地代码执行失败,此时JVM会退化到解释执行字节码阶段。
自动内存管理
自动内存管理用于管理运行时数据区的分配和释放。和C和C++相比,Java不须要程序员主动的管理内存(在new出对象后,不须要显示的delete),这样JVM就须要承担内存管理这个任务。内存管理的重点主要是在申请内存(new对象、类加载和初始化、启动线程时初始化栈等)得不到知足时,JVM能够自动回收那些再也不存活的对象所占用的内存,也就是常常听到的垃圾收集。在回收过程当中还要保证处理内存空间的碎片,以提升空间利用率。回收过程主要有两个关键点,标记存活对象和回收内存的算法。
标记存活对象主要有引用计算和根搜索法两种。
(1)引用计数,是一种很广泛的方法,在python、lua等一些脚本语言中都是使用这种算法。每一个对象持有一个计数器,标记这个对象被引用的次数。进行垃圾收集时,那些引用计数为0的对象就是“死”对象,须要被收集。引用计数的一个缺点就是它没有办法处理循环引用的状况(A->B, B->A)。
(2)根搜索,HotSpot虚拟机采用这种算法标记存活对象。把方法区、JVM栈中的全部的引用组成的集合做为搜索的根,从这个集合开始遍历直到结束。其中被遍历到的对象是存活对象;那些没有被遍历到的对象须要被垃圾收集。这样能够有效的避免循环引用的状况。
回收内存的算法主要有:
(1)复制算法,将内存分红两个部分,每一时刻只是用其中的一个。进行回收时,将全部存活的对象依次复制到另外一个部分(依次复制避免了内存碎片的产生),接下来只用这一个部分。复制算法须要在两个内存区域来回复制,有必定的复制开销和空间开销(每一时刻只使用一个区域),可是能够很好的解决内存碎片的问题,适用于对象频繁建立而且生命周期短的状况。
(2)标记清扫,先进行存活对象标记,回收时将“死”对象占用的内存直接释放掉,会产生大量的内存碎片。
(3)标记整理,标记阶段与标记清扫算法同样,回收阶段释放“死”对象的内存后,还须要进行对象的移动使得全部对象依次在内存中排列,避免了内存碎片的产生。标记整理与复制算法相反,适用于对象建立不频繁,生命周期长得状况。
(4)按代收集,将内存按照对象生命周期的不一样划分为多个部分,每一个部分采用不一样的收集算法。目前,大部分商业虚拟机都是采用这种算法。好比,在HotSpot中,内存被划分为:新生代(New)、老年代(Old)和永久代(Perm)。新生代采用复制算法,老年代和永久代采用标记整理算法。内存分配、回收的策略是,对象首先在新生代分配,若是新生代内存不知足要求,则触发一次新生代内存的垃圾收集(Young GC,或者是Minor GC)。Young GC会致使部分新生代的对象被移动至老年代,一部分是由于新生代内存不足以放下全部的对象;另外一部分是由于这些对象的年龄(每一个对象都保存着这个对象被垃圾收集的次数,表示它的年龄。存储在对象头的age属性中)大到足以晋升到老年代。当新生代的对象进入老年代,而老年代的内存不知足要求时,则会触发一次整个新生代和老年代的垃圾收集(Full GC, 或者是Major GC)。
在JVM中有多个后台线程用于完成自动内存管理,对于CPU来讲这些后台线程和用户线程是同样的,都须要占用系统的资源。在GC线程进行垃圾收集时必须执行“Stop the World”这一操做,也就是暂停全部的用户线程。这就致使对于实时性要求比较高的系统,JVM的垃圾收集多是一个短板。可是在JDK1.5,Sun提供了CMS(Concurrent Mark and Sweep)垃圾收集器,经过GC线程和用户线程并发执行减小GC时间,提升了JVM的实时性。在JVM的各类应用中,gc调优是一个关键的部分,主要目标是减小GC的次数而且下降每次GC的时间。关于这部份内容,后续的JVM内存管理会详细讨论。
JVM执行程序的流程分析
在命令行执行”java Main”就会开启一个JVM实例,咱们能够经过jps,jstat等JVM工具观察JVM的运行状态,下面以运行com.ntes.money.Main这个类为例来描述一下JVM执行一个程序的流程。
当在命令行执行”java -Xmx=12m -Xms=12m -Dname=value com.ntes.money.Main”这个命令时,JVM的执行流程是:
1)加载JVM,主要是加载动态连接库,windows下是jvm.dll,Linux下是libjvm.so;
2)设置JVM启动参数,好比命令中的-Xmx=12m -Xms=12m用于设置堆大小。
3)初始化JVM。
4)调用类加载器子系统,加载com.ntes.money.Main。这里给出的是自定义类,根据类加载器双亲委派链,最后是由系统默认类加载器(Classpath类加载器)进行加载。首先,根据全路径类型转化为文件路径com/ntes/money/Main.class,而后读取Main.class中的二进制信息、解析、加载,在方法区中造成Main类对应的数据结构。这里可能抛出ClassNotFoundException,有两种缘由。一是文件路径com/ntes/money/Main.class不存在;二是com/ntes/money/Main.class文件路径存在,可是Main.class文件中存储的不是Main类的信息,好比是Main1,Main2等其余类的信息。这种状况下,会抛出NoClassDefFoundError,而后致使ClassNotFoundException。
5)在方法区com.ntes.money.Main类对应的数据结构中,根据方法描述符及访问标志,查找main方法。这里的描述符,包括了方法的方法名、参数、返回值,也就是public static void main(String[])。若是找不到对应的main方法,会抛出NoSuchMethodError: main异常。
6)经过本地方法(JNI)执行main方法。
更多资料,私信小编“资料”即可领取。
但愿你帮小编点赞和转发哦~