第二章 Java内存区域与内存溢出异常
一、运行时数据区域
1.程序计数器
线程私有,是一块较小内存空间,可以看作当前线程的执行字节码的行号指示器。CPU分配给A线程的时间片用完,线程挂起,程序计数器记录当前线程执行的字节码指令地址。线程切换回来,再次获取CPU时间片,依靠程序计数器保存的字节码指令地址继续执行程序。
2.虚拟机栈
线程私有,生命周期和线程一样。Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈等信息,每一个方法的调用到完成的过程都对应一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译器可知的基本数据类型,对象引用和returnAddress类型。局部变量表所需的内存空间在编译期间完成分配,进入一个方法时这个方法需要在虚拟机栈中分配多大的局部变量空间是确定的。
3.本地方法栈
发挥的作用和虚拟机栈类似,虚拟机栈为java方法服务,本地方法栈为虚拟机使用到的native方法服务。
4.Java堆
是java虚拟机管理的内存中最大的一块。java堆线程共享,所有的对象实例和数组被分配到堆内存中。Java堆是垃圾收集器管理的主要区域,可以叫做GC堆。内存回收的角度可以分为新生代、老年代。内存分配的角度,共享的java堆还可以划分出多个线程私有的内地内存。 这里写图片描述
5.方法区
与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息 、常量、静态变量等数据。方法区和Java堆一样不需要连续的内存并且可以选择固定大小或是可扩展。方法区的垃圾回收目标主要是对常量池的回收与类型的卸载,垃圾收集频率很低。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
6.运行时常量池是方法区的一部分
Class文件除了有类的版本、字段、方法、接口的信息外,还有编译期生成的各种字面量和符号引用,这部分内容在类加载后会进入方法区的运行时常量池中存放。运行时常量池具有动态性,运行期间也可能将新的常量放入池中。
二、对象创建、对象的内存布局、对象的访问定位
1.对象的创建
虚拟机遇到一条new指令,先检查这个指令的参数是否能在方法区的运行时常量池定位到一个符号引用,检查符号引用代表的class类是否被加载,解析和初始化。若没有,那么先执行相应地类加载过程。
类加载后在java堆中为对象实例分配内存空间,对象内存空间大小在类加载时确定好。分配的策略分两种。如果java堆的内存是规整的,所有用过的内存放在一边,空闲内存放在一边,中间由一个指针作为分界器。分配内存就是将指针从使用过的内存往空闲内存区划一个分配空间的大小区域。这种叫做“指针碰撞”。若java堆使用内存和空闲内存相互交错,虚拟机就维护一个“空闲列表”记录哪些内存块是空闲。分配内存时,从空闲列表里查找差不多大小的内存块存储对象实例,并且更新空闲列表。
**哪种分配方式的使用由垃圾收集器的不同类型来决定。 **
还需要考虑并发情况下创建对象的线程安全问题。如果A线程给A对象分配内存,指针还没来得及修改,B线程使用原来的指针位置给B对象分配内存,导致内存分配出问题。
JVM解决的方法是使用CAS操作来保证更新指针位置的原子性。另一种是java堆里每个线程都有线程私有的分配缓冲区,对象分配在分配缓冲区中分配,相互独立。
对象实例分配完成后需要将分配到的内存空间初始化零值。这样保证实例字段不赋初始值也可以使用。
接着虚拟机会对对象头进行设置。对象头包含markword,和指向对象类型数据的指针。markword里面包含了对象的锁状态,hashcode和分代年龄等。 这样一个新的对象产生了。
2.对象的内存布局
对象在内存中存储分为3块区域:对象头、实例数据和对齐填充。 对象头分为俩个部分,一个是含有锁状态,对象hashCode,分代年龄 ,是否是偏向锁信息的markword,另一个是指向对象类型数据的指针。如果对象是数组,对象头里还需要有一块记录数组长度的内存。
实例数据部分是对象真正存储的有效信息,也是各种类型的字段内容。 对齐填充起着占位符的作用,因为JVM要求对象的大小必须是8字节的整数倍,对象头恰好是8字节的整数倍,若实例数据部分没有对齐则需要通过对齐填充来补全空间。
3.对象的访问定位
java程序通过虚拟机栈上的reference引用操作堆上的具体对象。对象访问方式有俩种:
句柄访问:java堆上划分一块内存作为句柄池,虚拟机栈的reference上存的是句柄池的地址,句柄池存有分别指向对象实例数据和类型数据的指针。好处:对象在垃圾回收时频繁移动,只需要修改句柄池中实例数据地址,reference不需要修改。但是访问效率慢,需要俩次指针定位。
直接指针访问:虚拟机栈的reference里存有指向java堆中具体实例对象的地址,对象头里存储指向类型数据的地址。指针访问速度快,一次定位。
三、OutOfMemoryError异常,内存溢出异常
- 堆 heap
- 栈 stack
- 新生代 new generation
- 老年代 tenured generation
1.内存溢出(OutOfMemory)
程序在申请内存的时候,没有足够的内存空间供其使用,出现内存溢出现象。比如java堆中内存不够用仍为大对象分配内存空间导致内存溢出。
2.内存泄露(memory leak)
程序申请分配内存后,无法回收已申请分配的内存空间。一次内存泄露危害可以忽略,但是内存泄露堆积后果很严重,内存不断分配但漏回收最后会导致内存溢出。
3.虚拟机启动参数(run->EditConfiguration)
- -Xms:堆的初始值大小 -Xmx:堆的最大值大小 将初始值和最大值设置为一样可以避免堆自动扩展
- -Xss128k:设置虚拟机栈的容量
- -XX:PermSize=10M –XX:MaxPermSize=10M 限制方法区的大小,从而间接限制运行时常量池的大小。
- -XX:NewSize=10M:设置新生代大小 –XX:SurvivorRatio=8 设置Eden区和Survivor区之间的比例:8:1:1
- -XX:+PrintGCDetails: 查看GC回收日志
- -XX:MaxTenuringThreshold=15:设置对象晋升老年代的年龄阈值
4.Java堆溢出
Java堆存储对象实例,如果不断创建对象,且对象到GCroots对象还有引用链导致对象仍存活,则堆内存不断被占用直到对象总大小到达-Xmx参数设置的堆最大值时,导致内存溢出异常。
Java堆出现内存溢出异常时,先判断内存中的对象是否需要被回收,也就是判断是内存泄露还是内存溢出。如果是内存泄露,查看对象是如何通过引用链连接上gc roots对象的,导致垃圾回收器无法回收对象,这样可以分析出泄露内存的代码。
如果内存的对象必须活着,则是内存溢出。则查看虚拟机的启动参数-Xms和-Xmx与机器的物理内存比是否可以再调大,堆内存扩展。再看看代码里是否存在很多不必要的大对象。
5.虚拟机栈和本地方法栈溢出
对于虚拟机栈,会有俩种异常出现:如果线程请求的栈深度大于虚拟机所允许的最大深度,则会抛出StackOverflowError。比如方法里定义了大量局部变量导致一个栈帧容量过大,或是方法调用过多,入栈的栈帧数量过多(不断递归情况下)。
虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常。比如无限制的创建线程,每个线程都有私有的虚拟机栈,导致内存空间不够用。
6.方法区和运行时常量池溢出
如果不断创建新的字符串对象,分配空间到运行时常量池中会导致运行时常量池溢出。加载大量的类到方法区中会导致方法区溢出。
第3章 垃圾收集器与内存分配策略
一、 概述
垃圾收集理解为内存回收,程序计数器,虚拟机栈,本地方法栈均为线程私有,生命周期和线程一样,线程死亡,内存自然回收。Java堆和方法区由于线程共享,内存分配是动态的,所以需要垃圾收集器动态回收内存,防止内存溢出或泄漏。
二、如何判断对象可回收,对象存活
1.引用计数算法(redis)
给对象添加一个引用计数器,每当有一个地方引用他时,计数器值加1;当引用失效时,计数器减1;当计数器值为0时表示对象可回收。 但是难以解决对象之间相互循环引用的现象。
2.可达性分析算法
JVM是通过可达性分析算法判定对象是否存活,通过一系列称为GC roots的对象作为起始点 ,从这些节点向下搜寻,搜寻走过的路径称为引用链,当一个对象到GC roots也就是起始点没有任何引用链时,表示该对象是可回收对象。
GC roots对象的4种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的变量
- 方法区中常量引用的变量
- 本地方法栈中引用的变量
三、Java的4种引用,引用强度依次减弱:
1.强引用
就是Object obj = new Object()这类引用,普遍存在。只要强引用还存在着,垃圾收集器永远不会回收被引用的对象。
2.软引用
使用SoftReference类实现,用来描述一些还有用但是非必需的对象,对于软引用关联着的对象,在系统抛出内存溢出异常之前,将软引用列进回收范围之中进行第二次回收。如果这次回收将软引用对象也回收了还没有内存空间,才会抛出内存溢出异常。
3.弱引用
使用WeakReference类实现,弱引用也是描述一些非必需的对象,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。
4.虚引用:
是最弱的一种引用,是否有虚引用存在完全不会影响对对象的生存构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。
四、垃圾收集算法(存活对象、可回收对象、未使用内存)
1.标记-清除算法
首先根据可达性分析算法标记出所有可回收对象,标记完成后统一回收所有被标记对象。是最基础的垃圾收集算法。但是有俩个缺点:1.效率较低,标记和清除俩个过程效率都较低。2.回收所有可回收对象后会产生大量的不连续内存碎片,导致后期如果要为大对象分配内存时,无法找到足够的连续内存只好提前触发另一次垃圾收集动作。
2.复制算法
将内存按照大小分为相等俩份,每次只使用其中一份。当使用的那份内存用完了,将所有存活对象统一都复制到另一块内存中,然后把所有已使用的内存空间一次性清理掉。优点:内存分配的时候不需要考虑内存碎片,直接移动指针按顺序分配内存。分配内存使用的是指针碰撞法。缺点:将内存缩小了一半。
使用复制算法来回收新生代。分配给新生代的对象具有朝花夕拾的特点,死亡率高。新生代的内存分为8:1:1的Eden区和survivor区。分配对象内存时每次使用其中的一块Eden区和survivor区。当回收时,将Eden区和survivor区的存活对象一次性复制到survivor区,然后清理掉Eden区和survivor区的所有内存。有可能出现Eden区和survivor区存活对象的内存大小大于survivor空间,survivor空间存活不够,这些对象会通过分配担保机制进入老年代。
3.标记-整理算法
老年代保存的对象是大对象,存活时间长。所以如果用复制算法需要大量复制操作,效率低。所以老年代对象使用标记-整理算法进行垃圾回收。和标记-清除算法一样,但是在清除阶段不是直接对可回收对象进行清除而是让所有存活对象向一端移动,直接清理掉端边界以外的内存。
4.分代收集算法
JVM使用的垃圾收集算法是分代收集算法,虚拟机根据对象存活周期将内存划分为新生代和老年代。新生代每次垃圾收集时总有大量对象死去,少量对象存活,所以使用复制算法。老年代对象存活率高,使用标记-整理算法进行回收。
第四章 垃圾收集器与GC日志理解
垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。新生代老年代都使用不同版本的垃圾收集器。
一、Serial垃圾收集器
- Serial收集器是一个单线程收集器,只使用一条收集线程使用复制算法收集可回收对象。更重要的是它进行垃圾收集时必须暂停其他所有的用户工作线程,直到收集结束,也就是stop the world。
- 是虚拟机在客户端模式下默认的新生代收集器。简单高效,由于没有线程切换的开销,在单个CPU环境下垃圾收集效率高。
二、ParNew收集器
- ParNew收集器是Serial收集器的多线程版本,使用多条收集线程使用复制算法收集可回收对象,同样在进行垃圾收集时必须暂停其他所有的用户工作线程,直到收集结束。
- ParNew收集器是Servier模式下的虚拟机首选的新生代收集器。单CPU下serial收集器没有线程切换开销收集效率高,随着CPU增多,ParNew的收集效率逐渐增高。
三、Parallel Scavenge收集器
- 是一个新生代收集器,使用复制收集算法,多条垃圾线程同时进行垃圾收集,暂停所有用户工作线程。追寻的是高吞吐量,吞吐量指的是(运行用户代码时间/运行用户代码时间+垃圾收集时间),高吞吐量表示程序运行速度快。而不是追寻尽可能地缩短垃圾收集时用户线程的停顿时间,用户线程停顿时间过长导致响应速度慢。
四、Serial Old收集器
• 是Serial收集器的老年代版本,是单线程收集器,只使用一条线程使用标记-整理算法回收老年代的可回收对象。具有stop the world的特性,暂停所有其他用户工作线程。在Client模式下给客户端使用。
五、Parallel Old收集器
- 是Parallel Scanvege的老年代版本,使用多线程并行进行垃圾收集,使用标记-整理算法,具有stop the world特性。
六、CMS(Concurrent Mark Sweep)收集器
CMS收集器是追寻尽可能缩短垃圾收集时用户线程停顿时间的收集器。用户线程和垃圾收集线程并发地执行,使得用户线程停顿时间缩小,系统响应快,用户体验好。
CMS收集器是基于“标记-清除”算法实现的,整个收集过程分为4个步骤:
- 初始标记:初始表示仅仅是标记一下GC roots对象可以直接关联到的对象,速度很快,需要“Stop the world”。
- 并发标记:进行GC roots Tracing的过程,将所有可回收对象标记出来,收集线程和用户工作线程同时执行。
- 重新标记:修正并发标记时,用户工作线程执行导致一些对象存活的变化。一般变化少,时间很短,需要“Stop the world”。
- 并发清除:并发地清除标记号的可回收对象。整个回收过程花费时间最长的是并发标记和并发清除,都是垃圾收集线程和用户工作线程一起并发执行,所以可以体现出:并发收集,低停顿的优点。
缺点:
- 对CPU资源敏感,因为用户工作线程和垃圾收集线程并发执行,垃圾收集线程占用了一定的cpu资源。
- CMS收集器在并发清除阶段用户工作线程并发执行,可能会产生浮动垃圾对象,本次的垃圾收集就无法回收。要是浮动垃圾对象过大,老年代没有空间分配,触发一次“Concurrent Mode Failure”失败,虚拟机需要启动后备预案:临时启动Serial Old 收集器回收老年代垃圾,停顿时间很长。
- 使用“标记-清除”算法容易尝试大量不连续的内存碎片。
主要注意GC日志里 GC发送的内存区域,新生代(Minor GC)还是老年代(Full GC)。注意GC内存区域,GC前内存大小,GC后内存大小
第五章 内存分配与回收策略
Java的自动内存管理实际就是给对象分配内存和回收分配给对象的内存。
1.Minor GC(新生代GC)
发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,对象死亡率高,且多数情况下对象分配到新生代,新生代内存很快不够用,所以minor GC非常频繁,回收的速度也很快。
2.Major GC(Full GC 老年代)
发生在老年代的垃圾收集动作,经常会伴随至少一次的Minor GC,一般回收速度比Minor GC慢10倍以上。
3.对象优先在Eden区分配
对象优先分配到Eden区,如果Eden区内存不够用时将触发Minor GC。
4.大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,比如很长的字符串对象或数组对象。大对象容易使虚拟机在内存还有不少空间但是没有足够连续内存时就触发垃圾收集动作。虚拟机提供了一个参数,令大于这个设置值的对象直接进入老年代,避免在Eden区和survivor区进行大量的存活对象复制。
5.长期存活的对象将进入老年代
虚拟机给每一个对象定义了一个对象年龄(Age)计数器,如果对象在Eden区出生并且经过一次MinorGC仍存活着,被移动到survivor区中,将对象的年龄设置为1。对象在survivor区每熬过一次Minor GC,年龄就增加1岁,当年龄增加到阈值,默认15岁,可以通过参数设置。对象就会进入到老年代。
6.动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到阈值才能晋升老年代,为了防止survivor区中存活的对象占用内存过多,导致minorGC时大量存活对象需要复制。如果在survivor区中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到阈值年龄。
7.空间分配担保(尽量少用Full GC)
当新生代的Eden区存活对象大小总和大于survivor区大小,存活对象就会通过分配担保机制进入老年代。在MinorGC之前,虚拟机先检查老年代最大可用的连续内存空间是否大于新生代对象大小总和。大于则表示MinorGC完全安全。如果条件不成立,则查看HandlePromotionFailure设置值是否允许担保失败,允许则检查老年代的最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC,如果小于或是设置不允许担保失败则进行一次Full GC。
第6章 类文件结构
一、为什么需要类文件(而不是直接编译成机器指令集)?
java具有“一次编写,到处运行”的特点。java代码通过java编译器编译成字节码,字节码被虚拟机加载解析机器指令集在cpu中执行。因为cpu和操作系统多种多样,所以要想运行与平台无关,java代码先编译成类文件.class文件,然后再用运行在不同平台下的各种虚拟机加载类文件到内存中,解析为适合该平台的机器指令集。
1.Class类文件结构
Class文件是一组以8字节为基础单位的二进制字节流,多个数据项目严格按照顺序紧凑地排列在Class文件中, Class文件中存储的内容几乎都是程序运行必要的数据。包含4个字节的魔数表示版本信息,常量池入口,存有常量字面量或是符号引用,指向类和接口的全限定名,字段名和描述符,方法名和描述符等信息。
第7章 虚拟机类加载机制
一、类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载机制。
1.类的生命周期
类从被加载到虚拟机内存开始到卸载出内存为止,生命周期包括:加载,验证,准备,解析,初始化,使用,卸载7个阶段。这些阶段通常是交叉混合地进行。
有且只有5中情况会对类进行初始化(初始化之前加载,验证,准备都要执行)
- 遇到new,putstatic ,getstatic,invokestatic等字节码指令时。比如new一个实例对象,比如读取或设置一个类的静态字段,调用类的静态方法都会触发类的初始化。
- reflect包的方法对类进行反射调用获取类信息时,需要先初始化类
- 初始化一个子类时,其父类还没有被初始化,先触发父类的初始化。
- 虚拟机启动时,需要初始化包含main方法的主类。 有些情况类不会被初始化:1.调用父类的静态方法或字段,类不会初始化。
2.调用某个类的常量(static final string)
该类不会初始化,因为常量在编译阶段会进入调用类的常量池中,调用类和被调用类之间编译成class文件之后没有任何关系。
二、类加载的过程(加载,验证,准备,解析,初始化)
- 加载阶段,虚拟机需要完成3件事:
- 通过一个类的全限定名来获取定义该类的二进制字节流。
- 将字节流代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口。
-
验证阶段是连接阶段的第一步,目的是为了确保加载的二进制字节流中包含的信息符合当前虚拟机的要求,不会导致虚拟机崩溃。 由于通过类的全限定名获取该类的二进制字节流,所以可以使用除了java源码编译的class文件的其他多种方式来加载类。 文件格式验证,字节码验证等。
-
准备阶段:正式为类变量(static修饰)分配内存并设置类变量初始值的阶段,类变量被分配到方法区中,初始值都设零值。
-
解析阶段:虚拟机将常量池中的符号引用替换为直接引用。符号引用指的是对类或接口全限定名,方法名称,字段名称的引用。符号引用引用的目标还未加载到内存中。当全限定名,方法名,字段名等信息在方法区中分配好内存时,符号引用被翻译替换成直接引用。直接引用存有的就是具体的内存地址。
-
初始化:给类变量,静态代码块里的变量赋值,赋值的是java代码里定义的初始值。
类加载器
类加载阶段加载时,“通过一个类的权限定名获取描述此类的二进制字节流”这个动作放到虚拟机外部实现,让应用程序自己决定如何获取需要的类,这个动作的代码叫做类加载器。 类加载器:类加载器用于实现类加载的动作,对于每一个类都需要类加载器和该类本身共同确立其在Java虚拟机中的唯一性。俩个类来源于同一个Class文件并且被同一个虚拟机加载,但是加载的类加载器不同,这俩个类必定不同。
双亲委派模型
类加载器可以分为3种:
- 启动类加载器(Bootstrap ClassLoader):使用C++语言实现,是虚拟机自身的一部分。这个类加载器负责将存放在\lib目录中的类库加载到虚拟机内存中。
- 扩展类加载器(Extension ClassLoader):独立于虚拟机外部,使用Java语言实现,负责加载\lib\ext目录下的类库。开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):负责加载用户类路径下所指定的类库,开发者可以直接使用这个类加载器。 类加载器的双亲委派模型:
- 类加载器之间的层次关系称为类加载器的双亲委派模型。双亲委派模型除了顶层的启动类加载器,其余的类加载器都有自己的父类加载器。加载器之间的关系通过组合(构造器传入引用)而不是继承来复用父类加载器的代码。
- 如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委托给父类加载器完成,每一个层次的类加载器都是这样。所以所有的类加载请求都传送到了顶层的启动类加载器中。,只有父类加载器在搜索范围内没有找到对应类反馈无法加载时,子加载器才会尝试加载。
好处:
Java类随着加载该类的类加载器一起具有优先级的层次关系,这样保证java类在什么情况下都只会被同一个类加载器所加载,保证了类在虚拟机里的唯一性。比如Java.lang.Object类由最顶层的启动类加载器加载,程序中又新建了一个Object类,在ClassPath里,如果没有双亲委派模型就会由应用程序类加载器加载。可能加载多个不同的Object类出现行为紊乱。因为使用了双亲委派模型,自己编写的Object类可以被编译,但是不会被类加载器加载。
第8章 虚拟机字节码执行引擎
虚拟机字节码执行引擎:解释执行字节码文件,输出执行结果
一、概述
1.运行时栈帧结构
虚拟机内存的运行时数据区中线程私有的虚拟机栈存有多个栈帧。每个栈帧包含局部变量表,操作数栈,动态链接和方法返回地址等信息。线程中一个方法的调用到执行完成的过程对应者一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧的大小在编译期已知,活动线程中,处于栈顶的栈帧才是有效的,称为当前栈帧,执行引擎的所有字节码指令针对当前栈帧进行操作。
2.局部变量表
- 用于存放方法参数和方法内部定义的局部变量。容量以变脸槽(slot)为最小单位。一个slot存放一个boolean,byte,int等基本数据类型或是reference引用数据类型。但是对于64位的long和double数据类型分配俩个连续的slot空间进行存储。局部变量定义了但没有赋初始值是不能使用的。
3.操作数栈
是一个后入先出的栈,操作数栈最大深度在编译期就写入到Class文件中,操作数栈每一个元素可以是任意的Java数据类型。方法执行的过程中会有各种字节码指令往操作数栈中写入和提取内容。比如整数相加iadd字节码指令会将栈顶的俩个整数提取出栈相加然后将结果入栈。
4.方法返回地址
方法退出有俩种:一种是遇到return字节码指令,返回值给上层的方法调用者。一种是异常退出,方法执行中遇到异常但是没有搜索到匹配的异常处理器,导致方法退出,不返回任何东西给上层调用者。栈帧中保存方法返回地址,用来恢复上层调用者的执行状态。
5.动态链接
是指一个指向方法区中运行时常量池中该栈帧所属方法信息的地址。
6.静态分派与动态分派的区别,重载与重写的区别
重载:
在同一个类中,多个方法拥有相同的名称,但是参数列表不相同,这些方法互相称为重载(overload)方法。
Human man = new Man();
- Human称为变量的静态类型,Man称为变量的实际类型。静态类型在编译期可知,类的信息按照静态类型写入到class文件中,实际类型在运行期才可以确定。
- 对象调用重载方法,如何定位方法名相同的方法执行版本呢?如果参数列表个数不同,依靠参数个数确定!如果参数个数相同,参数类型是父类子类关系,则通过静态分派来确定!所有依靠静态类型来定位方法执行版本称为静态分派。重载方法是在编译期确定的,不是由虚拟机执行的,依靠静态类型进行静态分派。
重写:是指子类和父类中有俩个名称、参数列表完全相同的情况。如果调用子类该方法,子类中的新方法会覆盖父类的方法。
- 重写与动态分派联系很密切。动态分派是指依靠对象的实际类型来定位方法执行版本,动态分派在运行期确定。
Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello();
- 如何确定调用的是哪个sayHello重写方法?通过调用对象的实际类型在运行期确定方法的执行版本。
多态:
父类引用指向子类对象,父类称为静态类型,编译期确定,子类称为实际类型,运行期确定。对象调用方法时会调用子类的重写实现,若子类未重写则调用继承得到的父类方法。