本文旨在给全部但愿了解JVM(Java Virtual Machine)的同窗一个概念性的入门,主要介绍了JVM的组成部分以及它们内部工做的机制和原理。固然本文只是一个简单的入门,不会涉及过多繁杂的参数和配置,感兴趣的同窗能够作更深刻的研究,在研究JVM的过程当中会发现,其实JVM自己就是一个计算机体系结构,不少原理和咱们平时的硬件、微机原理、操做系统都有十分类似的地方,因此学习JVM自己也是加深自我对计算机结构认识的一个很好的途径。 html
另外须要注意的是,虽然平时咱们用的大可能是Sun(现已被Oracle收购)JDK提供的JVM,可是JVM自己是一个规范,因此能够有多种实现,除了Hotspot外,还有诸如Oracle的JRockit、IBM的J9也都是很是有名的JVM。 java
下图展现了JVM的主要结构: 程序员
能够看出,JVM主要由类加载器子系统、运行时数据区(内存空间)、执行引擎以及与本地方法接口等组成。其中运行时数据区又由方法区、堆、Java栈、PC寄存器、本地方法栈组成。 数组
从上图中还能够看出,在内存空间中方法区和堆是全部Java线程共享的,而Java栈、本地方法栈、PC寄存器则由每一个线程私有,这会引出一些问题,后文会进行具体讨论。 浏览器
众所周知,Java语言具备跨平台的特性,这也是由JVM来实现的。更准确地说,是Sun利用JVM在不一样平台上的实现帮咱们把平台相关性的问题给解决了,这就比如是HTML语言能够在不一样厂商的浏览器上呈现元素(虽然某些浏览器在对W3C标准的支持上还有一些问题)。同时,Java语言支持经过JNI(Java Native Interface)来实现本地方法的调用,可是须要注意到,若是你在Java程序用调用了本地方法,那么你的程序就极可能再也不具备跨平台性,即本地方法会破坏平台无关性。 缓存
类加载器子系统负责加载编译好的.class字节码文件,并装入内存,使JVM能够实例化或以其它方式使用加载后的类。JVM的类加载子系统支持在运行时的动态加载,动态加载的优势有不少,例如能够节省内存空间、灵活地从网络上加载类,动态加载的另外一好处是能够经过命名空间的分隔来实现类的隔离,加强了整个系统的安全性。 安全
一、ClassLoader的分类:
a.启动类加载器(BootStrap Class Loader):负责加载rt.jar文件中全部的Java类,即Java的核心类都是由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,而且在Java语言中没法得到它的引用。 网络
b.扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包。 oracle
c.系统类加载器(System Class Loader):负责加载启动参数中指定的Classpath中的jar包及目录,一般咱们本身写的Java类也是由该ClassLoader加载。在Sun JDK中,系统类加载器的名字叫AppClassLoader。 jvm
d.用户自定义类加载器(User Defined Class Loader):由用户自定义类的加载规则,能够手动控制加载过程当中的步骤。
二、ClassLoader的工做原理
类加载分为装载、连接、初始化三步。
a.装载
经过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM。当类被加载之后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。
在内存中,ClassLoader实例和类的实例都位于堆中,它们的类信息都位于方法区。
装载过程采用了一种被称为“双亲委派模型(Parent Delegation Model)”的方式,当一个ClassLoader要加载类时,它会先请求它的双亲ClassLoader(其实这里只有两个ClassLoader,因此称为父ClassLoader可能更容易理解)加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader没法加载指定的类时,它才会本身加载类。
双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不一样类加载器加载的类之间是没法直接交互的,即便是同一个类,被不一样的ClassLoader加载,它们也没法感知到彼此的存在。这样即便有恶意的类冒充本身在核心包(例如java.lang)下,因为它没法被启动类加载器加载,也形成不了危害。
由此也可见,若是用户自定义了类加载器,那就必须本身保障类加载过程当中的安全。
b.连接
连接的任务是把二进制的类型信息合并到JVM运行时状态中去。
连接分为如下三步:
a.验证:校验.class文件的正确性,确保该文件是符合规范定义的,而且适合当前JVM使用。
b.准备:为类分配内存,同时初始化类中的静态变量赋值为默认值。
c.解析(可选):主要是把类的常量池中的符号引用解析为直接引用,这一步能够在用到相应的引用时再解析。
c.初始化
初始化类中的静态变量,并执行类中的static代码、构造函数。
JVM规范严格定义了什么时候须要对类进行初始化:
a、经过new关键字、反射、clone、反序列化机制实例化对象时。
b、调用类的静态方法时。
c、使用类的静态字段或对其赋值时。
d、经过反射调用类的方法时。
e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。
f、JVM启动时被标记为启动类的类(简单理解为具备main方法的类)。
Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。Java栈的主要任务是存储方法参数、局部变量、中间运算结果,而且提供部分其它模块工做须要的数据。前面已经提到Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程自己能够访问它本身的局部变量区。
它分为三部分:局部变量区、操做数栈、帧数据区。
一、局部变量区
局部变量区是以字长为单位的数组,在这里,byte、short、char类型会被转换成int类型存储,除了long和double类型占两个字长之外,其他类型都只占用一个字长。特别地,boolean类型在编译时会被转换成int或byte类型,boolean数组会被当作byte类型数组来处理。局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。
局部变量区包含了方法参数和局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。
二、操做数栈
操做数栈也是以字长为单位的数组,可是正如其名,它只能进行入栈出栈的基本操做。在进行计算时,操做数被弹出栈,计算完毕后再入栈。
三、帧数据区
帧数据区的任务主要有:
a.记录指向类的常量池的指针,以便于解析。
b.帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操做数栈中。
c.记录异常表,发生异常时将控制权交由对应异常的catch子句,若是没有找到对应的catch子句,会恢复调用方法的栈帧并从新抛出异常。
局部变量区和操做数栈的大小依照具体方法在编译时就已经肯定。调用方法时会从方法区中找到对应类的类型信息,从中获得具体方法的局部变量区和操做数栈的大小,依此分配栈帧内存,压入Java栈。
本地方法栈相似于Java栈,主要存储了本地方法调用的状态。在Sun JDK中,本地方法栈和Java栈是同一个。
类型信息和类的静态变量都存储在方法区中。方法区中对于每一个类存储了如下数据:
a.类及其父类的全限定名(java.lang.Object没有父类)
b.类的类型(Class or Interface)
c.访问修饰符(public, abstract, final)
d.实现的接口的全限定名的列表
e.常量池
f.字段信息
g.方法信息
h.静态变量
i.ClassLoader引用
j.Class引用
可见类的全部信息都存储在方法区中。因为方法区是全部线程共享的,因此必须保证线程安全,举例来讲,若是两个类同时要加载一个还没有被加载的类,那么一个类会请求它的ClassLoader去加载须要的类,另外一个类只能等待而不会重复加载。
此外为了加快调用方法的速度,一般还会为每一个非抽象类建立私有的方法表,方法表是一个数组,存放了实例可能被调用的实例方法的直接引用。方法表对于多态有很是重要的意义,具体能够参照《浅谈多态机制的意义及实现》一文中“多态的实现”一节。
在Sun JDK中,方法区对应了持久代(Permanent Generation),默认最小值为16MB,最大值为64MB。
堆用于存储对象实例以及数组值。堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。堆是全部线程共享的,因此在进行实例化对象等操做时,须要解决同步问题。此外,堆中的实例数据中还包含了对象锁,而且针对不一样的垃圾收集策略,可能存放了引用计数或清扫标记等数据。
在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为新生代、旧生代。分代方式大大改善了垃圾收集的效率。
一、新生代(New Generation)
大多数状况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后二者主要用于Minor GC时的对象复制(Minor GC的过程在此不详细讨论)。
JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配,咱们知道在堆上分配内存须要锁定整个堆,而在TLAB上则不须要,JVM在分配对象时会尽可能在TLAB上分配,以提升效率。
二、旧生代(Old Generation/Tenuring Generation)
在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。
执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。
JVM的指令集是基于栈而非寄存器的,这样作的好处在于可使指令尽量紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,而且有利于代码优化,因为Java栈和PC寄存器是线程私有的,线程之间没法互相干涉彼此的栈。每一个线程拥有独立的JVM执行引擎实例。
JVM指令由单字节操做码和若干操做数组成。对于须要操做数的指令,一般是先把操做数压入操做数栈,即便是对局部变量赋值,也会先入栈再赋值。注意这里是“一般”状况,以后会讲到因为优化致使的例外。
一、解释执行
和一些动态语言相似,JVM能够解释执行字节码。Sun JDK采用了token-threading的方式,感兴趣的同窗能够深刻了解一下。
解释执行中有几种优化方式:
a.栈顶缓存
将位于操做数栈顶的值直接缓存在寄存器上,对于大部分只须要一个操做数的指令而言,就无需再入栈,能够直接在寄存器上进行计算,结果压入操做数站。这样便减小了寄存器和内存的交换开销。
b.部分栈帧共享
被调用方法可将调用方法栈帧中的操做数栈做为本身的局部变量区,这样在获取方法参数时减小了复制参数的开销。
c.执行机器指令
在一些特殊状况下,JVM会执行机器指令以提升速度。
二、编译执行
为了提高执行速度,Sun JDK提供了将字节码编译为机器指令的支持,主要利用了JIT(Just-In-Time)编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存,以后就能够重复利用。Oracle JRockit采用的是彻底的编译执行。
三、自适应优化执行
自适应优化执行的思想是程序中10%~20%的代码占据了80%~90%的执行时间,因此经过将那少部分代码编译为优化过的机器码就能够大大提高执行效率。自适应优化的典型表明是Sun的Hotspot VM,正如其名,JVM会监测代码的执行状况,当判断特定方法是瓶颈或热点时,将会启动一个后台线程,把该方法的字节码编译为极度优化的、静态连接的C++代码。当方法再也不是热区时,则会取消编译过的代码,从新进行解释执行。
自适应优化不只经过利用小部分的编译时间得到大部分的效率提高,并且因为在执行过程当中时刻监测,对内联代码等优化也起到了很大的做用。因为面向对象的多态性,一个方法可能对应了不少种不一样实现,自适应优化就能够经过监测只内联那些用到的代码,大大减小了内联函数的大小。
Sun JDK在编译上采用了两种模式:Client和Server模式。前者较为轻量级,占用内存较少。后者的优化程序更高,占用内存更多。
在Server模式中会进行对象的逃逸分析,即方法中的对象是否会在方法外使用,若是被其它方法使用了,则该对象是逃逸的。对于非逃逸对象,JVM会在栈上直接分配对象(因此对象不必定是在堆上分配的),线程获取对象会更加快速,同时当方法返回时,因为栈帧被抛弃,也有利于对象的垃圾收集。Server模式还会经过分析去除一些没必要要的同步,感兴趣的同窗能够研究一下Sun JDK 6引入的Biased Locking机制。
此外,执行引擎也必须保证线程安全性,于是JMM(Java Memory Model)也是由执行引擎确保的。