学了这么久的Java,一直听说JVM虚拟机是运行所有java程序,但是不知道具体内部结构是怎样,以及它的运行机制是什么。今天刚好看到一篇文章,索性就开始学习。
JVM的主要结构:
由上图可以看出,Jvm主要组成有:类加载器、运行数据区、执行引擎、本地方法接口组成。其中运行数据区包含子模块方法区、堆、Java栈、本地方法栈以及寄存器。对于方法区、堆是对所有线程共享的,而其他则是属于当前线程私有。
下面开始一步步剖析JVM...........
1、类加载器(Class loader)
类加载器负责加载编译好的.class字节码文件,并装入内存,使其JVM可以实例化或者以其它方式使用加载后的类。Jvm的类加载支持在运行时的动态加载,可以节省内存空间、灵活网上加载以及通过命名空间分隔来实现类的隔离,增加系统的安全性。
1)Class Loader分类:
a、启动类加载器(BootStrap Class Loader):负责加载rt.jar文件中所有的Java类,java的核心类都是由该ClassLoader加载。
b、扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包。
c、系统类加载器(System Class Loader):负责加载启动参数中指定的classpath中的jar包及目录,通常自己写的类由该ClassLoader加载。
d、用户自定义加载器(User Defined Class Loader):由用户自定义的加载规则,可以手动控制加载过程中的步骤。
2) Class Loader工作原理(三步:装载、链接、初始化)
a、装载:通过类的全限定名和Class Loader加载类,主要是将指定的.class文件加载到JVM。内部以"xxx.class+ClassLoader实例 id"来标明类。在内存中,ClassLoader实例和类的实例都位于堆中,它们的类信息都位于方法区中。装载过程采用了一种“双亲委派模型 (Parent Delegation Model)”的方式,当一个ClassLoader要加载时,它会先请求它的双亲ClassLoader(父ClassLoader)加载类,而他的 父ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有等到父ClassLoader无法加载指定的类时,它 才会自己加载类。
双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加 载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存 在。这样即使有恶意的类冒充自己在核心包(例如java.lang)下,由于它无法被启动类加载器加载,也造成不了危 害。由此也可见,如果用户自定义了类加载器,那就必须自己保障类加载过程中的安全。
b、链接(把二进制的类型信息合并到JVM运行时状态中去)
验证:校验.class文件的正确性,确保该文件是否符合规范定义的,并且适合当前JVM使用。
准备:为类分配内存,同时初始化类中静态变量赋值为默认值。
解析:主要是把类的常量池的符号引用解析为直接引用。
c、初始化(类中的静态变量初始化,并执行类中的static代码、构造函数)
通过new关键字、反射、clone、反序列化机制实例化对象。
调用类的静态方法时。
使用类的静态字段或对其赋值时。
通过反射调用类的方法时。
初始化该类的子类时(初始化子类前父类必须已经被初始化)
JVM启动时被标记为启动类的类
2、Java栈(statck)
栈由栈帧组成,一个帧对应一个方法的调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部门其它模块工作需要的数据。
1)、局部变量区
a、局部变量区是以字长为单位的数组,byte、short、char类型会被转换成int类型存储,除了long和double类型占两个字长以外,其 余类型都只占用一个字长。boolean类型在编译时会被转换成int或byte类型,boolean数组会被当做byte类型数组来处理。局部变 量区也会包含对象的引用,包括类引用、接口引用以及数组引用。局部变量区包含了方法参数和局部变量,此外,实例方法含第 一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。
2)、操作数栈
a、操作数栈也是以字长为单位的数组,它只能进行入栈出栈的基本操作。
3)、帧数据区
a、记录指向类的常量池的指针,便于解析。
b、帮助方法正常返回,包括恢复调用该方法的栈帧,设置寄存器指向调用方法对应的下一条指令,把返回值压入栈帧的操作数栈 中。
c、记录异常表,发生异常时将控制权交由对应异常catch,如果没有找到对应catch,会恢复调用方法的栈帧并重新抛出异常。
局部变量区和操作数栈的大小依照具体方法在编译时就已确定。调用方法时会从方法区中找到对应的类型信息,从中得到具体方法的局部变量和操作数栈大小,依次分配栈帧内存,压入栈。
3、本地方法栈
本地方法栈类似于Java栈,主要存醋了本地方法调用的状态。
4、方法区(类型信息和类的静态变量都存储在方法区中)
a、类及其父类的全限定名
b、类的类型
c、访问修饰符
d、实现的接口的全限定名的列表
e、常量池
f、字段信息
g、方法信息
h、静态变量
i、ClassLoader引用
j、Class引用
类的所有信息都存储在方法区中。由于方法区是所有线程共享的,所以必须保证线程安全。
5、堆(Heap)
堆用于存储对象实例以及数组值。堆中有指向类数据的指针,该指针指向了方法区中对应类型信息。堆中还可能存放了指向方法的指针。堆是线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。堆中的实例数据中包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。
1)、新生代(New Generation)
大多数情况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成。后者主要用于Minor GC 的对象复制。
JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配。在堆上分配内存 需要锁定整个堆,在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配借此提高效率。
2)、旧生代(Old Generation/Tenuring Generation)
在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。
6、执行引擎
执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。
JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能的紧凑,便于快速的在网络上传输,同时也很容易适 应通用寄存器较少的平台,有利于代码优化,由于Java栈和寄存器是线程私有的,线程之间无法相互干涉彼此的栈。每个线程拥 有独立的JVM执行引擎实例。
JVM指令由单字节码操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋 值,也会先入栈再赋值。
1)、解释执行
采用token-threading的方式
a、栈顶缓存
将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令来说,就无需再入栈,可以直接在寄存器 上进行计算,结果压入操作数栈。这样减少了寄存器和内存的交换开销。
b、部分栈帧共享
被调用方法可调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了赋值参数的开销。
c、执行机器指令
特殊情况。。。。。
2)、编译执行
为了提升执行速度,主要利用JIT(Just-In-Time)编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存, 之后就可以重复利用。
3)、自适应优化执行
自适应优化执行的思想是程序中10%~20%的代码占据了80%~90%的执行时间,所以通过将那少部分代码编译为优化过的机器 码就可以大大提升执行效率。JVM会监测代码的执行情况,当判断特定方法是瓶颈或热点时,将会启动一个后台线程,把该字节码 编译为极度优化的、静态链接的代码。当方法不再是热区时,则会取消编译过的代码,重新进行解释执行。
自适应在执行过程中时刻监测,对内联代码等优化起到了很大的作用。由于面向对象的多态性,一个方法可能对应了很多种不同实 现,自适应优化就可以通过监测只内联那些用到的代码,大大减少了内联函数的大小。
JDK在编译上采用了两种模式:Client和Server模式。牵着较为轻量级,占用内存较少。后者的优化程度更高,占用内存更多。
执行引擎必须保证线程安全性,因而JMM(Java Memory Model)也是由执行引擎确保的。
本文出自 “througth” 博客,请务必保留此出处http://througth.blog.51cto.com/6471265/1709753
原文地址:http://througth.blog.51cto.com/6471265/1709753