Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。
Java 程序从源代码到运行一般有下面3步:
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
总结:
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
以下转载:谈谈对jvm的理解
一、JVM介绍:
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
二、JVM作用
与平台无关性:JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中JVM对中央处理器(CPU)所执行的一种软件操作,用于执行编译过的Java程序码(Applet与应用程序)。
JVM就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。当然只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。
三、JVM重要特征
1.内存管理机制
Java虚拟机内存模型包括程序计数器、虚拟机栈、本地方法栈、方法区、堆,如图所示
Java虚拟机运行时内存模型
(1)程序计数器
程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码行号指示器。需要注意以下几点内容:
1)程序计数器是线程私有,各线程之间互不影响
2)如果正在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址
3)如果执行native方法,这个计数器为null
4)程序计数器也是在Java虚拟机规范中唯一没有规定任何OutOfMemoryError异常情况的区域
(2)虚拟机栈
虚拟机栈即我们平时经常说的栈内存,也是线程私有,是Java方法执行时的内存模型,每个方法在执行时都会创建一个栈帧用于储存以下内容:
1)局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型。
2)操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
3)动态连接:每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接。
4)方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
(3)本地方法栈
本地方法栈是线程私有,与虚拟机栈类似,为native方法服务。
(4)方法区
线程共享,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent Generation),主要存放java类定义信息,与垃圾回收关系不大,但不是没有垃圾回收,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。运行时常量池,方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池。
(5)堆
堆是JVM中最大的一块区域,线程共享,此区唯一的目的就是存放对象实例,几乎所有对象实例都在这里分配
1)新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1
2)老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2、垃圾回收机制
说起GC,大部分人都会把这项技术当作Java语言的产物,其实GC的历史比Java久远。GC中不外乎两个步骤:1.确定哪些是垃圾,2.进行垃圾回收
(1)对象已死的判定
如何确定一个对象是否“死亡”?目前有两种方式:
1)引用计数算法:给对象添加一个计数器,每当有一个地方引用它时,计数器加1;当引用失效,计数器减1;计数器为0的对象就是不可能再被使用的。目前在微软的COM技术、Python语言都广泛使用该算法进行内存管理,但是至少主流的Java虚拟机没有选择该算法来管理内存对象,其中最主要原因是它无法解决对象之间的相互循环引用问题。
2)可达性分析算法:基本思想就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的。
(2)垃圾回收算法
1)标记-清除算法:首先标记出所有需要回收的对象,然后进行统一的回收,不足之处有两个:效率低、碎片多。
2)复制算法:将可用内存划分成大小相等的两块,每次只使用一块,当一块用完了,就将还存活的对象复制到另外一块上,然后把已使用的内存空间清理掉。不足之处是将内存缩小到一半,利用率不高。
3)标记-整理算法:与标记-清除类似,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的区域
4)分代收集算法:分代收集是目前jvm普遍采用的算法,即新生代采用复制算法,因为有大量新生对象死去,只有少量存活;老年代采用标记-整理,因为老年代中对象存活率高,没有额外的空间对它进行担保。
(3)垃圾回收器
如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的实现。Java虚拟机规范中并没有对垃圾收集器应该如何实现作相应规定,因此不同厂商、不同版本差异很大。在JDK1.7以后开始采用G1。
3 类加载机制
(1)Class文件结构
商业和开源机构已经在Java语言之外发展出一大批在Java虚拟机上运行的语言,如Groovy、JRuby、Scala等。
实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和任何语言绑定,它只与Class文件的二进制文件格式相关联,理论上讲,任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。
Class文件结构包括以下内容:
1)魔数:确定这个文件能否被Java虚拟机接受,值为0xCAFFBABE(咖啡宝贝?)
2)版本号:Class文件的版本号
3)常量池:Class文件的资源仓库
4)访问标志:用于识别一些类或者接口层次的访问信息,是类还是接口?是否为public?
5)索引集合:包括类索引、父类索引、接口索引
6)字段表集合:描述接口或类中声明的变量,但不包括方法内部的局部变量
7)方法表集合:代码在方法表中的属性集合“Code”属性
8)属性表集合:字段表、方法表都可以携带自己的属性表,以用于描述某些场景专用信息
(2)类加载过程
加载
加载阶段虚拟机需要完成以下3件事:
- 通过一个类的全限定名来获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配,注意是类变量(static修饰),不是实例变量。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
初始化
初始化类和其他资源
(3)类加载器
类加载器用于实现类的加载动作,对于任意一个类,都需要由加载它的类加载器和这个类本省一同确立其在Java虚拟机中的唯一性,每个类加载器都有一个独立的类名称空间,比较两个类是否相等,只有在这两个类是同一个类加载器加载的前提下才有意义。例如Class对象的equals()、isInstance()。
从Java开发人员的角度看,类加载器可划分为3种:
1)启动类加载器:负责加载存放在<JAVA_HOME>\lib下面的类库
2)扩展类加载器:负责加载存放在<JAVA_HOME>\lib\ext下面的类库
3)应用程序类加载器:负责加载用户路径上的类库
双亲委派模型:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去加载,每一层次的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
4 性能监控调优
jvm启动参数
参数名称 | 说明 |
-Xms | 初始堆大小,物理内存的1/64(<1GB) |
-Xmx | 最大堆大小,物理内存的1/4(<1GB) |
-Xmn | 年轻代大小,此处的大小是(eden+ 2 survivor space) |
-XX:PermSize | 设置持久代初始值,物理内存的1/64 |
-XX:MaxPermSize | 设置持久代最大值 物理内存的1/4 |
-Xss | 每个线程的堆栈大小,JDK1.5以后为1M |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值 |
性能优化四个命令:
1)jps:查看java进程
2)jstat:显示本地或者远程虚拟机垃圾回收,例如:jstat -gcutil $pid 1000 5
3)jmap:查看JVM堆中对象详细占用情况,例如:jmap -histo [pid]
4)jstack:用于生成虚拟机当前线程快照,jstack -l [pid]