字节码文件介绍:深入理解JVM之Java字节码(.class)文件详解_Windy_729的博客-CSDN博客_字节码文件
类加载子系统负责加载class文件,class文件开头有特定的文件标识。它的加载过程可以分为三个步骤:加载、链接、初始化
1.通过一个类的全限定名获取定义此类的二进制字节流(把class文件加载到方法区中)
没有指明二进制字节流必须从某个class文件中获取,可以通过多种方式获取:
本地系统获取
网络获取,Web Applet
zip压缩包获取---->生成jar,war、ear
运行时计算生成,如:动态代理
由其他文件生成,如:jsp,由jsp文件生成对应的class文件
专有数据库提取.class文件,比较少见
加密文件中获取,防止Class文件被反编译的保护措施
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(把class文件的常量池放入运行时常量池)
3.在堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
比较两个类是否“相 等”,只有在这两个类是由同一个类加载器加载的前提下才有意义
“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取上层:扩展类加载器
ClassLoader exClassLoader = systemClassLoader.getParent();
System.out.println(exClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586
// 获取上层:引导类加载器---获取不到:底层通过c/c++编写
ClassLoader bootStrapClassLoader = exClassLoader.getParent();
System.out.println(bootStrapClassLoader); // null
// 获取自定义类的加载器---器系统类加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// java核心类库的类加载器---引导类加载器
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null
}
}
这个类加载器使用C++语言实现,是虚拟机自身的一部分,这个类加载器负责加载存放在
以下↓这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。
这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,它负责加载用户类路径 (ClassPath)上所有的类库,如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
用户可以自定义一个类加载器去继承ClassLoader
1)什么是双亲委派机制?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器(不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用 组合(Composition)关系来复用父加载器的代码)去完成。
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
如果父类的加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这 就是双亲委派模式
2)双亲委派机制的好处?
通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载类就不会重复加载这个类。
保护程序安全,防止核心API被篡改
如果没有双亲委派机制,因为启动类加载器只会加载
3)双亲委派机制是怎么实现的?
先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法;
若父加载器为空则默认使用启动类加载器作为父加载器。
假如父类加载器加载失败, 抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
public abstract class ClassLoader {
.......
// 父类加载器在子类中的定义
private final ClassLoader parent;
......
}
// 实现双亲委派机制的代码都在loadClass中
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经被加载了
Class> c = findLoadedClass(name);
// 如果没有加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 获取父类加载器,如果父类加载器不为null
if (parent != null) {
// 调用父类加载器的loadClass方法,获取类对象
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为null,说明其父类加载器是最顶层的引导类加载器,调用引导类加载器的方法,去加载这个类,如果无法加载会抛出异常
/**
这里最终调用的是一个本地方法,也印证了bootstrap classloader 来自jvm的底层
private native Class> findBootstrapClass(String name);
**/
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 如父类加载器加载失败
// 抛出ClassNotFoundException异常的话,就调用自己的findClass()方法去加载
if (c == null) {
long t1 = System.nanoTime();
// 如果自己也无法加载也会抛出ClassNotFoundException异常
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
4)怎样破坏双亲委派机制?
知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。 因为它的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
loadClass()、findClass()、defineClass()的区别
ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
loadClass():就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
findClass():根据名称或位置加载.class字节码。
defineClass():把字节码转化为class。。
双亲委派破坏的例子
具体见:双亲委派模式 - 简书
第一种 被破坏的情况是在双亲委派出现之前
第二种,是JNDI、JDBC等需要加载SPI接口实现类的情况。
第三种,是为了实现热插拨、热部署工具
第四种,是Tomcat等web容器的出现
第五种,是OSGI、Jigsaw等模块化技术的应用。
验证
检验class文件中包含的信息符合虚拟机要求,保证被加载类的正确性,不会危害虚拟机。
文件格式验证
1.开头:CA FE BA BE(魔数,Java虚拟机识别) 2.主次版本号 3.常量池的常量中是否有不被支持的常量类型 4.指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量 元数据验证
1.对字节码描述的信息进行语义分析,保证描述符合Java规范 2.类是否有父类,除了Object之外,所有的类都应该有父类 3.类的父类是否继承了不允许被继承的类(被final修饰的类) 4.如果这个类不是 抽象类,是否实现了其父类或接口中要求实现的所有方法。 5.类的字段,方法是否与父类的产生矛盾。例如方法参数都一样,返回值不同 字节码验证
1.通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。 2.对类的方法体,进行校验分析,保证在运行时不会做出危害虚拟机的行为 3.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现类似于在操作数栈放了一个int类型的数据,使用时却按照long类型加载到本地变量表中的情况。 4.保障任何跳转指令都不会跳转到方法体之外的字节码指令上。 符号引用验证
1.通过字符串描述的全限定名是否能找到对应的类 2.符号引用中的类、字段、方法的可访问性是否可被当前类访问
可以考虑使用
-Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区 本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这 种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中
注:
public static int value = 123;
1.变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把 value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值 为123的动作要到类的初始化阶段才会被执行。
public static final int value = 123;
2.如果类字段 的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定 的初始值;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置 将value赋值为123。
3.首先是这时候进行内存分配的 仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
解析
将常量池内的符号引用替换为直接引用的过程
1.符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标
2.直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。
3.《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,只要求了在执行ane-warray、 checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、 invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于 操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需 要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引 用将要被使用前才去解析它。
初始化阶段就是执行类构造器
注:
1.编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问 到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访 问。
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用” }
static int i = 1;
}
2.
()方法与类的构造函数(即在虚拟机视角中的实例构造器 ()方法)不同,它不需要显 式地调用父类构造器,Java虚拟机会保证在子类的 ()方法执行前,父类的 ()方法已经执行 完毕。因此在Java虚拟机中第一个被执行的 ()方法的类型肯定是java.lang.Object。 3.由于父类的
()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值 操作 4.
()方法对于类和接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 ()方法 5.·接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 ()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法, 因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也 一样不会执行接口的()方法。
6.Java虚拟机必须保证一个类的
()方法在多线程环境中被正确地加锁同步,如果多个线程同 时去初始化一个类(去new类的实例对象),那么只会有其中一个线程去执行这个类的 ()方法,其他线程都需要阻塞等 待,直到活动线程执行完毕 ()方法。如果在一个类的()方法中有耗时很长的操作,那就 可能造成多个进程阻塞
public class DeadLoopClass {
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "start");
DeepLoopClass dlc = new DeepLoopClass();
System.out.println(Thread.currentThread().getName() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
public class DeepLoopClass {
static {
// 如果不加上这个if语句,编译器将提示“Initializer does not complete normally”
//并拒绝编译
if (true) {
System.out.println(Thread.currentThread().getName() + "init DeadLoopClass");
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
2.运行时数据区:
图片来源:终于搞懂了java8的内存结构,再也不纠结方法区和常量池了!_业余草的博客-CSDN博客
①程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。
在任何一 个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因 此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,所以它是“线程私有”的
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地 址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯 一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
②虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期 与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩 展时无法申请到足够的内存会抛出OutOfMemoryError异常。
HotSpot虚拟机的栈容量是不可以动态扩展的,所以在HotSpot虚拟 机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不 会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的。
局部变量表
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始 地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。)
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个(虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte....),byte、short 、char在存储前被转换为int, boolean 也被转换为int,0表示false,非0表示true;long,double则是转换为双字节大小的控件存储在栈里面。
局部变量表所需的内存空间在编 译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小。这里说的“大小”是指变量槽的数量,(这就是局部变量为什么要赋初值的原因,在编译期间就要确定好局部变量表的大小)
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
局部变量表中第0位索引的Slot默认“this”关键字的引用。
Slot可重用,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
操作数栈
操作数栈在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) /出栈(pop)。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如:执行复制、交换、求和等操作。
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_ stack的值。 栈中的任何一个元素都是可以任意的Java数据类型。32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位深度。操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop) 操作(先进后出)来完成一次数据访问。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令,在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:正常执行完成或者出现未处理的异常,非正常退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中拋出异常时的异常处理,存储在一一个异常处理表,方便在发生异常的时候找到处理异常的代码。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表,操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
一些附加信息
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、 性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现。
③本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失 败时分别抛出StackOverflowError和OutOfMemoryError异常。
《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规 定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接 就把本地方法栈和虚拟机栈合二为一。
④堆
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。主要存放对象实例,随着虚拟机的发展,堆中也存放了许多其他东西,主要有:
对象实例、数组、字符串常量池(原来放在方法区,jdk1.7开始放在堆中)、静态变量(静态属性、静态方法)(jdk7时从方法区移到了堆中)、线程本地分配缓冲区
字符串常量池
字符串常量池中存放的是字符串对象和字符串对象的引用
存放位置?
jdk1.6及之前存放在方法区,jdk7开始,移到了堆中
为什么从方法区移到了堆中?
方法区比较小,堆空间比较大,放在堆中可以减少OOM
方法区的GC频率比较低,堆空间的GC频率比较高,能够更好的对字符串进行回收,减少对内存空间的占用
为什么要设计字符串常量池?
因为String在Java中是不可变(immutable)的,那么如果对字符串进行拼接、截取等操作,都会创建新的字符串对象,字符串的使用又比较频繁,创建它要耗费大量的时间和空间,影响程序的性能。所以设计了字符串常量池来共享数据。
字符串常量池的数据结构
字符串常量池它底层StringTable(c++实现)类型的对象,是HashTable(数组+链表)的子类,数组的每个位置存放着HashTableEntry对象,HashTableEntry中又封装着key和value,索引下标由key和数组长度取余获得,key值是根据字符串内容+字符串长度计算出来的hash值,value指向String对象,String对象又指向char型数组,char[]保存实际数据。
StringTable默认长度1009。jdk1.6中StringTable的长度是固定的,就是1009;jdk1.7中StringTable的长度可以通过参数指定:-XX:StringTableSize=xxx
使用不同的方式创建字符串时在堆内存中如何存放?
(JVM调优部分进行分析)
java堆的大小是可以扩展的,通过参数-Xmx和-Xms设定:
-XX:InitialHeapSize(-Xms) 初始堆内存大小,默认是物理内存的1/64
-XX:MaxHeapSize(-Xmx) 最大堆内存大小 ,默认是物理内存的1/4
获取堆大小:
public class JVMMemoryDemo { public static void main(String[] args) { // 本机内存8G // 获取java虚拟机的内存总量 long totalMemory = Runtime.getRuntime().totalMemory(); // 获取java虚拟机的试图使用的最大内存总量 long maxMemory = Runtime.getRuntime().maxMemory(); // 默认是内存的1/64 System.out.println("TOTAL_MEMORY(-Xms):"+totalMemory + "字节、"+(totalMemory/(double)1024/1024)+"MB"); // 默认是内存的1/4 System.out.println("MAX_MEMORY(-Xmx):"+maxMemory + "字节、"+(maxMemory/(double)1024/1024)+"MB"); } }
⑤方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,在java8以前是放在JVM内存中的,由永久代实现,在java8时废弃了永久代的概念,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选 择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。
垃圾收集行为在这个区域的 确是比较少出现的,这区域的内存回 收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤 其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。
方法区在jdk8时,主要存放类元信息(kclass)、远行时常量池、、即时编译器编译后的代码缓存等数据
类元信息
放置了类的基本信息,包括类的版本、字段、方法、接口等描述信息以及常量池表(Constant Pool Table);常量池表用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池
对于运行时常量池, 《Java虚拟机规范》并没有做任何细节的要求,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来 的直接引用也存储在运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。String类的 intern()方法(动态向字符串常量池中放入堆中字符串对象的引用)。
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。
直接内存
直接内存(Direct Memory)位于本地内存,并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中 定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制。
3.执行引擎:
1)介绍
执行引擎是Java虚拟机的核心组成部分之一
虚拟机是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的。
而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM锁识别的字节码指令、符号表和其他辅助信息
那么,如果想让一个Java程序运行起来、执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者.
Java虚拟机的执行引擎在执 行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执 行)两种选择。HotSpot的模板解 释器工作的时候,动态产生每条字节码对 应的汇编代码来运行。
2)执行引擎的工作过程
Java代码编译是由Java源码编译器(Java前端编译器)来完成(与JVM无关),流程图如下所示
Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
现在JVM在执行Java代码的时候,会将解释执行与编译执行二者结合起来进行。如今Java采用的是解释和编译混合的模式:
执行引擎获取到,由javac将源码编译成字节码文件class.之后,
然后在运行的时候通过解释器interpreter转换成最终的机器码。(解释型)
另外JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,这种方式可以使执行效率大幅度提升(直接编译型)
解释器(interpreter)
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。
Interpreter模块:实现了解释器的核心功能
Code模块:用于管理HotSpot VM在运行时生成的本地机器指令
JIT (Just In Time Compiler):即时编译器
JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
JIT的热点探测技术
程序运行时,频繁被调用的代码被称为“热点代码”。
IT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On StackReplacement)编译。
一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠 热点探测功能。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
采用基于计数器的热点探测,HotSpot VM将会为每一个 方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter) 和回边计数器(BackEdge Counter) 。
方法调用计数器用于统计方法的调用次数。
回边计数器则用于统计循环体执行的循环次数。
方法调用计数器
这个计数器就用于统计方法被调用的次数,它的默认阈值在Client 模式 下是1500 次,在Server 模式下是10000 次。超过这个阈值,就会触发JIT编译。
这个阈值可以通过虚拟机参数-XX :CompileThreshold来人为设定。
当一个方法被调用时, 会先检查该方法是否存在被JIT编译过的版本,如 果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
注意:热频代码片段经过即时编译后的产物--机器指令,需要缓存起来Code Cache,存放在方法区(元空间/本地内存)
回边计数器
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边” (Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。
解释器依然存在的必要性
有些开发人员会感觉到诧异,既然HotSpotVM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
首先明确: 当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。
所以: 尽管JRockitVM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。
HostSpot JVM的执行方式
当虛拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。
并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
HotSpot VM 可以设置程序执行方式
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
-Xint: 完全采用解释器模式执行程序;
-Xcomp: 完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
-Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
测试表明:
纯解释器模式速度最慢(JVM1.0版本用的就是纯解释器执行)
混合模式速度更快
几种编译器
前端编译器:把.java文件转变成.class文件。包括Sun的Javac、Eclipse JDT中的增量式编辑器(ECJ)
后端运行期即时编译器(JIT编译器,Just In Time Compiler):把字节码转成机器码。包括HotSpot VM的C1、C2编译器
静态提前编译器(AOT编译器,Ahead Of Time Compiler):把*.java编译成本地机器码。包括GNU Compiler for the Java(GCJ)、Excelsior JET
本篇内容重点强调第二种,也就是我们的JIT。
HotSpot VM 中的JIT分类
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命.令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
-client: 指定Java虚拟机运行在Client模式下,并使用C1编译器;
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
-server: 指定Java虚拟机运行在Server模式下,并使用C2编译器。
C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
注意:64位操作系统默认使用-server服务器模式,即C2编译器。
C1和C2编译器不同的优化策略
在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除。
方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
去虚拟化:对唯一的实现类进行内联
冗余消除:在运行期间把一些不会执行的代码折叠掉
C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2.上有如下几种优化:(server模式下才会有这些优化,64位系统默认就是server模式)
标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未逃逸的对象分配对象在栈而不是堆
同步消除:清除同步操作,通常指synchronized(锁消除)
C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。
程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。
Graal编译器
自JDK10起,HotSpot又加入一个全新的即时编译器: Graal编译器
编译效果短短几年时间就追评了C2编译器。未来可期。
目前,带着“实验状态"标签,需要使用开关参数 -XX: +UnlockExperimentalVMOptions 一XX: +UseJVMCICompiler去激活,才可以使用。
AOT编译器
jdk9引入了AOT编译器(静态提前编译器,Ahead Of Time Compiler)
Java 9引入了实验性AOT编译工具jaotc。它借助了Graal 编译器,将所输入的Java 类文件转换为机器码,并存放至生成的动态共享库之中。
所谓AOT编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而AOT编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。
最大好处: Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待即时编译器的预热,减少Java应用给人带来“第一次运行慢”的不良体验。
缺点:
破坏了java"一次编译,到处运行”(提前干掉了能够跨平台的class文件),必须为每个不同硬件、oS编译对应的发行包。
降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知。
还需要继续优化中,最初只支持Linux x64 java base
原文链接:JVM执行引擎 - liuyanntes - 博客园
4.本地方法接口 JNI(Java Native Interface):
一个本地方法就是一个java调用非java代码的接口,一个本地方法是这样一个java方法:该方法的底层实现由非Java语言实现,比如C。这个特征并非java特有,很多其他的编程语言都有这一机制,比如在C++ 中,你可以用extern “C” 告知C++ 编译器去调用一个C的函数。
在定义一个本地方法时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。
为什么要使用本地方法呢?
与java环境外交互: 有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因。 你可以想想java需要与一些底层系统,如擦偶偶系统或某些硬件交换信息时的情况。本地方法正式这样的一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐细节。
与操作系统交互(比如线程最后要回归于操作系统线程) JVM支持着java语言本身和运行库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至jvm的一些部分就是用C写的。还有,如果我们要使用一些java语言本身没有提供封装的操作系统特性时,我们也需要使用本地方法。
Sun’s Java Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是它实现调用的事该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setPriority()API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。
原文链接:JVM--本地方法接口_Jesslili的博客-CSDN博客_java 本地方法接口
最后我们通过一个面试题,来看一下,解释器的执行过程--->下一篇
JAVA中的i++问题(字节码指令分析)_hanna22的博客-CSDN博客
下一篇,内存........JVM(2)之内存(对象的创建和结构+内存溢出OOM)_hanna22的博客-CSDN博客
原文链接:双亲委派模式 - 简书
原文链接:java之JVM的 类加载子系统_在路上的菜鸟的博客-CSDN博客
推荐阅读:java变量表_JAVA局部变量表_橙湖工作室的博客-CSDN博客
原文链接:Java虚拟机之虚拟机栈_tengxiaomeng的博客-CSDN博客_java虚拟机栈
原文链接:JVM执行引擎 - liuyanntes - 博客园