JAVA虚拟机是JAVA程序的运行环境,它是一种被虚拟出来的JAVA程序的计算机系统,它的与平台无关性,使得JAVA成为最具吸引力的程序设计语言。
文章目录
前言
本文将从JAVA虚拟机的各方面介绍JVM的神秘面纱,包括JAVA虚拟机的内存分布、JAVA虚拟机在执行JAVA程序的底层原理,深入理解JAVA虚拟机。本文的理论知识比较多,有大量的需要记忆的东西。
一、常见的java虚拟机(JVM)
1.1、Sun HotSpot VM
HotSpot是Sun/Oracle JDK 和 OpenJDK 中默认的虚拟机,是目前应运最为广泛的虚拟机,使用java –version来查看JDK版本信息:
C:\Users> java -version
java version "1.8.0_171" # 如果是openJDK, 则这里会显示:openjdk version
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode) # 使用的是HotSpot虚拟机,默认为服务端模式
1.2、IBM J9 VM
目前最具影响力的三大虚拟机之一。
1.3、Taobao VM
国内的一款虚拟机,由淘宝公司开发。
二、java虚拟机内存结构
java虚拟机内存结构大致由方法区、堆、虚拟机栈、本地方法栈、程序计数器部分组成。其中方法区和堆属于线程共享区,虚拟机栈、本地方法栈和程序计数器属于线程私有区。
方法区:一般存放被虚拟机加载的类信息(类的版本,字段,方法,接口),常量,静态常量,即时编译后的代码信息等。
堆:一般对象的实例就是在堆上分配内存,它是虚拟机管理的最大的一块内存区域,也是GC最主要的区域。JAVA堆可以处于不连续的内存空间,但是逻辑上是连贯的。
虚拟机栈:java方法执行的所用到的区域,每个方法执行都会创建一个栈帧,栈帧用于存放局部变量表,操作数栈,方法出口等信息。方法的从调用到执行完毕的过程就是栈帧在虚拟机栈中入栈到出栈的过程。当一个线程所请求的栈的深度大于虚拟机所允许的最大深度,就会抛出StackOverflowException。如果虚拟机栈能动态扩展内存,在申请内存时无法申请到足够的内存空间,则会抛出OutOfMemoryException。
本地方法栈:本地方法栈和虚拟机栈一样都是方法执行所用到的区域,不过,本地方法栈执行的是一些Native方法。
Native方法指的是:
(1)java类中的方法被native关键字修饰,类似于abstract修饰的方法一样,只有方法签名,没有具体实现。主要用于加载文件和动态链接库,比如java无法访问操作系统的底层信息,这时就必须使用C语言来实现访问操作系统底层信息的代码,也就是被native修饰的方法可以被C语言重写。
(2)实现步骤:
a、Java程序中声明native修饰的方法,类似于abstract修饰的方法,只有方法签名,没有方法实现。编译该java文件,会产生一个.class文件。
b、使用javah编译上一步产生的class文件,会产生一个.h文件。
c、.cpp文件实现上一步中.h文件中的方法。
d、.cpp文件编译成动态链接库文件.dll。
e、最用System或是Runtime中的loadLibrary()方法加载上一步的产生的动态连接库文件了。
程序计数器:程序计数器是程序执行的信号指示器。Java虚拟机在执行代码过程中,字节码解释器通过更改程序计数器的值来确定接下来执行哪一条指令。
三、Java对象
在java中,万物皆对象。在应运中对象是类的一个实例,指的是一个具体的东西。比如定义一个动物类,那么对象可以是具体的某个动物,比如猫。
3.1 对象的创建
3.1.1 使用new关键字创建对象
Record rr4 = new Record();
3.1.2 使用Class.forName()加载类,然后通过newInstance()获取类的实例或者先获取类的构造方法,通过构造器的newInstance()获取类的实例。
Class record = Class.forName("testDemo.Record");
// 直接通过newInstance()获取类的实例
Record re = (Record) record.newInstance();
System.out.println(re instanceof Record);
// 先获取类的构造器
Constructor con = record.getConstructor();
// 通过类的构造器获取类的实例
Record re1 = (Record) con.newInstance();
3.1.3 实现Clonable接口并重写clone()方法,调用clone()方法创建类的实例。
class Record implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Record rr2 = new Record();
Record rr3 = (Record) rr2.clone();
3.1.4 利用反序列化机制创建对象
序列化主要用到FileOutputStream和ObjectOutputStream两个类,主要是将某个对象转化成字节的数据,写出到磁盘上,所以FileOutputStream用来连接磁盘文件,ObjectOutputStream将文件写到文件上。
CurvedCounterStatModel curvedCounterStatModel = new CurvedCounterStatModel();
try (ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("curve.model"))) {
objOut.writeObject(curvedCounterStatModel);
} catch (IOException e) {
e.printStackTrace();
}
反序列化主要用到FileInputStream和ObjectInputStream两个类,将序列化写出的文件读取进来,转化成可用的对象。
try (ObjectInputStream obj = new ObjectInputStream(new FileInputStream("curve.model"))) {
CurvedCounterStatModel curved = (CurvedCounterStatModel) obj.readObject();
} catch (IOException e) {
e.printStackTrace();
}
注意:被序列化的类需要实现Serializablejie接口,告诉JVM,该类可以被序列化和反序列化。
补充内容:
如果被序列化的对象的成员变量包含引用数据类型,序列化的时候,引用数据类型的成员也会被一起被序列化,如果该成员变量还有引用成员变量,那么也会一起序列化,但是这些变量也要实现Serializable接口,否则会报错,这个过程中的序列化的过程是自动的。如果确实包含了不想被序列化的变量,那么使用关键字transient修饰即可,序列化的时候,会跳过transient修饰的变量。
如果父类实现了Serializable接口的话,因为继承关系,其子类也会编译成可被序列化的。
对象的序列化是为了保护对象的成员变量,所以序列化的时候,不会调用对象的构造函数,因为调用构造函数也就是改变了对象的成员变量。
对象的序列化是为了保护对象的成员变量,所以序列化的时候,其静态变量也不会被序列化。
3.2 对象分配内存
3.2.1 指针碰撞
假如堆内存空间连续,使用的空间占一半,未使用的内存空间占一半,中间使用一个指针指向零界点,当做指示器,当需要给对象分配内存的时候,就把指示器向未使用的内存空间移动对象大小个单位。
3.2.2 空闲列表
假如堆内存空间不连续,这时候需要维护一个内存列表来保存内存使用情况,当需要给对象分配内存时,就去内存列表中找一块内存分配给该对象,这个内存列列表称之为空闲列表。
注意:堆内存是否连续,取决于垃圾回收策略,虚拟机会根据垃圾回收器的回收策略来决定使用哪种内存分配方式。
3.3 对象的结构
3.3.1 对象头
_mark: 用于存储对象的运行时数据,例如:对象的Hashcode、GC的分代年龄、线程持有的锁、锁状态标志、偏向时间戳等,官方称之为“Markword”。
_kass: 用来存放对象所属class的指针,虚拟机通过这个来确认对象属于哪个类的实例。
_length: 对象数组独有,用来存放对象数组的长度。
3.3.2 实际数据
对象的实际数据中存储的是对象实际定义的各种数据,包自身定义的各种类型的数据,也包括从父类中继承下来的内容。
3.3.3 对齐填充
对齐填充这部分内容可有可无,没有实际上的意义,实际是起到一个占位符的作用。因为Hstpot VM要求对象大小必须是8字节的整数倍,而对象头正好是8字节的倍数,所以剩下的实际数据要是不够8字节的整数倍的时候,就需要从padding中补齐。
3.4 对象的访问
3.4.1 句柄访问
使用句柄访问的方式,会在java堆上开辟出一条内存空间,用来存放句柄信息,称之为句柄池,句柄池中存放的是对象实例数据的地址信息和对象类型数据的地址信息。
3.4.2 直接指针访问
如果是使用指针访问的方式,reference中存储的就是对象的地址(hotspot就是用的这种方式)。
优缺点:
句柄的方式访问,reference中存储就是固定的句柄信息,在对象被移除或者被GC后,只需要改变句柄中实际数据的指针即可,而reference本身不需要修改。
直接指针的方式访问,优势就是访问速度快,它节省了每次指针定位的开销(句柄池中指针的定位),因为java中对象的访问时很频繁的。
四、 垃圾回收
4.1 垃圾回收算法
4.1.1 标记-清除算法
这是最基础的回收算法,分“标记”和“清除”两个过程,首先标记出需要被回收的对象,然后再统一进行回收。
执行步骤:
缺点:
效率问题:“标记”和“清除”两个过程的效率都很低。
空间问题:标记清除算法回收完的内存空间不是连续的,后续再分配大对象时,可能会因找不大一块连续的足够的内存空间,而导致再触发一次垃圾回收。
4.1.2 复制算法
内存被分为一块大的Eden区域和两块相等的Survivor区域,每次使用其中的Eden区域和一块Survivor区域。当需要回收时,复制算法将Eden区域和使用的那块Survivor区域中还存活的对象一起复制到另外一块空闲的Survivor区域中(如果该Survivor区域空间不够存放这些对象,那么就需要向老年代借内存,剩余的内存将会通过内存担保进入老年代),然后清理掉Eden区域和刚才用过Survivor区域。清理完成后,Eden区域和刚刚放入存活对象的Survivor区域继续作为可用内存使用,被回收的Survivor区域作为空闲区域,留作下次回收使用。
4.1.3 分代回收算法
根据新生代对象“朝生夕死”,存活率低和老年代的对象存活高的特点,采用分代回收的算法。新生代因对象存活率低可以使用复制算法回收,老年代因对象的存活率高可使用标记-清除算法或者标记-整理算法回收。
4.1.4 标记-整理算法
标记-整理算法和标记-清除算法一样,只是标记-整理算法在标记过后不会立马清除,而是将可用存活对象向一边移动,之后将存活对象边界之外的区域全部回收。
4.2 垃圾回收器
4.2.1 Serial收集器(+XX:UseSeroalGC)
Serial收集器是一个串行收集器,也是一个单线程收集器。也就是一次只能使用一个CPU或者一个线程去进行垃圾回收,并且在进行垃圾回收的过程中,必须停掉所有的线程,直到垃圾回收工作结束。Serial收集器简单,没有线程交互的开销,是虚拟机Client模式下默认的垃圾收集器。
Serial收集器新生代采用的是“复制算法”,老年代采用的是“标记-整理”算法。
运行过程如下:
4.2.2 CMS收集器(-XX:+UseConcMarkSweepGC)
VM参数:
-XX:+UseCMSCompactAtFullCollection:控制CMS执行完Full GC后,进行一次碎片在整理过程。
-XX:CMSFullGCsBeforeCompaction=2:控制CMS执行完几次不带压缩的Full GC后,进行一次压缩的Full GC。
CMS收集器是一款以“获取最短回收停顿时间”为目的的垃圾收集器,对于这种要求高响应速度的系统或者网站,CMS是很好的选择。CMS收集器是基于“标记-清除”算法实现的。分四个步骤实现:
初始标记:标记一次GC Roots能直接关联到的对象。需要暂停用户线程,但是暂停时间很短。
并发标记:对对象进行可达性分析标记,这个过程是并发执行的,可以和用户线程要一起执行。
重新标记:标记并发标记过程中,变动过的那些对象。需要暂停用户线程,但是暂停时间很短。
并发清除:对标记过的对象,进行并发清除,可以和用户线程要一起执行。
运行过程如下:
CMS优点:并发收集,停顿时间短。
CMS缺点:
1、由于并发执行,对资源特别敏感,这是并发的通病,会减少吞吐率(简单的说,就是单位时间内会减少客户端与服务端之间的数据交互)。
2、 由于采用“标记-清除”算法,收集完毕后,会产生空间碎片。当遇到给大对象分配内存的时候,就需要提前执行FULL GC。为了解决这个问题,CMS提供了一个“- XX:+UseCMSCompaceAtFullCollection”的开关参数(默认开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程。
3、无法清除“浮动垃圾”,由于并发清除是和用户线程并发执行的,那么执行过程中用户线程又产生的垃圾将无法清除,当前GC任务已经无法处理,只能留在下一次GC任务中。
4.2.3 G1收集器(-XX:+UseG1GC)
G1收集器是目前最新的服务端收集器,具有以下特点:
并行与并发:G1收集器能利用多CPU和多核的硬件优势,极大的缩短用户线程的停顿时间。
分代收集:在G1收集器中依然保留了分代的概念,能独立并且采用不同的方式对新对象、存活了一段时间的对象和回收过多次的对象进行很好的处理。
空间整理:G1收集器既有“标记-整理”算法实现的部分,又有“复制”算法实现的部分,这两种算法不管怎么样都不会产生空间碎片,垃圾回收后,能对内存空间很好的进行归整处理。
可预测的停顿时间:
G1收集器分以下四个步骤执行:
初始标记
并发标记
最终标记
筛选回收
执行过程:
五、内存分配
5.1 优先在Eden上分配内存
设置以下几个vm参数:
-XX:+PrintGCDetails::打印GC信息
-Xms20m:设置最小堆内存10m
-Xmx20m:设置最大堆内存20m
-Xmn10m:设置年轻代内存10m
因为Eden和Survivor默认情况下是8:1,所以Eden区域总共大小8m,两个Survivor区域各占1m,可以看出,4m的内存全部在eden区域上分配。
5.2 大对象直接进入老年代
增加以下参数:
-XX:PretenureSizeThreshold=3M:设置老年代大小为3m。
可以看出5m的内存直接在老年代上分配。
5.3 长期存活的对象进入老年代
增加以下参数:
-XX:MaxTenuringThreshold=1(默认15):设置对象在新生代年龄为1的直接进入老年代
这里已经取消了设置限制老年代大小。发现在发生又一次GC后,对象直接进入了老年代。
5.4 对象年龄动态判断
Vm参数:
-XX:MaxTenuringThreshold=5:新生代对象进入老年代年龄阈值
为了更加灵活的分配内存,虚拟机并不是按照对象达到设定的年龄才进入老年代,如果虚拟机发现,Survivor区域中年龄相同的对象占了Survivor区域的一半或者年龄大于等于设置的年龄的对象正在进行GC,那么直接将这些年龄放入老年代。
5.5 空间分配担保
VM参数:
-XX:-HandlePromotionFailure:开启空间分配担保。
空间分配担保规则(以DK7为分界线不太准确):
JDK7之后:这个参数就失效了,GC规则就按照一下方式处理:如果老年代的连续空间比新生代对象的总大小都大,或者说比本次需要晋升到老年代的对象的平均大小都大,就进行Minor GC,否则就进行Full GC。
JDK7之前:如果在发生Minor GC,虚拟机首先会检查老年代的连续空间是否大于新生代对象的总大小。
如果大于:那么本次Minor GC是安全的。
如果不大于:那么检查是否开启空间分配担保(-XX:-HandlePromotionFailure):
如果没有开启:则直接进行一次Full GC。
如果开启了,则检查老年代的连续空间是否大于本次需要晋升到老年代的对象的平均大小
如果大于:则尝试进行Minor GC,但是仍是不安全的。
如果小于:则直接进行Full GC。
六、类文件的结构
6.1 class文件结构
使用无符号和表来表示class文件,二进制文件中没有空格和换行,使得文件更加紧凑,节省了空间,提高了性能。例如:Main.class文件:
魔数:class文件的前四个字节表示魔数,用来表示这个文件是不是虚拟机可以识别的class文件,而不是使用文件名后缀来识别。
版本号:接下来四个字节表示版本号,分为主板号和次版本号
主版本号(minor_version):0x0000
次版本号(major_version):0x0034,转化为十进制是52。Jdk8的版本号就是52。
常量池:接下来是常量池,常量池的数据是不固定的,所以会有一个u2类型的数据表示常量的数量计数(计数从1开始,其他计数从0开始)。0x000F翻译成十进制是16,表示有常量池中有16个常量,索引从1~15标识。
6.2 字节码指令
Java虚拟机的指令由一个字节长度、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。 操作码的长度为1个字节,因此最大只有256条,是基于栈的指令集架构。
如上图,相关指令的含义如下:
0: aload_0 //
1: invokespecial #1 // Method java/lang/Object."<init>":()V,调用特殊的方法,比如初始化方法,私有方法,父类方法。
4: return // 返回
============ main方法开始============
0: iconst_1 // 将本地常量1(Main.java定义的a)加载到操作数栈中。
1: istore_1 // 将操作数栈中的1(对应的a)加载到局部变量表中。
2: iconst_1 // 将本地常量1(Main.java定义的b)加载到操作数栈中。
3: istore_2 // 将操作数栈中的1(对应的b)加载到局部变量表中
4: iload_1 // 将第一个变量从局部变量表加载到操作数栈中
5: iload_2 // 将第二个变量从局部变量表加载到操作数栈中
6: iadd // 把操作数栈的量的元素执行add操作(a+b)。
7: istore_3 // 将iadd计算的结果加载到局部变量表中。
8: return // 返回
…………
6.3 字节码指令加载与数据类型
大多数指令包含了数据类型,比如iload表示将int类型的数据从操作数栈中加载到局部变量表中,其中i表示是对int类型数据进行操作,l表示long,s表示short,b表示byte,c表示char,f表示 float,d表示double,a表示reference。
字节码指令分好多中类型,比如方法调用指令,异常处理指令等,这里不多介绍。
七、类加载机制
虚拟机class文件加载到内存,并对数据进行校验,解析,初始化,最终形成可被虚拟机识别的java类型,这就是类的加载机制。
Jvm是懒加载,这样可以节省内存。
7.1 类的生命周期
加载,验证,准备,初始化,卸载这几个阶段的顺序是固定的,但是解析阶段不是固定的,有可能解析阶段会在初始化之后进行。
虚拟机规范中规定了,只有以下五种情况下才会对类进行初始化:
1. 当遇到new,getstatic,putstatic, invokestatic四个指令时,如果类没有初始化,则需要进行初始化。生成这四个指令最常见的场景:当遇到new关键字new对象的时候,获取或者设置静态字段的时候,调用静态方法的时候。
2. 利用反射进行类调用的时候,如果类没有初始化,则需要对其进行初始化。
3. 初始化一个类的时候,如果发现其父类没有初始化,则需要对父类进行初始化。
4. 程序启动的时候,需要给定一个程序入口,也就是包含main方法的那个类,虚拟机会先初始化这个类。
5. 类的动态引用
除过这个方式外,其他的引用类的方式都不会触发其初始化,称为被动引用。
1.通过子类直接访问父类的静态变量的时候,子类不会被初始化。
2.通过数组定义类的引用的时候,类不会被初始化。
3.通过new关键字创建类的实例的时候会触发类的初始化。
7.1.1 加载:
通过类的全限定名获取将定义这个类的二进制文件流。可以从本地磁盘上获取,也可以从网络上,jar包中等其他地方也可以。
将该二进制流中的静态存储结构转化为方法区中运行时的数据结构。
在内存中生成这个class类的代表对象,作为访问该类的数据的入口。
7.1.2 验证:
验证是连接阶段的第一步,为的是检验class文件的安全性,合法性,确保不对虚拟机造成危害。一般来说主要有:文件格式校验,元数据校验,字节码校验,符号引用校验。
7.1.3 准备:
仅对类变量(static修饰)分配内存并赋初始值,局部变量不会处理,局部变量会在初始化阶段和对象一起在堆中分配内存。对类赋初始值的时候,一般都是赋值的默认值,比如public static int a = 12,此阶段赋初始值0而不是12,因为此时还未执行任何java方法。但是对于public static final int a = 12,在准备阶段,会赋值成12。
7.1.4 初始化:
初始化阶段才正真执行java。
虚拟机开始产生并执行类构造器 () 方法,为类变量赋值(真正定义的值)和执行静态代码块。静态代码块只能访问定义在它之前的变量,可以为定义在它之后变量赋值,但是不能访问。
父类的静态代码块优先于子类的静态代码块。
如果一个类中没有对类变量赋值,也没有定义静态代码块,虚拟机可以不为该类生成()方法。
虚拟机会保证一个类的()方法只能被一个线程执行,它会很好的对()方法进行加锁,同步处理,如果有多个线程要执行()方法,则会阻塞,直到()方法执行完毕。
7.2 类的初始化过程(重要)
Student s = new Student();在内存中做了哪些事情?
加载Student.class文件进内存
在栈内存为s开辟空间
在堆内存为学生对象开辟空间
对学生对象的成员变量进行默认初始化
对学生对象的成员变量进行显示初始化
通过构造方法对学生对象的成员变量赋值
学生对象初始化完毕,把对象地址赋值给s变量
7.3 类加载器
一个类必须要必须由加载它的类加载器和它自身才能确认唯一性。如果两个类来自同一个class文件,但是类加载器不同,那么这两个类就相等。
启动类加载器
扩展类加载器
用户程序类加载器
自定义类加载器
定义一个类,继承ClassLoad类。
重写loadClass方法
实例化Class对象
7.4 双亲委派模型
双亲委派模型要求除了启动类加载器之外,其他的类加载器必须要有父类加载器。它的工作过程为:当一个子类加载收到类加载的请求时,它首先不会自己去加载这个类,而是将加载这个类的请求一层一层的抛出去丢给自己的父类加载器去处理,如果父类加载器处理不了或者加载不到这个类那么才会有子类自己去加载。
双亲委派模型下,用到的类加载器都具备一种优先级的层级关系。
类加载的loadClass()方法已经实现了双亲委派模型,自定义类加载器一般不要去重写loadClass()方法,但是需要重写findClass()方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
八、虚拟机字节码执行引擎
8.1 运行时栈帧结构
栈帧也叫过程活动记录,他是编译器用来调用方法和执行方法时用到的一种数据结构,它是虚拟机运行时的虚拟机栈的栈元素。栈帧包括了局部变量表,操作数栈,动态链接和方法返回地址以及一些附加信息。在编译的过程中变量的大小是确定的,相当于局部变量表是确定的,操作数栈的深度是确定的,没有逃逸的对象大小也是确定的,及时在栈上分配内存,栈帧的大小也是确定的,因此,栈帧的大小不会受运行时的影响。
当一个线程调用的方法很多并且多个方法都同时处于执行状态时,对于执行引擎来说,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧,与之关联的方法称为当前方法。执行引擎执行的字节码仅限当前栈帧。
每一个方法从调用待执行完毕过程,都对应着虚拟机栈中一个栈帧从入栈到出栈的过程。
局部变量表:局部变量表是一组变量值的存储空间,用来存储方法中定义的局部变量。局部变量表以变量槽(Slot)为最小单位,虚拟机规范中规定,一个Slot都应该能存放一个boolean、byte、char、short、int、 float、reference或returnAddress类型的数据。由于局部变量表是定义在线程中堆的,属于线程的私有数据,所以无论读写两个连续的Slot数据是否为原子操作,都不会出现数据安全问题。
为了节省栈帧空间,Slot可以重用,当程序计数器的值已经超出了某个变量的作用域的时候,那这个变量对应的Slot就可以交给别的地方使用。但是这中ot的重用可能会影响垃圾收集的行为。l
操作数栈:操作数栈是一个先入后出的栈结构。当方法执行的时候,就会有各种字节码指令往操作数栈中读写内容,也就是入栈出栈操作。
动态链接:每个栈帧中都包含一个从常量池中指向该栈帧所属方法的引用,包含这个引用是为了支持动态链接。
方法返回地址:方法退出的时候意味着栈帧出栈的过程。这个过程中需要执行的内容如下:
1、恢复上层方法的局部变量表和操作数栈。
2、将方法返回值(如果有返回值)压入调用者的栈帧的操作数栈中。
3、调整调用者程序计数器指向的下一条指令。
附加信息:虚拟机规范中规定,可以往栈帧中早呢更加一些额外的内容,这个取决于JVM的实现。
8.2 方法调用
方法调用不等于方法执行,方法调用的唯一目的是为了确定被调用方法的版本(继承和多态)。
方法调用-解析: 对于静态方法和私有方法,这些属于不可变方法,方法调用期间会直接把符号引用转化为直接引用。这些方法的调用称为解析。
方法调用-分派:
静态分派:方法重载属于静态分派,Java语言的静态分派属于多分派类型。
动态分派:方法重写属于动态分派,动态分派属于单分派类型。
动态分派调用过程:
1. 找到操作数栈栈顶的一个元素,确定它的实际类型。
2. 如果能找到该实际类型和常量池中的简单名称和描述符都相符的方法,则进行访问权限校验,如果权限校验通过,则直接返回这个方法的引用,否则抛出异常。
3. 如果没有找到,按照继承关系从下往上在各个父类中对实际类型进行查找和验证。
4. 如果最终还是没有找到,则抛出AbstactMethodErroe。
九、附加信息
9.1 System.gc常识
system.gc其实是做一次full gc
system.gc会暂停整个进程
system.gc一般情况下我们要禁掉,使用-XX:+DisableExplicitGC
system.gc在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent来做一次稍微高效点的GC(效果比Full GC要好些)
system.gc最常见的场景是RMI/NIO下的堆外内存分配等
总结
深入理解JVM的相关知识能够很好的帮助我们理解JAVA程序的运行原理,能够帮助我们在定位日常的各种生产问题。作为一个JAVA程序员这是必备的一些知识,希望本文介绍的各种内容能够帮助到大家。