Fork me on GitHub

JVM 面试题

JVM 内存结构

主要由 Java 堆,JVM栈,方法区,本地方法栈和程序计数器组成。

  • 方法区:主要存储类结构信息、常量、静态变量、即时编译器编译后的代码等数据。
  • Java堆:主要存放对象实例,给对象分配内存。
  • JVM栈:主要描述 Java 方法执行的内存模型,用于存储局部变量表、操作栈、动态链接、方法出口等信息。
  • 本地方法栈:为虚拟机使用到的Native方法服务。
  • 程序计数器:当前线程所执行的字节码的行号指示器。

类加载过程

类的加载全过程:==加载,验证,准备,解析和初始==化这五个阶段。

加载:
==通过一个类的全限定名来获取其定义的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口==。

可以通过一些渠道加载 class 文件

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

验证:
==目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全==。

验证阶段大致会完成4个阶段的检验动作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行

准备:
==为类变量分配内存并设置类的初始值的阶段==

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

假设一个类变量的定义为:public static int value = 3;(如果变量用 final 修饰,则是初始化为3,并将 3 存放常量池中)

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的public static指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

解析:
==解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程==

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化:
==为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化==

GC回收机制

在什么时候

首先需要知道,GC又分为minor GC 和 Full Gc(也称为Major GC)。Java 堆内存分为新生代和老年代,新生代中又分为1个Eden区域 和两个 Survivor区域。

那么对于 Minor GC 的触发条件:大多数情况下,直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC;对于 Full GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次 Full GC。

Ps:能说明minor gc/full gc的触发条件、OOM的触发条件,降低GC的调优的策略。

eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等……

对什么东西

判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

从GC Roots搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象。

做什么事情

主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。例如新生代采用了复制算法,老年代采用了标记整理法。在新生代中,分为一个Eden 区域和两个Survivor区域,真正使用的是一个Eden区域和一个Survivor区域,GC的时候,会把存活的对象放入到另外一个Survivor区域中,然后再把这个Eden区域和Survivor区域清除。那么对于老年代,采用的是标记整理法,首先标记出存活的对象,然后再移动到一端。这样也有利于减少内存碎片。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派模型意义:

  • 当不同类加载器加载同一份 class 文件时,JVM 会存在两个独立的对象。(系统类防止内存中出现多份同样的字节码)
  • 保证Java程序安全稳定运行

JVM 调优及参数

参数

-Xms:初始堆大小

-Xmx :最大堆大小 此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存

-Xmn :年轻代大小 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-XX:NewSize:设置年轻代大小

-XX:MaxNewSize:年轻代最大值

-XX:NewRatio 年老代与年轻代的比值

-XX:SurvivorRatio:设置年轻代中Eden区与Survivor区的大小比值

-XX:MaxTenuringThreshold:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概论。

-XX:PermSize:设置持久带

-XX:MaxPermSize:设置持久代最大值

调优

JVM调优主要是针对内存管理方面的调优,包括控制各个代的大小,GC策略。由于GC开始垃圾回收时会挂起应用线程,严重影响了性能,调优的目是为了尽量降低GC所导致的应用线程暂停时间、 减少Full GC次数。

关键参数:-Xms -Xmx 、-Xmn 、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize

(1)-Xms、 -Xmx 通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM heap所能使用的最大内存。

(2)-Xmn 决定了新生代空间的大小,新生代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制(假如值为 4 表示:Eden:S0:S1 = 4:3:3 )

(3)-XX:MaxTenuringThreshold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。

(4)-XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。

java对象创建过程

  1. jvm遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)

  2. 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”

  3. 将除对象头外的对象内存空间初始化为0

  4. 对对象头进行必要设置

java对象结构

java对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

对齐填充:jvm要求对象起始地址必须是8字节的整数倍(8字节对齐)

java对象的定位方式

句柄池、直接指针。

如何判断对象可以被回收

  1. 引用计数(有bug 不能解决循环引用的问题)

  2. 可达性分析:

    可选做GC Roots的对象包括以下几种:

    虚拟机栈(栈帧中局部变量表)中引用的数据

    方法区中静态属性引用的对象(static)

    方法区中常量引用的对象(final)

    本地方法栈(native方法)中引用的对象

引用的分类

强引用:GC时不会被回收

软引用:描述有用但不是必须的对象,在发生内存溢出异常之前被回收

弱引用:描述有用但不是必须的对象,在下一次GC时被回收

虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知。

判断一个对象应该被回收

  1. 该对象没有与GC Roots相连

  2. 该对象没有重写finalize()方法或finalize()已经被执行过则直接回收(第一次标记)否则将对象加入到F-Queue队列中(优先级很低的队列)在这里finalize()方法被执行,之后进行第二次标记,如果对象仍然应该被GC则GC,否则移除队列。(在finalize方法中,对象很可能和其他 GC Roots中的某一个对象建立了关联,finalize方法只会被调用一次,且不推荐使用finalize方法)

回收方法区

方法区回收价值很低,主要回收废弃的常量和无用的类。

如何判断无用的类:

  1. 该类所有实例都被回收(java堆中没有该类的对象)

  2. 加载该类的ClassLoader已经被回收

  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方利用反射访问该类

垃圾收集算法

  1. 标记-清除 先标记再清除、效率不高、产生碎片。

  2. 复制算法 内存划分为大小相等的两块,每次只使用一块,当这一块用完了就将对象中存活的对象复制到另一块上边。 效率较高、有内存被空闲不能完全利用。 商业虚拟机目前使用这种算法回收新生代。用8:1:1来分配eden:From Survivor: To Survivo,即可以使用90%的对象,survivor空间不够时需要老年代分配担保

  3. 标记-整理 标记之后将所有存活的对象向一端移动,不产生碎片

OopMap、安全点、安全区域、抢先式中断、主动中断

OopMap:一组特定的数据结构,记录数据引用(用来判断哪些对象是存活的)

安全点:在特定位置生成OopMap,这些位置成为安全点,只有在安全点才能暂停进行GC

安全区域:一段代码中引用关系不会变化,这一段代码称为安全区域(扩展的安全点)

抢先式中断:GC是所有线程都中断、把不在安全点上的线程重新启动跑到安全点上。

主动式中断:多线程去轮询一个标志位,当发现标志位为真时则自动挂起(标志位为真的位置应该与安全点位置重合)

垃圾收集器

Serial:新生代复制算法(单线程)STW,老年代标记整理。STW

ParNew:新生代复制算法(多线程)STW,老年代标记整理。STW

Parrallel Scavenge:新生代收集器,专注于吞吐量
吞吐量 = 运行用户代码时间/(运行用户代码时间 P+ GC时间)

CMS(Concurrent Mark Sweep)收集器:以获取最短回收时间为目标的收集器。
分为四个步骤

  1. 初始标记:STW 很快,用来标记GC Roots能直接关联到的对象
  2. 并发标记:并行
  3. 重新标记:并行,修正并发标记中产生变动的对象记录
  4. 并发清理
    CMS对CPU资源敏感、无法处理浮动垃圾、用标记清除实现有内存碎片

G1(Garbage-First)
整体看来采用标记整理,局部看来(Region之间)采用复制算法。G1将整个java堆分成许多Region,跟踪各个Region里垃圾的回收价值大小,优先回收价值大的Region.
步骤:初始标记、并发标记、最终标记、筛选回收。除了最后一步都和CMS差不多

自动化内存分配是怎么分配的

对象主要分配在新生代的Eden区(新生代)上,如果启用了TLAB(本地线程分配缓冲)就分配在TLAB上,大对象(比如很长的数组)直接进入老年代,长期存活的对象进入老年代(参考对象头中的年代值)
动态对象年龄判定(如果在survivor空间中相同年龄所有对象的大小总和大于等于survivor空间的一半,则年龄大于或等于该年龄的对象直接进入老年代)

空间分配担保

在Minor GC之前,jvm会检查老年代最大可用连续空间是否大于新生代所有对象的空间。如果成立,则Minor GC是安全的。否则

  1. jvm设置不允许担保失败则立刻Full GC
  2. jvm设置允许担保失败则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行一次冒险的Minor GC 否则Full GC

PS: Full GC 其实是为了让老年代腾出更多空间

Class文件结构

  1. Class文件是一组以8位字节(1byte = 8bit)为基础单位的二进制流,中间没有任何分隔符,遇1个字节以上的数据,采用高地址对应低字节来分割存储。
  2. class文件从前到后依次是
    魔数、版本号、常量池计数、常量池、访问标识、类索引、父类索引、接口计数、接口表、字段计数、字段表、方法计数、方法表、属性计数、属性表。
  3. 常量池从1开始计数,存储两样东西:字面量(Literal)和符号引用。字面量可以理解为常量(文本字符串、声明为final的变量)符号引用则包括以下几种:类和接口的全限定名、字段的名称和描述(字段可以理解为在class内,方法外声明的那些变量)、方法的名称和描述。
  4. class中字符串、方法名的长度(占用byte的个数)用一个2字节的int来表示,即方法名和字符串最大长度为 2^16 * 1字节 = 64KB 也就是说,超过这个长度的方法名或者字符创(”xxxxx”)将无法编译
  5. 访问标识
    占两个字节,用来标识是否 public abstract interface final 注解 枚举 等等
  6. 类索引、父类索引
    确定类的全限定名和父类的全限定名
  7. 字段… 这段以后再总结吧

类加载时机、什么时候类会加载

类在内存中的整个生命周期包括
加载
链接: 验证、准备、解析
初始化
使用
卸载
除解析外所有步骤依次执行,解析可能会发生在初始化之后(多态,动态绑定)

什么时候会加载?

  1. 遇到***指令的时候,即new对象了,调用类静态字段(fina定义的除外且有初值的除外 )、静态方法
  2. 反射调用
  3. 初始化一个类时需要先初始化其父类
  4. jvm启动时加载的包含main()的那个类
  5. jdk1.7中反射调用方法。。。没看懂。。。

类加载过程

  1. 通过一个类的全限定名获取定义这个类的二进制字节流
    包括:.class 、jar、 网络、 甚至实时生成(动态代理 java.lang.reflect.Proxy)、其他文件(jsp)…
  2. 将这个字节流所代表的类的静态数据结构存储在方法区中(转换成了动态结构)
  3. 在内存中生成一个代表该类的java.lang.Class对象,作为方法区中所有访问该类的数据访问入口

类验证过程

  1. 文件格式验证

Class文件是否符合规范,当前虚拟机版本是否能处理

  1. 元数据验证

进行语义分析:是否有父类、是否实现了所继承的抽象类中所有方法…

  1. 字节码验证

通过数据流和控制流分析,语义是合法合理的,进一步的语义分析。(一个int的值没有被当成lang来写入)

  1. 符号引用验证

发生在将符号引用转换为直接应用的时候(解析阶段)用来检查权限,以及是否能成功解析

类的准备阶段

正式为类变量分配内存并设置变量初始值(0和null),只包括的类变量(static修饰),不包括实例变量

类的解析

解析阶段是将常量池内的符号引用替换为直接引用的过程

解析的动作针对于:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符

符号引用:用一组符号(字符串)来描述所引用的目标,符号可以使任何形式的字面量,符号引用与jvm的内存布局无关,此时类可以没有加载到内存中

直接引用:可以使直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄,直接引用与内存布局相关,此时类一定已经加载到内存中了

类的初始化

真正开始执行类中定义的java程序代码,编译器收集类中所有变量的赋值语句和静态语句块合并成<clinit方法(执行该方法前一定要执行父类的方法,所以所有类中最先执行的<clinit方法一定是java.lang.Objer)并执行

类加载器、双亲委派模型

对于任意一个类都需要由加载它的类加载器和这个类本身一同确立其在jvm中的唯一性(即比较两个类是否相等先要比较这两个类是否由同一个ClassLoader加载)

jvm的角度来看有两种不同的类加载器:

  1. 启动类加载器,用C++实现,是虚拟机的一部分,负责启动<java_home>\lib目录中,或者用-Xbootclasspath参数指定的类

  2. 所有其他类的加载器(可以细分为扩展类加载器、应用程序类加载器),由java实现,独立于虚拟机,继承于java.lang.ClassLoader

扩展类加载器用来加载<java_home>\lib\ext目录中的

双亲委派模型:当一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,二十把这个请求委派给父类加载器去完成,只有当父加载器反馈无法加载这个请求(如:它的搜索范围中没有找到所需要的类),子类才会去加载(好处就是java类随着加载器一起构成了一个优先级关系,比如我同样定义了一个java.lang.Object类最终加载出来的还是系统自带的那个。。。也有可能编译通过,拒绝加载)

启动类加载器

|_扩展类加载器

​ |_应用程序类加载器

​ |_自定义类加载器

破坏双亲委派模型:基础类需要回调用户的代码(即要先加载用户类 比如 JNDI服务)
采用 线程上下文类加载器(父类请求子类的加载器去加载这个类)

运行时栈帧结构

栈帧是用来支持虚拟机进行方法调用和方法执行的数据结构,jvm运行时数据区中虚拟机栈的栈元素。栈帧中存储了:局部变量表、操作数栈、动态链接、返回地址。每个方法从调用到执行完成对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。只有栈顶的当前栈帧是有效的

局部变量表

  1. 是一组变量值存储空间。用来存储方法参数和方法内部定义的局部变量。在.class中已经由code属性的max_locals项决定了局部变量表的最大容量。

  2. 局部变量表容量以变量槽(Slot)为最小单位(32/64bit)

  3. 局部变量表中第0为存储的是:方法所属对象的实例引用,即 this~~

  4. slot可以重用,即局部变量表的空间可能会比实际所有方法中变量需要占用的空间要小,有时候把不使用的对象手动赋null,可能会有助于GC

  5. 类变量(成员变量)(class内 方法外)是默认有初值的(在类准备阶段被赋0和nul了),而方法内的变量,即局部变量是没有初值的,不能直接调用

  6. long和double占两个slot,可以用volatile关键字修饰,防止在多线程环境下的字撕裂。

操作数栈

  1. 操作数栈中每一个元素可以是任意的java类型

  2. 操作数栈中的元素数据类型必须与字节码执行指令序列严格匹配(eg:当前指令时iadd~两个int相加的指令,那么操作数栈顶必须是两个int类型!)

方法返回地址

方法返回地址八成存的是上一个方法PC计数器的下一条指令。方法执行后有两种方式退出方法。

  1. 正常退出

  2. 异常退出(不会给上层调用者提供返回值)

正常退出的情况下,需要把当前栈帧出栈、恢复上层方法的局部变量表和操作数栈、把返回值(如果有)压入栈顶。调整PC计数器让它指向方法调用指令的后一条指令

动态链接(多态/动态绑定 DynamicLinking)

  1. 语法层面简单的说,多态:有继承、有重写、父类引用指向子类对象。

深层次的讲:

  1. 每个栈帧都包含一个指向运行时常量池中改栈帧所述方法的引用(即一个符号引用,表明这个栈帧代表哪个类的哪个方法?)。在一种情况下:类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一种情况:在方法的每一次运行时都进行解析转换为直接引用,这就是动态链接

  2. 方法调用,不等于方法执行!方法调用的目的是确定需要调用方法的哪个版本(即调用哪个类的哪个方法)

  3. 调用目标在程序代码写好后,编译期编译时就能确定的方法,就可以用静态解析,即“编译期可知,运行期不变”。包括两种方法:其一,静态方法(static 与类的类型直接关联).其二,私有方法(private 不能被继承并重写,因此只有一个版本)

补充:实例构造器、父类方法也可以静态解析,这四种方法统称为非虚方法(外加fina修饰的方法),所有其他的方法称为虚方法。

  1. 分派:解析调用一定是静态过程(在运行期之前就已经确定),而分派可能是静态可能是动态的过程,即分为静态单分派、静态多分派、动态单分派、动态多分派。具体是多还是单是有分派所依据的宗量数决定;

  2. eg

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Human{ }
    class Man extends Human{ }
    class Woman extends Human{ }
    public static void say(Human guy){
    System.out.printlun(“Human”);
    }
    public static void say(Man guy){
    System.out.printlun(“Man”);
    }
    public static void say(Woman guy){
    System.out.printlun(“Woman”);
    }
    public static void main(String[] args){
    Human man= new Man();
    Human woman = new Woman();
    say(man);
    say(woman);
    }
    //结果
    //Human
    //Human
  3. eg

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Human{
    public void say(){
    System.out.println("Human say");
    }
    }
    class Man extends Human{
    public void say(){
    System.out.println("Man say");
    }
    }
    class Woman extends Human{
    public void say(){
    System.out.println("Woman say");
    }
    }
    public static void main(String[] args){
    Human m1 = new Human();
    Human m2 = new Man();
    Human m3 = new Woman();
    m1.say();
    m2.say();
    m3.say();
    m3 = new Man();
    m3.say();
    }
    //结果
    //Human say
    //Man say
    //Woman say
    //Man say

java中重写是动态分派,当运行期调用某个对象的方法时需经历以下几个过程:

  1. 找到操作数栈顶的第一个元素(即当前对象)的实际类型,记为C

  2. 如果在类型C中找到了与常量中描述符和简单名称都相符的方法(方法名一样、参数类型个数一样)则进行权限访问检查,能通过检查则调用这个方法,否则返回java.lang.IllegalAccessError.

  3. 否则按照继承关系依次对C的父类进行第二步的操作。

  4. 最终如果没有找到合适方法,抛异常~

  5. 单分派与多分派。

宗量:方法的接受者与方法的参数统称为总量 A.method( XX,YY,ZZ) (A XX YY ZZ 称为method的宗量 )

单分派:根据一个宗量对目标方法进行选择(选择哪个类、选择这个类中的哪个方法)

多分派:根据超过一个宗量对目标方法进行选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
eg:
public class JavaDispath{
public static void main(String[] args){
Father father = new Father();
Father son = new Son();
father.choice(new QQ());
son.choice(new _360());
//son.choice(new _360(),1);
System.out.println("~~~~~~~~~~~");
QQ qiruiQQ = new QiruiQQ();
son.choice(qiruiQQ);
}
}
class QQ{
static void say(){
System.out.println("I'm QQ");
}
}
class QiruiQQ extends QQ{
static void say(){
System.out.println("I'm QiruiQQ");
}
}
class TengxunQQ extends QQ{
static void say(){
System.out.println("I'm TengxunQQ");
}
}
class _360{}
class Man{
void choice(_360 arg, int a){
System.out.println("Man chooce 360");
}
}
class Father extends Man{
void choice(QQ arg){
QQ.say();
System.out.println("Father chooce QQ");
}
void choice(_360 arg){
System.out.println("Father chooce 360");
}
}
class Son extends Father{
void choice(QQ arg){
QQ.say();
System.out.println("Son chooce QQ");
}
void choice(_360 arg){
System.out.println("Son chooce 360");
}
void choice(QiruiQQ arg){
QQ.say();
System.out.println("Son chooce QQ");
}
}
//结果
//Father chooce 360
//I'm QQ
//Son chooce QQ
//~~~~~~~~~~~
//I'm QQ
//Son chooce QQ

上边的程序运行过程时这样的:

分割线之前

在编译期(静态分派的过程~),系统根据father和son的静态类型即Fahter和后边的方法名还有参数,决定调用

Father.choice(360) 和Father.choice(360)

因此静态分派是多分派(超过了一个宗量)

此时是方法调用的过程

在运行期(动态分派过程~),系统根据father和son的动态类型决定调用

Father.choice(360) 和Son.choice(QQ),此时是动态分派,根据实际类型去找到应该执行哪个类的哪个方法,此时根据son的动态类型Son来选择了执行Son.choice(QQ)。而不论里边的QQ传进来的到底是奇瑞QQ、还是腾讯QQ,jvm都不会去理睬,只是把它当做一个普通QQ来对待。因此动态分派只取决于方法的接受者的实际类型,与参数无关。因此java语言的动态分派是单分派(一个宗量决定)。

分割线之后

在分割线之后,我们尝试着在son.choice中传入了一个QiruiQQ 但是jvm还是只把它当成一个普通QQ来看待,验证了我们上边的说法。

总的来说,java是静态多分派,动态单分派。

  1. 多态是怎么实现的

出于效率的考虑,我们上文中所说的那种一层层向上搜索的算法在实际中不会用到。。。

实际中为每个类在方法区的位置中搞了一个虚方法表(也有接口方法表),方法表中罗列着这个类所继承的所有方法的全限定名以及他们的引用指向,比如上文Father类的方法表中clone()方法指向java.lang.Objer.clone()的入口地址,而choice(QQ)方法,就指向自身的choice(QQ)方法。

坚持原创技术分享,您的支持将鼓励我继续创作!