JVM原理与调优
一.JVM是什么
JVM是Java Virtual Machine的缩写,JVM是一种用于计算机设备的规范,它是一种虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个重要特点就是平台的无关性,而使用Java虚拟机就是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java虚拟机后,Java语言在不同平台上运行时就不需要重新编译了,Java语言使用Java虚拟机上屏蔽了与具体平台相关的信息,使得Java语言编译程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修饰的运行。Java虚拟机在实行字节码时,把字节码解释成具体平台上的机器指令执行,这就是Java语言能够“一次编译,到处运行”的原因。
二.Java代码编译和执行过程
Java代码的编译是由Java源代码编译器来完成的,Java字节码的执行是由JVM执行引擎来完成的。
Java代码的编译和执行的整个过程包含三个重要机制:
Java源码编译机制
类加载机制
类执行机制
1.Java源码编译机制
三个过程:
- 分析和输入到符号表
- 注解处理
- 语义分析和生成class文件
生成class文件组成部分:
- 结构信息:包括class文件的格式,版本号及各部分的数量与大小信息
- 元数据:对应Java源码中声明与常量的信息。包括类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
- 方法信息:对应Java源码中语句与表达式对应的信息。包括字节码,异常处理器表,求值栈与局部变量区的大小,求值栈的类型信息,调试符号信息
2.类加载机制
负责加载JAVA_HOME中的jre/lib/jar里所有的class,JVM的类加载是通过ClassLoader及其子类来完成的。
3.类执行机制
JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC Register)和栈(Stack),程序计数器存放下一条要执行的指令在方法区的偏移量,栈中存放一个个栈帧,每个栈帧对应每个方法的每次调用,而栈帧又是由局部变量区和操作数栈等部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
三.JVM内存管理和垃圾回收
1.JVM内存体系结构
1.1 运行数据区(Runtime Date Area)——JVM内存从计算机内存中开辟一块内存区域存储JVM需要用到的对象,变量等,运行数据区分为很多小区:栈,堆,本地方法栈,方法区,程序计数器。
1.2 栈(Stack)——由编译器自动分配释放,在线程创建时创建,生命周期给随线程的生命周期,线程结束栈内存就释放,栈是线程私有的,对于栈来说不存在垃圾回收问题。
基本类型变量和对象引用变量都是在栈内存中分配。
每个线程在执行每个方法时都会在栈中申请一个栈帧,栈中的数据都是以栈帧的方式存在。
栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,一个有关方法和运行期数据的数据集,当方法A被调用是就产生一个栈帧F1,并被压入栈中,A又调用了B,于是产生栈帧F2也被压入栈中·····依次执行完毕,先弹出····F2,再弹出F1,其操作方式类似于数据结构的栈,遵循“先进后出,后进先出”的原则。
栈的优势:
存取速度比堆要快,仅次于位于CPU中的寄存器。
栈的缺点:
存在栈中的数据大小与生存周期必须是确定的,缺乏灵活性。
1.3 堆(Heap)——是一个可以动态申请的内存的空间,是JVM中最大的,在Java中,所有使用new构造出来的对象(类,方法)和数据(字符串常量)都在堆中储存,堆是线程共享的,是GC的主要回收区。它与数据结构中的堆是两回事。
堆的优势:
可以动态的分配内存大小,所有使用new构造的对象都在堆中储存,生存周期也不必事先告诉编译器,Java的GC(Garbage Collect)会自动回收那些不在使用的数据。
堆的缺点:
由于在运行时动态分配内存,存取速度慢。
堆的内部可划分为新生代,旧生代,持久代。新生代又被划分为伊甸区和幸存区。幸存区又被划分为From Space和To Space。
- 新生代——新建的对象都是由新生代分配内存。Eden空间不足时,经过垃圾回收存活的对象转移到Survior中
- 老年代——当新生代空间不足时,经过多次垃圾回收,仍然存活的对象会转移到旧生代中。
- 永久代——用于存放老年代经垃圾回收仍然存活的对象,主要存放class元数据(已加载的类信息,方法信息,常量池等)的地方。
注: 常量池——用于保存在编译期已确定的,已编译的class文件中的一份数据(包含代码中所定义的各种基本类型(如int、long等等)和引用类型(如String及数组)的常量值,还包含一些以文本形式出现的符号引用),常量池为了避免频繁创建和销毁对象而影响系统性能而存在的,其实现了对象的共享。
- Class文件常量池
- 运行时常量池
- 字符串常量池(JDK 1.7及以后在堆内存中)
1.4 类加载器(Class Loader)——负责加载.class文件,class文件在文件开头有特定的文件标示,至于class文件是否可以运行,由Execution Engine决定。
1.5 本地接口(Native interface)——本地接口的作用是融合不同编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生时C/C++正横行当时,要想立足,就必须调用C/C++程序,于是内存专门开辟一块区域专门处理标记为native的代码,具体做法是Native Method Stack登记native方法,在Execution Engine执行加载native library。
1.6 执行引擎(Execution Engine)——执行包在类中方法的指令
1.7 本地方法栈(Native Method Stack)——它的具体做法就是Native Method Stack中登记native方法,在Execution Engine执行时加载native library。
1.8 程序计数器(PC Register)——每个线程都有一个程序计数器,是线程私有的,是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一个指令。
PC 寄存器储存当前线程正在执行的Java方法的下一条即将执行的指令地址。CPU需要不停的切换各个线程,那么在线程切换回来之后,就需要知道接着从哪里开始执行。JVM的字节码解释器就需要通过PC 寄存器的值来确定下一条应该执行什么样的字节码指令。
1.9 方法区(Method Area)——方法区是被所有线程共享的,所有定义的方法的信息都保存在此区域中。在JDK 1.8及以后方法区被从堆内存中分离出来。方法区包含Class文件常量池和运行时常量池。
2.栈帧的数据结构
栈帧的组成:
- 局部变量表(Local Variables Table)
- 操作数栈(Operand Stack)
- 动态连接(Dynamic Linking)
- 方法返回地址
- 附加信息
2.1 局部变量表——是用来存储一组变量值的内存空间,用于存储方法参数和方法内部定义的局部变量。在已经编译好的Class文件中,方法的Code属性的max_locals数据项中,就确定了该方法所需分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Slot)为最小单位,每个变量槽存放一个32位数据类型(int,short,char,boolean,byte,reference),
reference类型表示对于一个对象实例的引用,通过引用做到两件事:根据引用直接或间接的查找实例在Java堆中的数据存放的起始地或索引;根据引用直接或间接的查找在方法区中存储的类信息。对于64位数据类型(long,double),是以高位对齐的方式为其分配两个连续的变量槽空间。
使用局部变量表,通过索引定位对应数据的位置,索引值从0至局部变量表最大的变量槽数量。访问64位数据类型变量,同时使用第n个和第n+1个变量槽,JVM不允许采用任何方法单独访问其中的某一个,否则JVM在类加载的校验阶段会抛出异常。
2.2 操作数栈——操作数栈是一个后入先出(LIFO)栈。操作数栈的最大深度已在编译好的Class文件方法Code属性的max_stacks数据项中。操作数栈的元素可以是任意数据类型,32位数据类型占栈容量位1,64位占栈容量位2。
当一个方法刚刚开始执行的时候,方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令对操作数栈进行出栈和入栈操作。操作数栈中会存储方法执行过程中的产生的一些中间结果,一个方法调用另一个方法时,通过操作数栈来进行方法参数传递。在JVM规范中,两个不同栈帧作为两个不同方法的虚拟机栈元素,是完全相互独立的。但大多数JVM会进行一些优化:两个不同方法的栈帧会出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,这样不仅可以节省一些内存空间,更重要的是在进行方法调用时就可以直接共享一部分数据,不需要在进行额外的的参数复制和传递。
2.3 动态连接——每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
Class文件常量池是指编译生成的class字节码文件,其结构组成的一项,用于存放在编译时期生成的各种字面值和符号引用。
class文件常量池中的内容经过类加载后进入运行时常量池中储存。一个类加载到JVM中后就对应一个运行时常量池,运行时常量池相对于class文件常量池具有动态性,class文件常量池是一个静态存储结构,里面的引用都是符号引用,而运行时常量池可以在运行期间将符号引用转化位直接引用。
符号引用与直接引用的区别:
比如org.simple.People类引用org.simple.Tool类,在编译People类的时候并不知道Tool类的实际内存地址,因此只能用符号org.simple.Tool(假设)来表示Tool类的地址。而在加载People类时,可以通过JVM来获取Tool类的实际地址。
2.4 方法返回地址——方法返回时可能需要在栈帧中保存一些信息,用于恢复调用者(调用当前方法的方法)的执行状态。一般情况下,方法正常退出时,调用者的程序计数器的值就可作为方法返回地址,栈帧很可能会保存这个技术器值。方法异常退出时,方法返回地址是要通过异常处理器表来确定的,栈帧一般不会保存这类信息。
2.5 附加信息——在JVM规范中,允许JVM增加一些规范里没有描述的信息到栈帧中,比如:调优,性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现。
3.垃圾回收
垃圾回收(GC)是Java虚拟机垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
什么时候进行垃圾回收?
- 在CPU空闲时自动进行回收
- 在堆内存储存满时进行回收
- 主动调用System.gc()后尝试回收
JVM的垃圾回收的优缺点?
优点:
开发人员无须过多的关心内存管理问题,可以将更多的精力放在解决业务上。虽然内存泄漏(OOM)在技术上仍然存在,但是很少发生。
GC在管理内存上有很多智能算法,它们在后台自动运行。与手动回收相比,它能更好的确定什么时候是进行垃圾回收的最好时机。
缺点:
当垃圾回收发生时将影响程序的性能,显著降低运行速度甚至会将程序停止。“Stop the World”就是当垃圾回收发生时应用程序的其他任务被冻结。对于应用程序来说这是不可接受的,虽然GC调优可以最小化甚至消除此影响,但开发者不能指定应用程序在什么时候怎样执行GC。