深入浅出JVM(五)之Java中方法调用

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 深入浅出JVM(五)之Java中方法调用

本篇文章将围绕Java中方法的调用,深入浅出的说明方法调用的指令、解析调用以及分派调用等

方法调用

要知道Java中方法调用唯一目的就是确定要调用哪一个方法

方法调用可以分为解析调用和分派调用,接下来会详细介绍

image.png

非虚方法与虚方法

非虚方法: 静态方法,私有方法,父类中的方法,被final修饰的方法,实例构造器

其他不是非虚方法的方法就是虚方法

非虚方法的特点就是没有重写方法,适合在类加载阶段就进行解析(符号引用->直接引用) 【编译时就能够确定】

调用指令

  • 普通调用指令
  • invokestatic:调用静态方法
  • invokespecial:调用私有方法,父类中的方法,实例构造器方法,final方法
  • invokeinterface:调用接口方法
  • invokevirtual: 调用虚方法
  • 使用invokestaticinvokespecial指令的一定是非虚方法
    使用invokeinterface指令一定是虚方法(因为接口方法需要具体的实现类去实现)
    使用invokevirtual指令的是虚方法
  • 动态调用指令
  • invokedynamic: 动态解析出需要调用的方法再执行
  • jdk 7 出现invokedynamic,支持动态语言

测试虚方法代码

  • 父类
 public class Father {
     public static void staticMethod(){
         System.out.println("father static method");
     }
 ​
     public final void finalMethod(){
         System.out.println("father final method");
     }
 ​
     public Father() {
         System.out.println("father init method");
     }
 ​
     public void overrideMethod(){
         System.out.println("father override method");
     }
 }
  • 接口
 public interface TestInterfaceMethod {
     void testInterfaceMethod();
 }
  • 子类
 public class Son extends Father{
 ​
     public Son() {
         //invokespecial 调用父类init 非虚方法
         super();
         //invokestatic 调用父类静态方法 非虚方法
         staticMethod();
         //invokespecial 调用子类私有方法 特殊的非虚方法
         privateMethod();
         //invokevirtual 调用子类的重写方法 虚方法
         overrideMethod();
         //invokespecial 调用父类方法 非虚方法
         super.overrideMethod();
         //invokespecial 调用父类final方法 非虚方法
         super.finalMethod();
         //invokedynamic 动态生成接口的实现类 动态调用
         TestInterfaceMethod test = ()->{
             System.out.println("testInterfaceMethod");
         };
         //invokeinterface 调用接口方法 虚方法
         test.testInterfaceMethod();
     }
 ​
     @Override
     public void overrideMethod(){
         System.out.println("son override method");
     }
 ​
     private void privateMethod(){
         System.out.println("son private method");
     }
 ​
     public static void main(String[] args) {
         new Son();
     }
 }

image.png

注意: 接口中的默认方法也是invokeinterface,接口中的静态方法是invokestatic

解析调用

解析调用就是在调用非虚方法

在编译期间就能够确定,运行时也不会改变

分派调用

分派调用又分为静态分派与动态分配

早期绑定:解析调用和静态分派这种编译期间可以确定调用哪个方法

晚期绑定: 动态分派这种编译期无法确定,要到运行时才能确定调用哪个方法

静态分派
   //静态类型         实际类型
     List list = new ArrayList();

静态分派: 根据静态类型决定方法执行的版本的分派

发生在编译期,特殊的解析调用

典型的表现就是方法的重载

 public class StaticDispatch {
     public void test(List list){
         System.out.println("list");
     }
 ​
     public void test(ArrayList arrayList){
         System.out.println("arrayList");
     }
 ​
     public static void main(String[] args) {
         ArrayList arrayList = new ArrayList();
         List list = new ArrayList();
         StaticDispatch staticDispatch = new StaticDispatch();
         staticDispatch.test(list);
         staticDispatch.test(arrayList);
     }
 }
 /*
 list
 arrayList
 */

方法的版本并不是唯一的,往往只能确定一个最适合的版本

动态分派

动态分派:动态期根据实际类型确定方法执行版本的分派

动态分派与重写有着紧密的联系

 public class DynamicDispatch {
     public static void main(String[] args) {
         Father father = new Father();
         Father son = new Son();
 ​
         father.hello();
         son.hello();
     }
     static class Father{
         public void hello(){
             System.out.println("Father hello");
         }
     }
 ​
     static class Son extends Father{
         @Override
         public void hello() {
             System.out.println("Son hello");
         }
     }
 }
 /*
 Father hello
 Son hello
 */

image.png

虽然常量池中的符号引用相同,invokevirtual指令最终指向的方法却不一样

分析invokevirtual指令搞懂它是如何确定调用的方法

  1. invokevirtual找到栈顶元素的实际类型
  2. 如果在这个实际类型中找到与常量池中描述符与简单名称相符的方法,并通过访问权限的验证就返回这个方法的引用(未通过权限验证返回IllegalAccessException非法访问异常)
  3. 如果在实际类型中未找到,就去实际类型的父类中寻找(没找到抛出AbstractMethodError异常)

因此,子类重写父类方法时,根据invokevirtual指令规则,先在实际类型(子类)中寻找,找不到才去父类,所以存在重写的多态

频繁的动态分派会重新查找栈顶元素实际类型,会影响执行效率

为提高性能,JVM在该类方法区建立虚方法表使用索引表来代替查找

字段不存在多态

当子类出现与父类相同的字段,子类会覆盖父类的字段

 public class DynamicDispatch {
     public static void main(String[] args) {
         Father son = new Son();
     }
     static class Father{
         int num = 1;
 ​
         public Father() {
             hello();
         }
 ​
         public void hello(){
             System.out.println("Father hello " + num);
         }
     }
 ​
     static class Son extends Father{
         int num = 2;
 ​
         public Son() {
             hello();
         }
 ​
         @Override
         public void hello() {
             System.out.println("Son hello "+ num);
         }
     }
 }
 /*
 Son hello 0
 Son hello 2
 */

先对父类进行初始化,所以会先执行父类中的构造方法,而构造方法去执行了hello()方法,此时的实际类型是Son于是会去执行Son的hello方法,此时子类还未初始化成员变量,只是有个默认值,所以输出Son hello 0

单分派与多分派

方法参数或方法调用者被称为宗量

分派还可以分为单、多分派

根据一个宗量(方法参数或方法调用者)选择方法被称为单分派

根据多个宗量(方法参数和方法调用者)选择方法被称为多分派

 public class DynamicDispatch {
     public static void main(String[] args) {
         Father son = new Son();
         Father father = new Father();
 ​
         son.hello(new Nod());
         father.hello(new Wave());
     }
     static class Father{
 ​
 ​
         public void hello(Nod nod){
             System.out.println("Father nod hello " );
         }
 ​
         public void hello(Wave wave){
             System.out.println("Father wave hello " );
         }
     }
 ​
     static class Son extends Father{
 ​
         @Override
         public void hello(Nod nod) {
             System.out.println("Son nod hello");
         }
 ​
         @Override
         public void hello(Wave wave) {
             System.out.println("Son wave hello");
         }
     }
 ​
     //招手
     static class Wave{}
     //点头
     static class Nod{}
 }
 /*
 Son nod hello
 Father wave hello 
 */

在编译时,不仅要关心静态类型是Father还是Son,还要关心参数是Nod还是Wave,所以静态分派是多分派(根据两个宗量对方法进行选择)

在执行son.hello(new Nod())时只需要关心实际类型是Son还是Father,所以动态分派是单分派(根据一个宗量对方法进行选择)

总结

本篇文章围绕Java方法的调用,深入浅出的解析非虚方法与虚方法、调用的字节码指令、解析调用和分派调用、单分派以及多分派

不能重写的方法(静态、私有、父类、final修饰、实例构造)被称为非虚方法,其他方法为虚方法

非虚方法是编译时就能够确定的,解析调用就是调用非虚方法

分派调用中的静态分派也是编译时确定的,是特殊的解析调用,根据静态类型选择方法,典型例子就是方法重载

分派调用中的动态分派是根据实际类型选择方法,在运行时才能够确定实际类型,典型例子就是方法重写

静态分派需要考虑方法调用者和方法参数是多分派,动态分派只需要考虑方法调用者是单分派


相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
24天前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
30 0
|
21天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
23天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
29天前
|
安全 Java
Java中WAIT和NOTIFY方法调用时机的深层解析
在Java多线程编程中,`wait()`和`notify()`方法的正确使用对于线程间的协调至关重要。这两个方法必须在同步块或同步方法中调用,这一规定的深层原因是什么呢?本文将深入探讨这一机制。
36 5
|
27天前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
39 1
|
27天前
|
Oracle 安全 Java
深入理解Java生态:JDK与JVM的区分与协作
Java作为一种广泛使用的编程语言,其生态中有两个核心组件:JDK(Java Development Kit)和JVM(Java Virtual Machine)。本文将深入探讨这两个组件的区别、联系以及它们在Java开发和运行中的作用。
68 1
|
1月前
|
监控 Java 开发者
Java虚拟机(JVM)深度优化指南####
本文深入探讨了Java虚拟机(JVM)的工作原理及其性能优化策略,旨在帮助开发者通过理解JVM的内部机制来提升Java应用的运行效率。不同于传统的技术教程,本文采用案例分析与实战技巧相结合的方式,为读者揭示JVM调优的艺术。 ####
58 8
|
2月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
70 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
45 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))