学习JVM虚拟机原理总结
0x00:JAVA虚拟机的前世今生
1991年,在Sun公司工作期间,詹姆斯·高斯林和一群技术人员创建了一个名为Oak的项目,旨在开发运行于虚拟机的编程语言,允许程序多平台上运行。后来,这项工作就演变为Java。随着互联网的普及,尤其是网景开发的网页浏览器的面世,Java[1] 成为全球流行的开发语言。因此被人称作Java之父。 1996年1月Sun公司发布了Java的第一个开发工具包(JDK 1.0),2009 年Oracle收购sun后到今天己经到了JDK9。
java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(linux下为:libjvm.so),这个动态连接库才是java虚拟机。
JVM在哪里?
在windows平台上虚拟机的位置在:
%JAVA_HOME%\jre\bin\client\jvm.dll
%JAVA_HOME%\jre\bin\server\jvm.dll
0x01:JAVA虚拟机跨平台
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行, 来一张从java原码编译到执行的大致流程图吧。如下图
0x02:class文件格式分析
当我们写好的.java 源代码,最后会被Javac编译器编译成后缀为.class的文件,该类型的文件是由字节组成的文件,又叫字节码文件。流程图如下所示:
那么,class字节码文件里面到底是有什么呢?它又是怎样组织的呢?让我们先来大概了解一下他的组成结构吧。
1. 魔数(magic):所有的由Java编译器编译而成的class文件的前4个字节都是“0xCAFEBABE” 它的作用在于:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为可以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如果是,则JVM会认为可以将此文件当作class文件来加载并使用。
2.版本号(minor_version,major_version): JVM在加载class文件的时候,会读取出主版本号,然后比较这个class文件的主版本号和JVM本身的版本号,如果JVM本身的版本号 < class文件的版本号,JVM会认为加载不了这个class文件,会抛出我们经常见到的"java.lang.UnsupportedClassVersionError: Bad version number in .class file " Error 错误;反之,JVM会认为可以加载此class文件,继续加载此class文件。
3. 类中的method方法的实现代码---即机器码指令存放到这里,JVM执行时会根据结构定位到具体的方法指令。
4.class文件结构文件在硬盘上表现如下:
上图是用16进制工具打开时就可以分析具体的字节码对应在结构体中的项,JVM在执行时也是通过解析这些字节码。
0x03:JAVA虚拟机引擎工作原理
前面我们分析完class文件结,知道里面存放有方法Java字节码,执行由JVM执行引擎来完成,大致流程图如下所示:
从源到到执行大致流程图如下所示:
一个简单的java程序执行示例:
JVM概念上工作原理图:
第一代引擎简单实现:
Java虚拟机引擎和真实的计算机一样,运行的都是二进制的机器码;CPU指令运行流程都是通过->取指->译码->执行,这三个基本过程完成一个程序的执行 。下面将通过一个非常简单的例子,带你感受一下Java虚拟机运行机器码的过程和其工作的基本原理。假如有如下java代码,计算两个整数之和:
int run(int a, int b){
return a+b;
}
通过编译后生成的字节码为0x88(代表加操作);那么我们要模拟java虚拟机来执行这指令时将是如下这样(jvm主要由C、C++、汇编来开发,这里我用C来演示):
#include(stdio.h)
int jvm(int a, int b, int opcode);
int main(){
int a = 5;
int b = 3;
int code = 0x88;
int result = jvm(a, b, code);
printf(“result = %d\n”,result);
return;
}
int jvm(int a, int b, int opcode){
if(0x88 == opcode)
return a+b;
}
这个示例程序中的jvm函数可以看成是执行引擎了,这是世界上最精简的小巧的虚拟机执行引擎啦,执行引擎接到操作数和指令编码,然后判断指令是否为加操作,如果是就便对入栈的两个操作数执行加法运算并返回结果。第一代jvm引擎就是这么简单,然而这种方式却有一个比较大的缺陷->运行时效率低下(编译器会产生一些不必要的代码)。所以第一代java虚拟机常被吐槽。
可移植性:返种方式容易用高级语言实现,所以容易达到高可移植性 。
第二代引擎简单实现:
第二代通过汇编精确控制 (不是直接用汇编写的 ,通过内联机器码 ),其实就是使用机器码模板方式先写好一些功能代码对应与class字节码对应起来,称为模板解释器
上面我们用0x88字节码在jvm中表示加法,如果要用汇编语言编写实现两个数相加时如下所示:
Push %ebp
Mov %esp, %ebp
Mov 0xc(%ebp), %eax //将操作1数从栈中取出放到eax寄存器
Mov 0x8(%ebp),%edx //将操作2数从栈中取出放到edx寄存器
Add %edx,%eax //将两个数相加后存放在eax中
Pop %ebp
Ret
其它指令也有类似的功能代码。
第三代引擎介绍:
虽然将字节码对应着编写好的汇编模板直接运行其效率相比使用C语言解释执行,它已经提高了很多,但是一个字节码须机对应很多的汇编代码,指令数量增多执行时间成本必然增加,因此运行效率仍然不够高。
为了能够进一步提升性能,jvm在运行时将字节码直接编译为对应平台本地机器指令 ,也就是JIT编译技术,”及时编译”,(比如安卓5.0以上的ART模式就使用了AOT技术,安装app时直接将字节码编译成对应平台本地机器指令,运行速度就要快很多)。
动态编译具有一些缺点,大量的初始编译可能直接影响应用程序的启动时间。目前jvm主要是模板解释与编译混合执行。 通过java –version命令可以查看:
对只执行一次的代码做JIT编译再执行,可以说是得不偿失。编译过程慢,须要词法分析,语法分析,生成语树,生成本地机器码。对只执行少量次数的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。只有对频繁执行的代码,JIT编译才能保证有正面的收益。
况且,并不是说JIT编译了的代码就一定会比解释执行快。切不可盲目认为有了JIT就可以鄙视解释器了,还是得看实现细节如何。
缺点
但是,动态编译确实具有一些缺点,这些缺点使它在某些情况下算不上一个理想的解决方案。例如,因为识别频繁执行的方法以及编译这些方法需要时间,所以应用程序通常要经历一个准备过程,在这个过程中性能无法达到其最高值。在这个准备过程中出现性能问题有几个原因。首先,大量的初始编译可能直接影响应用程序的启动时间。
JIT编译技术,”及时编译”流程图如下:
0x04:JAVA关键字volatile分析
Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步。
下面来看一个示例:
JVM 可以-client与-Server模式来运行一个程序,区别:当虚拟机运行在-client模式的时候程序启动快, -Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升, -Server模式要比-client性能快10%左右,原因是模式不同代码优化级别不同。 -Server一般在服务器使用。我们看看同一个代码在不同模式下运行情况:
上述代码在-client模式下运行正常结束,在-server模式运行上述代码,永远不会停止 。
使用jps命令可以查看到进程一直存在,内存一直在上升。
程序比较简单,在主线程中启动一个线程,这个线程不停的对局部变量做自增操作,主线程休眠 1 秒中后改变启动线程的循环控制变量,想让它停止循环。这个程序在 client 模式下是能停止线程做自增操作的,但是在 server 模式先将是无限循环。若是改成private volatile boolean stop;
我们通过HSDIS反汇编插件将JVM动态生成的本地代码还原为汇编代码输出,这样我们就可以通过输出的代码来分析问题。
HSDIS是由Project Kenai提供并得到Sun官方推荐的HotSpot VM JIT编译代码的反汇编插件,作用是让HotSpot的-XX:+PrintAssembly指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码来分析问题。读者可以根据自己的操作系统和CPU类型从Kenai的网站上下载编译好的插件,直接放到JDK_HOME/jre/bin/client和JDK_HOME/jre/bin/server目录中即可。如果没有找到所需操作系统(譬如Windows的就没有)的成品,那就得自己拿源码编译一下,或者去HLLVM圈子中下载也可以,这里也有 win32 和 win64 编译好的。
-server
-Xcomp
-XX:+UnlockDiagnosticVMOptions
-XX:CompileCommand=dontinline,*VisibilityTest.run
-XX:CompileCommand=compileonly,*VisibilityTest.run
-XX:+PrintAssembly
其中
-Xcomp 参数-Xcomp是让虚拟机以编译模式执行代码,这样代码可以偷懒,不需要执行足够次数来预热都能触发JIT编译。
-XX:CompileCommand=dontinline,*VisibilityTest.run 这个表示不要把 run 方法给内联了,这是解决内联问题。
-XX:CompileCommand=compileonly,*VisibilityTest.run 这个表示只编译 run 方法,这样的话只会输出sum方法的ASM码。
-XX:+UnlockDiagnosticVMOptions 这个参数是和 -XX:+PrintAssembly 一起才能生效答应汇编代码
看完上面汇编代码发现Server模式代码没有去取stop值一直执行jmp了,Client模式下去取Stop值并做判断了,所以才能正常结束。
解决办法就是在变量前加volatile关键字,这样在jit 编译时就不用代码优化了,能正常结束。
0x05:JVM虚拟机GC算法
JVM GC回收哪个区域内的垃圾?
垃圾收集 Garbage Collection 通常被称为“GC”,并非是java独有,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。
jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这两部分内存的分配和使用都是动态的。
JVM GC怎么判断垃圾可以被回收了?
· 对象没有了引用
· 发生异常
· 程序在作用域正常执行完毕
· 程序执行了System.exit()
· 程序发生意外终止(被杀线程等)
JVM GC 算法:
.引用计数器算法
.根搜索算法
.复制算法
.标记 - 清除算法
.标记 - 整理算法
.分代收集算法
下面分析算法原理:
引用计数法:
当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器变量,同时引用计数器变量为1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!当我们的代码出现下面的情形时,该算法将无法适应。
这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。
根搜索算法:
如果把所有的引用关系看作一棵树,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
标记 - 清除算法:
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象, B清楚后就空出来了,导致内存不连续,因此会造成内存碎片!
内存碎片:导致内存不连续,即使空闲的内存足够多,也不一定能分配出足够大小的内存,同时还会降低内存分配的效率。
复制算法:
复制算法将内存划分为两个区间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(备用区间)则是空闲的。
当扫描完毕活动区间后,并将存活对象复制到一块新的,没有使用过的空间中(备用空间),此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。
复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服50%内存的浪费。
标记 - 整理算法:
标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记与清除,但在回收对象占用的空间后,会将所有存活的对象移动,并更新对应的指针。标记-整理 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
分代收集算法:
JVM执行GC时须机停止除GC所需的线程外所有线程的执行(stop-the-world ),(这就好比当有人打扫卫生时我们须要停下来不要随意走动产生垃圾),JVM为了减少GC做了很多优化,所以就有了分代收集算法,分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
新生代(Young generation)->老年代(Old generation)->持久代(Permanent generation)
对象在刚刚被创建之后,是保存在伊甸园空间的(Eden)。那些新生代Gc后存活的对象会存放幸存者空间(Survivor),幸存者空间Gc 15次后还活着的转存到老年代空间(Old generation),持久代用于保存类常量以及字符串常量,这个区域不是用于存储那些从老年代存活下来的对象,这个区域的对象与程序同生共死。
0x06:总结
我不认为为了使用好Java必须去了解Java底层的实现。许多没有深入理解JVM的开发者也开发出了很多非常好的应用,但是,如果你要定制与优化JVM(比如阿里淘宝jvm深度定制解决高并发)就须要深入了解JVM原理了。
官方JVM是一个通用产品,一大目标是尽可能的兼容各个平台和满足大部分应用场景的需求。由于开发和维护资源有限,对于特定平台和应用场景而言,官方JVM在性能和功能上,都有取舍。比如所使用的平台是统一的x86平台,应用也有自己的场景特点。针对平台和应用场景的极致的优化,可以做得更多。
除了我以上提到的技术,JVM还是用了其他的很多特性和技术。由于时间和水平有限,我没有对它们进行讲解。如果你对JVM感兴趣并想要深入学习JVM话,可以去阅读它的源代码。(源代码主要由机器码、C/C++语言组成,源码大概50多万行)
源码下载地址: http://download.java.net/openjdk/jdk8/
以上是来是网络的学习资料总结。
欢迎关注公众号:
学习JVM虚拟机原理总结的更多相关文章
- jvm虚拟机原理1
JVM是虚拟机,也是一种规范,他遵循着冯·诺依曼体系结构的设计原理.冯·诺依曼体系结构中,指出计算机处理的数据和指令都是二进制数,采用存储程序方式不加区分的存储在同一个存储器里,并且顺序执行,指令由操 ...
- java内功 ---- jvm虚拟机原理总结,侧重于虚拟机类加载执行系统
参考书籍:<深入理解java虚拟机>,三天时间用了八个小时看完,像读一本武侠小说,挺爽. 另外需声明:图片都是从我自己的csdn博客转载,所以虽然有csdn标识,但都是我自己画的图片. j ...
- java内功 ---- jvm虚拟机原理总结,侧重于GC
写作日期 2016-08-22-23 交流qq:992591601 参考资料:<深入理解java虚拟机>.<thinking in java>.<Effective Ja ...
- java 基础知识学习 JVM虚拟机参数配置
1) 设置-Xms.-Xmx相等: 2) 设置NewSize.MaxNewSize相等: 3) 设置Heap size, PermGen space: Tomcat 的配置示例:修改%TOMCAT_H ...
- 五分钟学Java:如何学习Java面试必考的JVM虚拟机
原创声明 本文首发于微信公众号[程序员黄小斜] 本文作者:黄小斜 转载请务必在文章开头注明出处和作者. 本文思维导图 为什么要学习JVM虚拟机 最近的你有没有参加Java面试呢?你有没有发现,Java ...
- 《Java虚拟机原理图解》 1.2、class文件里的常量池
[最新更新:2014/11/11] 了解JVM虚拟机原理 是每个Java程序猿修炼的必经之路. 可是因为JVM虚拟机中有非常多的东西讲述的比較宽泛.在当前接触到的关于JVM虚拟机原理的教程或者博客中 ...
- 《Java虚拟机原理图解》1.4 class文件里的字段表集合--field字段在class文件里是如何组织的
0.前言 了解JVM虚拟机原理是每个Java程序猿修炼的必经之路.可是因为JVM虚拟机中有非常多的东西讲述的比較宽泛.在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描写 ...
- 《Java虚拟机原理图解》1.5、 class文件中的方法表集合--method方法在class文件中是怎样组织的
0. 前言 了解JVM虚拟机原理是每一个Java程序员修炼的必经之路.但是由于JVM虚拟机中有很多的东西讲述的比较宽泛,在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描 ...
- 《Java虚拟机原理图解》1.4 class文件中的字段表集合--field字段在class文件中是怎样组织的
0.前言 了解JVM虚拟机原理是每一个Java程序员修炼的必经之路.但是由于JVM虚拟机中有很多的东西讲述的比较宽泛,在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描述 ...
随机推荐
- flex 实例Demo
Flex 页面布局 很方便 快捷 <!DOCTYPE html> <html lang="en"> <head> <meta charse ...
- Spring学习笔记(一)—— Spring介绍及入门案例
一.Spring概述 1.1 Spring是什么 Spring是一个开源框架,是于2003年兴起的一个轻量级的Java开发框架, 由Rod Johnson 在其著作<Expert one on ...
- python学习之路---day008
文件操作一:文件操作01):文件读取:(r 只读) 001):我们先在当前文件夹内创建txt文件取名为123,在其中添加几句话内容. f 称之为文件句柄,控制着 123 这个文本文档 f=open(& ...
- Hibernate Restrictions的运算符
HQL运算符 QBC运算符 含义 = Restrictions.eq() 等于equal <> Restrictions.ne() 不等于not equal > Restrictio ...
- CDQZ Day1
#include<cassert> #include<cstdio> #include<vector> using namespace std; ,maxt=,ma ...
- LightOJ - 1197 素数筛
深夜无事可干啊 #include<bits/stdc++.h> using namespace std; const int maxn = 1e6+11; typedef long lon ...
- mac 系统配置(一)
1.终端颜色配置 文件 .bash_profile下添加环境变量如下: export CLICOLOR=1 export LSCOLORS=gxfxaxdxcxegedabagacad 环境变量生效: ...
- PIE SDK过滤控制
1. 功能简介 栅格数据前置过滤是在渲染之前对内存中的数据根据特定的规则进行处理,然后再进行数据渲染.本示例以定标为例进行示例代码编写. 定标(校准)是将遥感器所得的测量值变换为绝对亮度或变换为与地 ...
- mysql PXC集群方案总结
同时写集群内的所有机器 写性能依赖最慢的那个机器 读性能提高X倍
- html自定义垂直导航菜单
html自定义垂直导航菜单(目前只支持上级+下级两级菜单) 由于工作的需要,昨天花了三个多小时的事件整理了一份关于垂直导航二级菜单,可以通过js配置的方式初始化菜单box(测试环境:chrome 49 ...