0.学习准备
- 基础落下了很多了,是时候啃一波JVM了
感觉很深奥,就先从视频看起,以后再看书补充啦 - 参考资料
参考书籍《深入理解Java虚拟机》
参考视频《深入理解JVM》(主要学习) - JVM原理
- JVM的概念
- JVM的发展史
- JVM的种类
- Java语言规范
- JVM规范
- JVM运行机制
- JVM启动流程
- JVM基本结构
- 内存模型
- 编译和解释运行的概念
1.JVM的概念
1)虚拟机简介:
- JVM:
Java Virtual Machine的简称。意为Java虚拟机。 - 虚拟机:
指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。 - 常见的虚拟机:
- JVM
- VMwave
- Virtual Box
2)JVM和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集
物理系统中会有很多的寄存器 - JVM则是通过软件模拟Java字节码的指令集
JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪 - JVM是一台被定制过的现实当中不存在的计算机。
2.JVM和Java的发展史
1)20世纪Java发展:
- 1996年SUN JDK 1.0时发布:Classic VM
- 纯解释运行,使用外挂进行JIT(编译器)
- 1997年JDK 1.1发布:
- AWT、内部类、JDBC、RMI、反射(Java的核心)
- RMI:远程方法调用(Remote Method Invocation)。能够让在某个java虚拟机上的对象像调用本地对象一样调用另一个java 虚拟机中的对象上的方法。
- 1998年JDK 1.2发布:Solary Exact VM(仅存在了很短的时间)
- JIT和解释器混合执行
- Accurate Memory Management 精确内存管理,数据类型敏感
- 提升GC性能
- JDK1.2开始成为Java2(J2SE,J2EE,J2ME出现)
并且加入了Swing Collection。
2)二零零几年:
- 2000年JDK1.3:Hotsport作为默认虚拟机发布
JDK1.3加入了JavaSound - 2002年JDK1.4:Classic VM退出历史舞台
1.4更新内容:Assert,正则表达式,NIO,IPV6,日志API,加密类库,异常链,XML解析器等。 - 2004年JDK1.5即JDK5,Java5(很重要的一个版本)
Java5更新内容:泛型,注解,装箱,枚举,可变长参数,Foreach循环。
虚拟机层面:改进了Java内存模型(JMM),提供了JUC并发包。 - JDK1.6 java6:
更新内容:脚本编程的支持(动态语言支持),jdbc4.0,Java编译器API,微型Http服务器API等。
虚拟机层面:锁与同步,垃圾收集,类加载等算法的改动 - JDK1.6发布后由于一系列原因,JDK有好几年都没有大更新。JDK1.6一共有37个Update版本。
3)二零一几年:
- 2011年JDK1.7/Java7发布:
指定了十个里程碑但是始终无法完成,未完成的项目推到Java8(模块化则推到了Java9)。- G1收集器(Update4才正式发布)
- 加强对非Java语言的调用支持
- 升级类加载器架构
- 64位系统压缩指针
- NIO2.0
- 2014年Java8发布:
- Lamda表达式
- 语法增强
- Java类型注释等
- 2016年Java9发布:
- 模块化
4)Java与JVM发展历史中的大事件:
- 使用最广泛的JVM为HotSpot
- HotSpot最早为Longview Technologies开发,被SUN收购
- 2006年,Java开源,并建立OpenJDK
- HotSpot成为SUN JDK和OpenJDK中所带的虚拟机
- 2008年,Oracle收购BEA
- 得到JRockit VM
- 2010年Oracle收购SUN
- 得到HotSpot
- Orcale宣布在JDK8时整合HotSpot和JRockit VM,优势互补
- 在HotSpot的基础上移植JRockit的优秀特性
3.JVM的种类
- KVM:
- SUN发布
- IOS和Android兴起之前,广泛应用与手机系统
- CDC/CLDC HotSpot
- 手机、电子书、PDA等设备上的统一Java编程接口
- J2ME的重要组成部分
- 同样在IOS和Android兴起之后受到了很大的挑战
- JRockit:
- BEA
- IBM J9 VM:
- 仅在IBM内部使用
- Apache Harmony
- 兼容JDK1.5和1.6的Java程序运行平台,开源
- 与Orcale关系恶劣,退出JCP,Java社区分裂
- OpenJDK出现后受到挑战,于2011年退役
- 没有大规模的商用经历
- 对Android的发展又积极作用
4.Java及JVM规范
Java语言规范:
- 语法
- 变量
- 类型
- 文法
JVM规范:
- Class文件类型
- 运行时数据
- 帧栈
- 虚拟机的启动
- 虚拟机的指令集
1)Java语言规范:
- 语法:
- 词法结构:
- \u+4个16进制数字,表示UTF-16
- 行终结符:CR,or LF,or CR LF
- 空白符的定义:
空格 tab \t(换页) \f(行终结符) - 注释
- 标识符:就是变量名啊那种
不能是布尔型,关键字,Null - 关键字
- 词法结构中的数值的表示:允许使用下划线
- int:
如:0 2 0237 0xDada_cafe 0x00_FF__00_FF - long:
如: 0l 2L 0x1000000l 2_17_483_648L 0xC0B0L - float:
如:1e1f 2.f .3f 3.14f 6.66666e+23f - double
如:1e1f 2. .3 3.14 1e-9d 1e137 - 操作:
如:+= -= *= /= &= |= ^= %= < <= > >= …
- int:
- 类型和变量:
- 元类型:
byte,short,int,long,float,char - 变量初始值:
- boolean false
- char \u0000
- 泛型:
- 元类型:
- 元数据类型和对象(封装类型)的差异:
- 其他语言规范:
- Java内存模型
- 类加载链接的过程
- public final static abstract的定义
- 异常
- 数组的使用
- 。。。
- Java规范定义了什么是Java语言
JVM规范
2)JVM规范:
- Java语言和JVM是相对独立的:
运行在JVM上的语言还有:Groovy,Clojure,Scala
能在JVM上运行说明这些语言都是符合JVM规范的。 - JVM只要定义了二进制class文件和JVM指令集等。
- JVM规范主要定义的内容:
- Class文件格式
- 数字的内部表示和存储:(Java中没有无符号数)
如:Byte -128 to 127(-2^7到2^7-1) - ReturnAddress数据类型定义:
指向操作码的指针。不对应Java的数据类型,不能在运行时修改。Finally实现需要。(在Java中找不到该数据类型) - 定义PC
- 堆
- 栈
- 方法区
- JVM当中整数的表达:
- 原码:第一位符号位(0为正数,1为负数)
- 反码:符号位不动,原码取反
- 负数补码:符号位不动,反码加一
- 正数补码:与原码相同
- 打印出整数在二进制当中的表示:
0x800000000代表最高位为1的数字。int a=-6;想要打印为二进制的整数 for(int i=0;i<32;i++){ //整数32位,每次取一位 int t=(a&0x800000000>>>i)>>>(31-i); System.out.print(t); }
- 为什么要使用补码:
- 0的表示:不管算正算负补码都是一样的(无歧义表示0)
- 补码能很好的参与计算机之间的计算:
- 0的表示:不管算正算负补码都是一样的(无歧义表示0)
- Float的表示与定义:支持IEEE754
s eeeeeeee (x)mmmmmmmmmmmmmmmmmmmmmmm
s:符号位;e:8位指数位;m:23尾数位;x:附加位(不写出,由JVM给出)- 尾数附加位的值由指数位确定:
- 指数位e全为0,则尾数附加位为0,否则尾数附加位为1
- 最后的数值:e要进行位移
s*m*2^(e-127)
- 如-5的表示:
- 一些特殊的方法:
<init>
:默认的构造函数(无参)<clinit>
:类的初始化方法
- JVM指令集:
- 类型转换:l2i(长整型转整型)
- 出栈入栈操作:(JVM中没有寄存器,数据传递依靠栈,所以又很多栈的指令)
aload
,astore
- 运算:
iadd
,isub
等 - 流程控制:
ifeq
相等,ifne
不相等 - 函数调用:
invokevirtual
、invokeinterface
、invokespecial
、invokestatic
等
- JVM对Java Library提供以下支持(Java无法实现):
- 反射 java.lang.reflect
- 类加载器 ClassLoader
- 初始化class和interface
- 安全相关 java.security
- 多线程
- 弱引用
JVM的编译与反编译:
- 源码到JVM指令的对应格式
- javap
- JVM反汇编格式:
1
2<index> <opcode> [<operand1>[<operand2>...]] [comment]
操作码 操作 操作数 注解
反编译的例子:一个for循环的执行过程
5.JVM的启动流程
简单流程图:
- 第一步:启动JVM——Java或者Javaw命令后面跟一个启动类
JAVA XXX,启动类中一般都有一个main方法。 - 第二步:装载配置
会根据当前路径和系统版本寻找jvm.conf文件,找到文件之后会去定位所需要的dll文件。 - 第三步:根据配置寻找JVM.dll
需要寻找的是匹配当前系统版本的dll
JVM.dll为JVM的主要实现
使用JVM.dll初始化虚拟机,获得相关的一系列native调用接口。 - 第四步:初始化JVM,获得JNIEnv接口。
JNIEnv为JVM接口,findClass等操作通过它实现。 - 第五步:找到Main方法并允许
6.JVM基本结构简介
简单示意图:
- 类加载器子系统:ClassLoader
将Class文件加载到JVMz中 - PC寄存器:又称为程序计数器
虚拟机运行时需要有指针指向下一条指令的地址。 - 执行引擎:执行类的字节码
- 垃圾收集器:很重要的一个模块
- 本地方法栈暂时不过多介绍。
pc寄存器,方法区与Java堆
1)pc寄存器
- 又称为程序计数器
- 每个线程都有一个PC寄存器
- 在线程创建时创建
- 总是指向下一条指令的地址
- 执行本地方法时,PC寄存器的值为空(undefined)
2)方法区:
- 保存装载的类的信息:
- 常量(除字符串常量)
- 静态变量
- 字段方法信息
- 编译后的字节码
- 有一个别名叫Non-Heap(非堆)
- JDK6时String常量信息置于方法,JDK7时已经移动到了堆。
- 方法区经常被称为”永久区”,仅仅是因为涉及团队将GC分代收集扩展至方法区(使用永久代实现的方法区),保存一些相对稳定的信息。
- 所有线程共享的内存区域,由虚拟机维护。
3)Java堆:
- 和程序开发最为密切的一个区间
- 简介:
- 和程序开发密切相关
- 应用系统对象都保存在Java堆中
- 所有线程共享的Java堆
- 对分代GC来说,堆也是分代的
- GC的主要工作区间
- 不同的GC算法也对应着不同的堆
7.Java栈
1)Java栈,局部变量表与帧
- Java栈:又叫做Java虚拟机栈
- 线程私有
- 栈由一系列帧组成,因此栈也叫帧栈
- 帧保存一个方法的局部变量,操作数栈,动态链接,操作出口,常量池指针等
- 每一次方法的调用创建一个帧,并且压栈。
- 先进后出的数据结构
- 局部变量表:包括了函数的参数以及局部变量
如有两个方法如下:(一个静态方法,一个实例方法)
则这俩方法的局部变量表如下: - 上面的局部变量表的说明:
- 分为很多个槽,一个槽占32位。
- long,double类型占两个槽,对象类型则会变成引用(reference)。
- 实例方法的第一个槽是对自身的引用。
- 函数的调用组成帧栈:
如一个递归方法:
则该方法的帧栈结构如下:public static int runStatic(int i,long l,float f,Object o,byte b){ return runStatic(i,l,f,b); }
每一次调用就会往上面压一个栈,直到栈溢出。
2)操作数栈:
- Java中没有寄存器,所有的参数传递依靠操作数栈。
- 简单的例子:
- 简单过程示意图:
3)栈上分配:
- 堆上分配:(C++代码)
new一个对象后需要收动删除。(JVM则有GC回收)
分配了内存空间但是没有进行回收:内存泄露
内存泄露很难察觉,难以全面修复。 - 栈上分配:不需要手动删除
这时是一个对象引用,这种情况的到的对象是分配在栈上的,调用完成后自动清理。
栈上分配不用担心内存泄露的问题。 - 有一段分配代码:
分别使用栈上分配与堆上分配运行:
栈上分配只用了5ms,堆上分配会触发垃圾回收,时间也多达564ms。
(但是很明显不是所有对象都能分配到栈上的,否则还要GC啥用) - 栈上分配也不是完美的:
- 小对象(一般几十byte),没有逃逸的情况下可以分配到栈上。
- 直接分配到栈上可以自动回收,减轻GC压力
- 大对象,或者逃逸对象无法栈上分配。
(逃逸:对象在这个线程要用,在其他线程也要使用。栈是线程私有,不能分配到栈上)
4)堆,栈,方法区交互:
- 所运行的代码:
- 交互示意图:
- 简单来说就是类的信息放在了方法区,运行后实例对象放在了堆中,对对象的引用放在栈中。(每个线程会单独分配一个栈空间)
8.Java内存模型
1)内存模型简介:
- 示意图:
- 内存模型:
- 每一个线程有一个和主存独立的工作内存。
- 工作内存存放的是主存中的变量值的拷贝。
- 内存运行过程:
- 原子操作不能被中断,但是原子操作之间可以被中断。
- 简单来说就是主存执行读写,而工作内存复负责一些和线程交互的工作。
- 多线程下的内存模型:
2)Volatile:
- 详细的用法可以参考前面的学习笔记:
《Java多线程编程核心技术》笔记(二) - Volatile的性能一般会比锁好(重量级锁),但是不能代替锁。
是否选择Volatile的条件:语义是否满足应用。 - 需要使用-server运行才有效果。
9.内存模型的相关概念:
主要概念有:
- 可见性
- 保证可见性方法
- 有序性
- 指令重排序
- 保证线程有序性
- 重排基本原则
1)可见性与有序性:
- 可见性:
- 一个线程修改了变量,其他线程可以立刻知道。
- 保证数据可见性的方式:
- Volatile
- Synchronized(上锁时是写回主存的)
- final(一旦初始化完成,其他线程可见)
- 有序性:
- 在线程内,所有的操作都是有序的。
- 在线程外观察,操作都是无序的( 指令重排 或 主内存同步延时 )
2)指令重排:
程序运行时编译系统会尽可能让性能更加好,就有可能对一些代码指令进行重排序。
- 示意过程:
- 保证线程内的语义串行,线程间则无所谓。
- 视频中的例子:
a=1和flag=true没有语义冲突,所以本身就是无序的,这两者谁先执行都没有关系。 - 同步:保证线程之间的有序性
3)指令重排原则:(先行性原则)
- 程序顺序原则:一个线程内保证语义的串行性。
- Volatile规则:volatile变量的读一定要在写前面。
- 锁规则:解锁必然发生在后面的加锁前。
- 传递性:A先于B,B先于C,那么A必然先于C。
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终极(join)
- 线程的中断先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
10.字节码运行的两种方式:
- 解释执行:
- 解释执行以解释的方式运行字节码
- 读一句执行一句
- 编译运行:(JIT)
- (运行时)将字节码编译成机器码
- 直接执行机器码
- 运行时编译
- 编译后性能很大提升
(保守估计编译执行性能比解释执行快十倍以上)
最后更新: 2018年05月18日 18:30
原始链接: https://zjxkenshine.github.io/2018/04/28/JVM学习笔记(一):初识JVM/