Java虚拟机学习笔记(1)--小试
最近开始研究jvm,mark一下。。。
1. 概览
虚拟机,一个根据规范定义出的虚拟机的计算机
虚拟机可能是:一个规范、一个实现、一个运行中的机器实列
1.1 生命周期
当java程序开始运行时一个新的jvm实列就出现了,启动jvm必须指定初始类,初始类中的有大家熟悉的main方法
main会被初始线程调用,其他线程(守护和非守护)都是从此线程衍生
解释: 只要有非守护线程在jvm就会继续,当没了非守护jvm不管有没有守护线程都会shutdown
1.2 体系结构
子系统+内存区+数据类型+指令
子系统主要有ClassLoader子系统(装载class)和执行引擎子系统(执行指令)
内存区:jvm instance都有shared的方法区(类型信息区)和堆(对象区),而当每个线程启动时,又会得到自己的PC寄 存器(熟悉的PC)和一个Java栈,如果调用了本地方法还会得到本地方法栈。
jvm没有使用寄存器,指令直接操作栈,从而让指令集更紧凑,兼容更多平台,也更好的适应动态编译和即时编译
1.2.1 数据类型
jvm规定了数据类型和支持的运算
类型可以分为基本类型和引用类型两种
Java中基本类型同样也是jvm中的基本类型,但boolean有点特殊,boolean可能被映射为int or byte;jvm还有个只在内部使用的returnAddress类型,用于finally;
1.2.2 字长设置
一般一个字长可以容下byte、short、char、int、float等
两个字长可以容下long和double
1.2.3 ClassLoader子系统
classloader有两种bootstrap和user classloader。bootstrap是jvm自己实现的(可能是c),而后者是java程序
对于每个被装载的类型,jvm都会创建一个java.lang.Class文件,classloader本身和class类都将在堆区分配空间,而装载的信息将进入方法区
装载(查找并load)-> 连接(执行验证、准备并解析) -> 初始化(设为正确的值)
在java1.1时,是使用bootstrap来查找定位class文件的,在1.2后,bootstrap只负责载入系统类,而搜索classpath的任务交给了一个开机启动的自定义类加载器----系统类装载器
classloader中有几个关键方法:
defineClass()通过一块二进制片段+类名来进行类定义导入
findSystemClass()使用系统类加载器来载入指定class文件
resolveClass()完成类的连接操作
classloader会带来命名空间,被不通classloader转载的类处于不同的空间,所以最准确的class定位还需要加上装载器标识
1.2.4 方法区
classloader在读取class文件到vm中后,vm提取类型信息,并输入到方法区中,为了能更快的得到方法区中信息,jvm可以通过在方法区中添加冗余数据来加快访问速度
作为共享使用的方法区,其数据访问必须被设计为是线程安全的(只应该有一个线程去装载)
方法区大小不是固定的,可以被垃圾回收
方法区存储的基本类型信息包括:
1.类型的全限名(class中的全限名的.被替换为/)
2.直接超类的全限名
3.是类还是接口
4.访问修饰符
5.超接口的全限名的有序列表
此外还还保存:类型的常量池,字段信息,方法信息,静态变量,ClassLoader引用,Class引用
1)常量池:包括直接常量和对其他类型、字段和方法符号的引用(每个类一个可以通过索引访问)
2)字段信息:(字段名、字段类型、修饰符),每个字段都保存,并且维护字段顺序
3)方法信息:类型中的每个方法一个,并维护方法申明顺序
方法名、返回类型、参数数量和类型(+申明顺序)、修饰符
如果不是抽象或本地方法那还有:
方法的字节码、操作数栈和该方法栈中局部变量的大小、异常表
4)类型变量:作为类型申明的一部分而保存
5)ClassLoader引用:vm会在进行动态连接时是用这个引用来找到并使用用户自定义Classloader
6)Class引用:通过这个引用,可以访问位于方法区内的和类型有关的信息(在动态连接时可能会利用此信息来确定分配大小)
7)方法表:为了加快访问速度,可选的,可以为每个非抽象类生成一个vtable
1.2.5 堆
java在运行时创建的所有类实例或数组都放在一个共享的堆中
jvm有在堆中分配空间的指令,但没有释放空间的指令
垃圾回收:主要是自动回收不在被程序引用的对象所占用的内存,并且也有可能会移动正在使用的对象,从而减少碎片
堆不是固定不变的,垃圾回收要求动态调整堆,在堆上也有可能会承载类型信息,也就是说垃圾回收也可能回收堆上的不使用的类型信息。
堆一般有两种思路:1)通过堆中的句柄再指向对象 2)直接指向对象
和世上其他的“两种”一样,合理结合分情况使用才是“最佳”思路
堆中的数据可能在类型转换和instanceof操作符时需要到方法区查找使用类型信息
如果有方法表,则方法表每一项是一个指向“实列方法数据的指针”(包括:操作数数栈、局部变量区大小、字节码、异常表)
此外堆中的每个对象都还有一个对象锁引用,以用来协调对象的共享访问(很多实现在使用到锁的时候才分配锁引用)
堆中对象还有等待集合的数据,用来实现wait、notify、notifyAll
最后,对象还有些数据来标记垃圾回收的知识
数组也是对象,所有具有相同纬度和类型的数组都是同一个类的实列,数组类由“[+类型名称”组成
1.2.6 程序计数器
当线程启动时,vm会分配一个java栈,vm只会对栈进行压栈和出栈操作
当线程开始调用一个方式时,会压入一个新帧,成为当前帧;而当方法返回时(return或异常),则会弹出一个帧并释放之
栈上的数据都是此线程私有的
java栈和帧在内存中可以是不连续的
1.2.7 栈帧
三部分组成:局部变量区、操作数栈和帧数据区
当vm调用一个方法时,会询问类的类型信息,并获取局部变量区和操作数栈的大小
1)局部变量(包括方法参数和局部变量):
java栈帧的局部变量区被组织为以字长为单位,从0开始计数的数组
对于java方法参数,vm会严格按照申明顺序,将参数放入局部变量区,而对于方法中局部变量,可以任意防止,甚至一个索引对应多个变量
note:byte、short和char可以被vm直接引用,但在放入局部变量和操作数栈时会被转为int
2) 操作数栈
被组织为一个以字长为单位的数组,只通过压栈和出栈操作来访问
jvm的指令的操作数不是从寄存器而是从操作栈中取得
操作栈就是jvm的工作区,指令都从这弹出数据,执行运算,然后把结果压回操作数栈
当vm需要常量池数据指令时,他会通过帧数据区中的常量池指针来访问
vm在return后会恢复栈帧,包括pc计数器指向下一个
如果异常退出,帧数据区还得保存一个对异常表的引用
可能还有其他数据:如调试数据
stack的实现方式:
a. 单独分配
b. 重叠分配
c. 两者结合
1.2.8 本地方法栈
1.2.9 执行引擎(core)
运行的java程序的每个线程都是一个独立的虚拟机执行引擎实列。从开始到结束要么执行字节码要么执行本地方法。
指令集:方法字节流是由jvm的指令序列构成。每个操作码后跟0个或多个操作数,可能使用常量池、局部变量或帧操作栈顶端的值
如何决定下一步操作:
1) 一般情况就是之后跟随的下一句语句
2)goto 或者return 时,引擎决定下一个操作码时把他当作当前指令的一部分
3) athrow指令,就明确的抛出一个异常
jvm指令有各种助记符,并使用汇编语言的风格。
使用一栈为中心的方法,可以让vm适应没有寄存器 的环境。
并且可以便于以栈为结构向连接器或优化器传递数据,便于优化,可以和即时编译和动态连接结合
class中的字节码,除了跳转指令,其他指令都是按照字节对其的,这种相对紧凑的组织方式,可以提高一定的运行效率,并且易于检查
为了便于检查,指令中的的一部分操作码指明了需要操作的类型(istore、fstore),检查操作可以用预验证和执行前验证的方式,检查操作了正确类型的数据
执行技术:解释、即时编译、自适应优化、芯片直接执行等
自适应:对经常使用且要求效率的部分使用编译优化(比如常用代码的inline)。
线程:可以在多种体系结构上实现并行处理。
vm线程需要支持:对象锁定和线程等待通知 机制
vm线程的行为是通过 变量、内存和工作内存-----来定义的
如果某个访问没有被同步,vm可以使用任何顺序来更新主存(可见性问题)
1.2.10 本地方法接口
JNI为的设计考虑了可移植性