java JVM 内存结构
一、java虚拟机内存结构:
JVM是一种规范。
1.线程共享:堆、方法区
线程独享:虚拟机栈、本地方法栈、程序计数器
2、堆:是垃圾回收的主要区域
堆的内存又根据垃圾回收分为:
(1).新生代:Eden、S0(Survive from)、S1(Survice to)
(2).老年代
3、方法区:它存储了每一个类的结构信息
字节码被JVM保存在方法区,主要是分成:访问权限和类的属性(access_flags)、类索引父类索引和接口索引集合、常量池、字段表、方法表、和字段和方法结构中的属性表(方法的内容字节码就是存在属性表的code[]属性中)。 可以理解成Class的结构就是一张表,在执行的时候提供需要的信息
需要注意的一点是方法区是规范,在不同类型的虚拟机中采用不同的实现方法,hotspot在jdk 1.7包括1.7之前方法是永久代实现的(目的是为了不用在给方法区单独写内存管理的代码了),永久代在物理内存上和老年代连续,所以老年代或者永久带的垃圾回收会同时触发两个区域的垃圾回收,因为永久代的实现更容易使方法区产生内存溢出(因为永久代有maxPermSize的上限,而不用永久代实现的方法区只要在进程的最大内存范围内就不会内存溢出),在jdk1.7开始逐渐去永久代,jdk1.7把字符串常量池和静态变量放到了堆中, 在 jdk 1.8取消了永久代,是用元空间代替了永久带,最大区别就是方法区的物理空间和老年代的物理空间不再连续,和堆相对独立了。
运行时常量池是一个统称 也包括字符串常量池,但是字符串常量池放的只是字符串,而运行时常量池中,还包括类信息,属性信息,方法信息,以及其他基础类型的的常量池比如int,long等
jdk1.7之前,运行时常量池(包含着字符串常量池)都在方法区,具体的hotspot虚拟机实现为永久代
jdk1.7阶段,字符串常量池从方法区移到堆中,运行池常量池剩下的部分依旧在方法区(剩下类信息、属性信息、方法信息等),同样是hotspot中的永久代
jdk1.8, 方法区的实现从永久代变成了元空间,因此 字符串常量池依然在堆中,运行时常量池在方法区,hotspot中的元空间(metaspace)
运行时常量池:
有一篇文章有助于理解常量池:
[java]JVM之运行时常量池里到底有什么 - 简书1. 概念 首先我们来复习一下java内存模型,java运行时数据区大概分为五块,分别是 方法区 虚拟机栈 本地方法栈 堆 程序计数器 而运行时常量池是方法区的一部分,文字解...https://www.jianshu.com/p/614e2b6a0f22https://www.jianshu.com/p/614e2b6a0f22
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符 号引用则属于编译原理方面的概念,包括了下面三类常量:
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符
为啥要有这个常量池呢?
(1).Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟
机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段
的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正
的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的
符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
方法符号引用会被方法表中的那个方法结构的地址所替代,字段符号引用会被字段表中的那个字段结构所替代
(2).运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池
(3).因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。
字段表(field_info)、方法表(method_info)、属性表(attribute_info)会引用到常量池里的一些常量,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?
所以我理解的常量池的作用就是:提供一些常量(字面量),这些表用到这些常量时都引用一个地方,提供本Class文件中用到的字段和方法的符号引用(在jvm进行类加载的解析阶段这些符号引用会变成字段或者方法结构的地址),在使用的时候直接在常量池中找,这样就节省了内存空间。
在编译的时候所有在本Class文件中用到的字段和调用的方法(只要有方法调用就行例如上面的setAge中的getName方法),符号引用都会被保存在字节码的常量池中(包括其他类的也会存在自己的常量池表中),类加载后JVM会为每一个类都维护一个自己的常量池,在类加载的解析阶段,这些符号引用就被解析成了内存地址直接引用。
4、虚拟机栈:
虚拟机栈是线程独享的,和线程的生命周期一样,可以通过-Xss参数设置虚拟机栈的大小,hotspot默认是1M,不同的虚拟机默认值不同。
虚拟机栈越大是不是执行速度越快?
不是,虚拟机栈越大,递归或者循环执行方法的次数越大,不影响执行速度。虚拟机的内存大小是固定的,虚拟机栈越大代表着虚拟机可以同时工作的线程越少。
虚拟机栈存储当前线程运行方法所需的数据,指令、返回地址
运行时栈帧结构:
栈帧结构图:
栈帧的结构:
(1).局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。
public static void main(String[] args) throws Exception {byte[]placeholder=new byte[64*1024*1024];System.gc();}
代码很简单,即向内存填充了64MB的数据,然后通知虚拟机进行垃圾收集。我们在虚拟机运行参数中加上“-verbose:gc”来看看垃圾收集的过程,发现在System.gc()运行后并没有回收这64MB的内存,下面是运行的结果:
[GC 66846K->65824K(125632K),0.0032678 secs]
[Full GC 65824K->65746K(125632K),0.0064131 secs]
没有回收placeholder所占的内存能说得过去,因为在执行System.gc()时,变量placeholder还处于作用域之内,虚拟机自然不敢回收placeholder的内存。那我们把代码修改一下,变成代码清单8-2中的样子。
例 : 局部变量表Slot复用对垃圾收集的影响之二:
public static void main(String[] args) throws Exception {{byte[] placeholder = new byte[64 * 1024 * 1024];}System.gc();}
public static void main(String[] args) throws Exception {{byte[] placeholder = new byte[64 * 1024 * 1024];}int a = 0;System.gc();}
例:
(2).操作数栈
是执行引擎的一部分,有点像CPU的寄存器
拟机的实现里都会做一些优化处理, 令两个栈帧出现一部分重叠。 让下面栈帧的部分操作数
栈与上面栈帧的部分局部变量表重叠在一起, 这样在进行方法调用时就可以共用一部分数
据, 无须进行额外的参数复制传递如图:
(3).动态连接
每个栈帧都包含一个指向运行时常量池[1]中该栈帧所属方法的引用, 持有这个引用是为
了支持方法调用过程中的动态连接( Dynamic Linking) 。 我们知道Class
文件的常量池中存有大量的符号引用, 字节码中的方法调用指令就以常量池中指向方法的符
号引用作为参数。 这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接
引用, 这种转化称为静态解析。 另外一部分将在每一次运行期间转化为直接引用, 这部分称
为动态连接。
(4).完成出口,方法返回地址
记录上一个栈帧执行的代码行数,恢复现场
当一个方法开始执行后, 只有两种方式可以退出这个方法。 第一种方式是执行引擎遇到
任意一个方法返回的字节码指令, 这时候可能会有返回值传递给上层的方法调用者( 调用当
前方法的方法称为调用者) , 是否有返回值和返回值的类型将根据遇到何种方法返回指令来
决定, 这种退出方法的方式称为正常完成出口( Normal Method Invocation Completion) 。
另外一种退出方式是, 在方法执行过程中遇到了异常, 并且这个异常没有在方法体内得
到处理, 无论是Java虚拟机内部产生的异常, 还是代码中使用athrow字节码指令产生的异
常, 只要在本方法的异常表中没有搜索到匹配的异常处理器, 就会导致方法退出, 这种退出
方法的方式称为异常完成出口( Abrupt Method Invocation Completion) 。 一个方法使用异常
完成出口的方式退出, 是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式, 在方法退出之后, 都需要返回到方法被调用的位置, 程序才能
继续执行, 方法返回时可能需要在栈帧中保存一些信息, 用来帮助恢复它的上层方法的执行
状态。 一般来说, 方法正常退出时, 调用者的PC计数器的值可以作为返回地址, 栈帧中很可
能会保存这个计数器值。 而方法异常退出时, 返回地址是要通过异常处理器表来确定的, 栈
帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈, 因此退出时可能执行的操作有: 恢复
上层方法的局部变量表和操作数栈, 把返回值( 如果有的话) 压入调用者栈帧的操作数栈
中, 调整PC计数器的值以指向方法调用指令后面的一条指令等。
(5).方法调用
方法调用并不等同于方法执行, 方法调用阶段唯一的任务就是确定被调用方法的版本
( 即调用哪一个方法) , 暂时还不涉及方法内部的具体运行过程。 在程序运行时, 进行方法
调用是最普遍、 最频繁的操作, 但前面已经讲过, Class文件的编译过程中不包含传统编译中
的连接步骤, 一切方法调用在Class文件里面存储的都只是符号引用, 而不是方法在实际运行
时内存布局中的入口地址( 相当于之前说的直接引用) 。 这个特性给Java带来了更强大的动
态扩展能力, 但也使得Java方法调用过程变得相对复杂起来, 需要在类加载期间, 甚至到运
行期间才能确定目标方法的直接引用。
(6) 解析
继续前面关于方法调用的话题, 所有方法调用中的目标方法在Class文件里面都是一个常
量池中的符号引用, 在类加载的解析阶段, 会将其中的一部分符号引用转化为直接引用, 这
种解析能成立的前提是: 方法在程序真正运行之前就有一个可确定的调用版本, 并且这个方
法的调用版本在运行期是不可改变的。 换句话说, 调用目标在程序代码写好、 编译器进行编
译时就必须确定下来。 这类方法的调用称为解析( Resolution) 。
在Java语言中符合“编译期可知, 运行期不可变”这个要求的方法, 主要包括静态方法和
私有方法两大类, 前者与类型直接关联, 后者在外部不可被访问, 这两种方法各自的特点决
定了它们都不可能通过继承或别的方式重写其他版本, 因此它们都适合在类加载阶段进行解
析。
与之相对应的是, 在Java虚拟机里面提供了5条方法调用字节码指令, 分别如下。
invokestatic: 调用静态方法。
invokespecial: 调用实例构造器< init> 方法、 私有方法和父类方法。
invokevirtual: 调用所有的虚方法。
invokeinterface: 调用接口方法, 会在运行时再确定一个实现此接口的对象。
invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法, 然后再执行该方
法, 在此之前的4条调用指令, 分派逻辑是固化在Java虚拟机内部的, 而invokedynamic指令
的分派逻辑是由用户所设定的引导方法决定的。
只要能被invokestatic和invokespecial指令调用的方法, 都可以在解析阶段中确定唯一的
调用版本, 符合这个条件的有静态方法、 私有方法、 实例构造器、 父类方法4类, 它们在类
加载的时候就会把符号引用解析为该方法的直接引用。 这些方法可以称为非虚方法, 与之相
反, 其他方法称为虚方法( 除去final方法, 后文会提到)
Java中的非虚方法除了使用invokestatic、 invokespecial调用的方法之外还有一种, 就是被
final修饰的方法。 虽然final方法是使用invokevirtual指令来调用的, 但是由于它无法被覆盖,
没有其他版本, 所以也无须对方法接收者进行多态选择, 又或者说多态选择的结果肯定是唯
一的。 在Java语言规范中明确说明了final方法是一种非虚方法
解析调用一定是个静态的过程, 在编译期间就完全确定, 在类装载的解析阶段就会把涉
及的符号引用全部转变为可确定的直接引用, 不会延迟到运行期再去完成。 而分派
( Dispatch) 调用则可能是静态的也可能是动态的, 根据分派依据的宗量数可分为单分派和
多分派。 这两类分派方式的两两组合就构成了静态单分派、 静态多分派、 动态单分派、 动态
多分派4种分派组合情况, 下面我们再看看虚拟机中的方法分派是如何进行的
(7). 分派
众所周知, Java是一门面向对象的程序语言, 因为Java具备面向对象的3个基本特征: 继
承、 封装和多态。 本节讲解的分派调用过程将会揭示多态性特征的一些最基本的体现,
如“重载”和“重写”在Java虚拟机之中是如何实现的, 这里的实现当然不是语法上该如何写,
我们关心的依然是虚拟机如何确定正确的目标方法。
1.静态分派
在开始讲解静态分派[1]前, 笔者准备了一段经常出现在面试题中的程序代码, 读者不妨
先看一遍, 想一下程序的输出结果是什么。 后面我们的话题将围绕这个类的方法来重载
( Overload) 代码, 以分析虚拟机和编译器确定方法版本的过程
public class Test2{static abstract class Human{} static class Man extends Human{}static class Woman extends Human{}public void sayHello(Human guy) {System.out.println("hello,guy! ") ;}public void sayHello(Man guy) {System.out.println("hello,gentleman! ") ;}public void sayHello(Woman guy) {System.out.println("hello,lady! ") ;}public static void main(String[]args) {Human man=new Man() ;Human woman=new Woman() ;Test2 sr=new Test2() ;sr.sayHello(man) ;sr.sayHello(woman) ;}}
运行结果:
上面的代码实际上是在考验阅读者对重载的理解程度, 相信对Java编程稍有经
验的程序员看完程序后都能得出正确的运行结果, 但为什么会选择执行参数类型为Human的
重载呢? 在解决这个问题之前, 我们先按如下代码定义两个重要的概念。
Human man=new Man() ;
我们把上面代码中的“Human”称为变量的静态类型( Static Type) , 或者叫做的外观类型
( Apparent Type) , 后面的“Man”则称为变量的实际类型( Actual Type)或者叫运行时类型(Runtime type) , 静态类型和实际类型在程序中都可以发生一些变化, 区别是静态类型的变化仅仅在使用时发生(例如强转), 变量本身的静态类型不会被改变, 并且最终的静态类型是在编译期可知的; 而实际类型变化的结果在运行期才可确定, 编译器在编译程序的时候并不知道一个对象的实际类型是什么。 例如下面的
代码:
//实际类型变化Human human= (new Random().nextBoolean() ? new Man() : new Woman());//静态类型变化sr.sayHello((Man)human);sr.sayHello((Woman)human);
对象human的实际类型是可变的,编译期间不清楚它到底是Man还是Woman,必须等到程序运行到这行时才能确定。而human的静态类型是Human,也可以在使用时像上面这段代码一样强制转型临时改变这个类型,但这个改变仍在编译期可知。
解释了这两个概念, 再回到静态分派第一段代码中。 main( ) 里面的两次
sayHello( ) 方法调用, 在方法接收者已经确定是对象“sr”的前提下, 使用哪个重载版本, 就
完全取决于传入参数的数量和数据类型。 代码中刻意地定义了两个静态类型相同但实际类型
不同的变量, 但虚拟机( 准确地说是编译器) 在重载时是通过参数的静态类型而不是实际类
型作为判定依据的。 并且静态类型是编译期可知的, 因此, 在编译阶段, Javac编译器会根
据参数的静态类型决定使用哪个重载版本, 所以选择了sayHello( Human) 作为调用目标,
并把这个方法的符号引用写到main( ) 方法里的两条invokevirtual指令的参数中。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。 静态分派的典型应用
是方法重载。 静态分派发生在编译阶段, 因此确定静态分派的动作实际上不是由虚拟机来执
行的。 另外, 编译器虽然能确定出方法的重载版本, 但在很多情况下这个重载版本并不
是“唯一的”, 往往只能确定一个“更加合适的”版本。 这种模糊的结论在由0和1构成的计算机
世界中算是比较“稀罕”的事情, 产生这种模糊结论的主要原因是字面量不需要定义, 所以字
面量没有显式的静态类型, 它的静态类型只能通过语言上的规则去理解和推断。
何为更适合的版本:
public class Test2{public static void sayHello(Object arg) {System.out.println("hello Object") ;}public static void sayHello(int arg) {System.out.println("hello int") ;}public static void sayHello(long arg) {System.out.println("hello long") ;}public static void sayHello(Character arg) {System.out.println("hello Character") ;}public static void sayHello(char arg) {System.out.println("hello char") ;}public static void sayHello(char...arg) {System.out.println("hello char……") ;}public static void sayHello(Serializable arg) {System.out.println("hello Serializable") ;}public static void main(String[]args) {sayHello('a') ;}}
上面的代码运行后会输出:
这很好理解, 'a'是一个char类型的数据, 自然会寻找参数类型为char的重载方法, 如果
注释掉sayHello( char arg) 方法, 那输出会变为:
这时发生了一次自动类型转换, 'a'除了可以代表一个字符串, 还可以代表数字97( 字
符'a'的Unicode数值为十进制数字97) , 因此参数类型为int的重载也是合适的。 我们继续注释
掉sayHello( int arg) 方法, 那输出会变为:
这时发生了两次自动类型转换, 'a'转型为整数97之后, 进一步转型为长整数97L, 匹配
了参数类型为long的重载。 笔者在代码中没有写其他的类型如float、 double等的重载, 不过实
际上自动转型还能继续发生多次, 按照char-> int-> long-> float-> double的顺序转型进行匹
配。 但不会匹配到byte和short类型的重载, 因为char到byte或short的转型是不安全的。 我们继
续注释掉sayHello( long arg) 方法, 那输出会变为:
这时发生了一次自动装箱, 'a'被包装为它的封装类型java.lang.Character, 所以匹配到了
参数类型为Character的重载, 继续注释掉sayHello( Character arg) 方法, 那输出会变为:
这个输出可能会让人感觉摸不着头脑, 一个字符或数字与序列化有什么关系? 出现hello
Serializable, 是因为java.lang.Serializable是java.lang.Character类实现的一个接口, 当自动装箱
之后发现还是找不到装箱类, 但是找到了装箱类实现了的接口类型, 所以紧接着又发生一次
自动转型。 char可以转型成int, 但是Character是绝对不会转型为Integer的, 它只能安全地转
型为它实现的接口或父类。 Character还实现了另外一个接口java.lang.Comparable< Character
> , 如果同时出现两个参数分别为Serializable和Comparable< Character> 的重载方法, 那它
们在此时的优先级是一样的。 编译器无法确定要自动转型为哪种类型, 会提示类型模糊, 拒
绝编译。 程序必须在调用时显式地指定字面量的静态类型, 如: sayHello( ( Comparable<
Character> ) 'a') , 才能编译通过。 下面继续注释掉sayHello( Serializable arg) 方法, 输出会变为:
这时是char装箱后转型为父类了, 如果有多个父类, 那将在继承关系中从下往上开始搜
索, 越接近上层的优先级越低。 即使方法调用传入的参数值为null时, 这个规则仍然适用。
我们把sayHello( Object arg) 也注释掉, 输出将会变为:
7个重载方法已经被注释得只剩一个了, 可见变长参数的重载优先级是最低的, 这时候
字符'a'被当做了一个数组元素。 笔者使用的是char类型的变长参数, 读者在验证时还可以选
择int类型、 Character类型、 Object类型等的变长参数重载来把上面的过程重新演示一遍。 但
要注意的是, 有一些在单个参数中能成立的自动转型, 如char转型为int, 在变长参数中是不
成立的
上面的示例演示了编译期间选择静态分派目标的过程, 这个过程也是Java语言实现方法
重载的本质。 演示所用的这段程序属于很极端的例子, 除了用做面试题为难求职者以外, 在
实际工作中几乎不可能有实际用途。 笔者拿来做演示仅仅是用于讲解重载时目标方法选择的
过程, 大部分情况下进行这样极端的重载都可算是真正的“关于茴香豆的茴有几种写法的研
究”。 无论对重载的认识有多么深刻, 一个合格的程序员都不应该在实际应用中写出如此极
端的重载代码。
另外还有一点读者可能比较容易混淆: 笔者讲述的解析与分派这两者之间的关系并不是
二选一的排他关系, 它们是在不同层次上去筛选、 确定目标方法的过程。 例如, 前面说过,
静态方法会在类加载期就进行解析, 而静态方法显然也是可以拥有重载版本的, 选择重载版
本的过程也是通过静态分派完成的。
2.动态分派
public class TestF {static abstract class Human {protected abstract void sayHello();}static class Man extends Human {@Overrideprotected void sayHello() {System.out.println("man say hello");}}static class Woman extends Human {@Overrideprotected void sayHello() {System.out.println("woman say hello");}}public static void main(String[] args) {Human man = new Man();Human woman = new Woman();man.sayHello();woman.sayHello();man = new Woman();man.sayHello();}
}
运行结果:
这个运行结果相信不会出乎任何人的意料,对于习惯了面向对象思维的Java程序员会觉得这是完全理所当然的。现在的问题还是和前面的一样,虚拟机是如何知道要调用哪个方法的?
显然这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?我们使用javap命令输出这段代码的字节码,尝试从中寻找答案:
main()方法的字节码:
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new #2 // class com/example/fragmentadaptertest/testjava/TestF$Man3: dup4: invokespecial #3 // Method com/example/fragmentadaptertest/testjava/TestF$Man."<init>":()V7: astore_18: new #4 // class com/example/fragmentadaptertest/testjava/TestF$Woman11: dup12: invokespecial #5 // Method com/example/fragmentadaptertest/testjava/TestF$Woman."<init>":()V15: astore_216: aload_117: invokevirtual #6 // Method com/example/fragmentadaptertest/testjava/TestF$Human.sayHello:()V20: aload_221: invokevirtual #6 // Method com/example/fragmentadaptertest/testjava/TestF$Human.sayHello:()V24: new #4 // class com/example/fragmentadaptertest/testjava/TestF$Woman27: dup28: invokespecial #5 // Method com/example/fragmentadaptertest/testjava/TestF$Woman."<init>":()V31: astore_132: aload_133: invokevirtual #6 // Method com/example/fragmentadaptertest/testjava/TestF$Human.sayHello:()V36: returnLineNumberTable:line 26: 0line 27: 8line 28: 16line 29: 20line 30: 24line 31: 32line 32: 36LocalVariableTable:Start Length Slot Name Signature0 37 0 args [Ljava/lang/String;8 29 1 man Lcom/example/fragmentadaptertest/testjava/TestF$Human;16 21 2 woman Lcom/example/fragmentadaptertest/testjava/TestF$Human;
Human man = new Man();Human woman = new Woman();
public class TestF {public static class Father {public int money = 1;public Father() {money = 2;showMeTheMoney();}public void showMeTheMoney() {System.out.println("I am Father,I have $" + money);}}public static class Son extends Father {public int money = 3;public Son(){money = 4;showMeTheMoney();}public void showMeTheMoney() {System.out.println("I am Son,I have $" + money);}}public static void main(String[] args) {Father gay = new Son();System.out.println("This gay has $" + gay.money);}
}
执行结果:
输出两句都是"I am Son",这是因为Son类在创建的时候,首先隐式调用了Father的构造函数,而Father的构造函数中对showMeTheMoney()的调用是一次虚方法调用,实际执行的版本是Son的showMeTheMoney()方法,所以输出I am Son。而这时候虽然父类的money字段已经被初始化成2了,但Son的showMeTheMoney()方法中访问的却是子类的money字段,这时候结果自然还是 0,因为它要到子类的构造函数执行时才会被初始化。main的最后一句话通过静态类型(外观类型)访问到了父类中的money,输出了2
public class TestF {static class QQ {}static class _360 {}public static class Father {public void hardChoice(QQ arg) {System.out.println("father choose qq");}public void hardChoice(_360 arg) {System.out.println("father choose 360");}}public static class Son extends Father {public void hardChoice(QQ arg) {System.out.println("son choose qq");}public void hardChoice(_360 arg) {System.out.println("son choose 360");}}public static void main(String[] args) {Father father = new Father();Father son = new Son();father.hardChoice(new _360());son.hardChoice(new QQ());}
}
运行结果:
虚拟机在分派中“会做什么”这个问题。 但是虚拟机“具体是如何做到的”, 可能各种虚拟机的
实现都会有些差别。
方法元数据中搜索合适的目标方法, 因此在虚拟机的实际实现中基于性能的考虑, 大部分实
现都不会真正地进行如此频繁的搜索。 面对这种情况, 最常用的“稳定优化”手段就是为类在
方法区中建立一个虚方法表( Vritual Method Table, 也称为vtable, 与此对应的, 在
invokeinterface执行时也会用到接口方法表——Inteface Method Table, 简称itable) , 使用虚
方法表索引来代替元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。 如果某个方法在子类中没有被重写, 那子
类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的, 都指向父类的实现入
口。 如果子类中重写了这个方法, 子类方法表中的地址将会替换为指向子类实现版本的入口
地址。 Son重写了来自Father的全部方法, 因此Son的方法表没有指向Father类型数
据的箭头。 但是Son和Father都没有重写来自Object的方法, 所以它们的方法表中所有从
Object继承来的方法都指向了Object的数据类型
一样的索引序号, 这样当类型变换时, 仅需要变更查找的方法表, 就可以从不同的虚方法表
中按索引转换出所需的入口地址。
类的方法表也初始化完毕。
5、程序计数器:
就是记录要执行的下一条机器码的地址。再多线程切换的时候需要线程独享的程序计数器来记录当前线程需要执行的下一句机器码。
6.对象的创建和内存布局
a.对象的内存布局
对象的内存可以分为3块区域:对象头、实例数据、对齐填充(占位符为了达到8字节的整数倍)
hotspot虚拟机中对象头分为两部分:第一部分是用于存储对象自身的运行时数据例如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,这部分数据长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为"mark word"
第二部分是用于存储类的类元数据的指针:注意类元数据的指针不一定存在对象头中,它的存储方式取决于虚拟机在查找类元数据时的实现,一般有两种方式:一、在堆中划分出一个句柄池,栈中存储对象的句柄地址,句柄中存储类的实例和类元信息地址。 二、像hotspot虚拟机就是栈中直接存储对象实例的地址,在对象头中存储类元数据的地址。
java JVM 内存结构相关推荐
- 区分 JVM 内存结构、 Java 内存模型 以及 Java 对象模型 三个概念
本文由 简悦 SimpRead 转码, 原文地址 https://www.toutiao.com/i6732361325244056072/ 作者:Hollis 来源:公众号Hollis Java 作 ...
- Java 内存模型和 JVM 内存结构真不是一回事
这两个概念估计有不少人会混淆,它们都可以说是 JVM 规范的一部分,但真不是一回事!它们描述和解决的是不同问题,简单来说, Java 内存模型,描述的是多线程允许的行为 JVM 内存结构,描述的是线程 ...
- 【转】JVM内存结构 VS Java内存模型 VS Java对象模型
JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途. 其中有些区域随着虚拟机进程的启动而 ...
- JVM内存结构 VS Java内存模型 VS Java对象模型
Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚.比如本文我们要讨论的JVM内存结构.Java内存模型和Java对象 ...
- java多线程构造函数_java线程基础巩固---多线程与JVM内存结构的关系及Thread构造函数StackSize的理解...
多线程与JVM内存结构的关系[了解]: 对于最后一个有疑问的构造中stackSize参数,其实学过编程滴人从参数字面就比较容易理解,栈大小嘛,这里从官方文档上来了解一下这个参数: 而之前在学习java ...
- java 堆内存结构_基于JDK1.8的JVM 内存结构【JVM篇三】
在我的上一篇文章别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析[JVM篇二]中,相信大家已经对java类加载机制有一个比较全面的理解了,那么类加载之后,字节码数据在 ...
- 学习笔记:Java虚拟机——JVM内存结构、垃圾回收、类加载与字节码技术
学习视频来源:https://www.bilibili.com/video/BV1yE411Z7AP Java类加载机制与ClassLoader详解推荐文章:https://yichun.blog.c ...
- java:JVM内存结构初步理解入门:堆、栈、方法区(浅显通俗易懂自记)
自己整理的一些资料以及自己的一些理解,希望记下来的不是高大上而晦涩的概念,将自己此时此刻的理解最大程度地刻模,以便将来重温知新. JVM内存结构最简单可以初步分为:1栈 2堆 3方法区 4 程序计数器 ...
- 快速带你分清java内存结构,java内存模型,java对象模型和jvm内存结构!
现如今你是否有这样的感觉,无论生活还是学习,节奏都是非常的快,每天面对海量的知识信息,自己感觉都要hold不住了,每天打开微信公众号,是不是发现有几十条未读,无论是技术文章还是其他类型的文章,我们大多 ...
最新文章
- python pip 错误 ModuleNotFoundError: No module named pip._internal 解决办法
- pandas mysql index_Pandas从入门到精通(3)- Pandas多级索引MultiIndex
- 2021年度公有云安全报告
- ExtJS中listener方法和handler方法的区别
- This is the default error page for nginx that is distributed with EPEL.
- sublime text3 常用配置
- Eclipse创建Java项目时提示Open Associated Perspective?
- JavaBridge install in ubuntu
- 通过I2C总线向EEPROM中写入数据,记录开机次数
- 在线渐变配色网站分享
- VS(visual studio)中使用ReportViewer控件和报表设计器 RDLC
- 什么是雷曼时刻(Lehman Moment)
- WebRTC语音对讲无声音
- selenium中的三种等待方法
- w ndows10摄像头设置,windows10系统电脑摄像头怎么打开
- 瑞吉外卖项目的购物车sub操作
- 2023年,如何管理你的绩效目标?
- 如何剪切视频,只截取视频中间的一部分
- 入门 | 我们常听说的置信区间与置信度到底是什么?
- python 斗破苍穹 词云
热门文章
- AAAI 2020 提前看 | 三篇论文解读问答系统最新研究进展
- 【STC15控制WS2812 RGB彩灯级联】
- Spring学习(二)—— 对象创建方式及依赖注入
- A53开发板命令操作wifi-wap主要命令
- 谷歌翻译服务退出中国大陆,使用SwitchyOmega仍需要全文翻译,恢复访问的方法
- Vivado 2020.1 and 2020.2 错误 arm-none-eabi-ar: *.o: Invalid argument
- Ubuntu安装ros rotors 以及中间出现的问题的解决办法
- Linux服务器批量管理工具 - TeamRemote
- Android 项目接入网易云信IM单聊,群聊
- 猿人学试题(非常简单js混淆、雪碧图、样式干扰 css加密、js混淆源码乱码、js混淆动态cookie、访问逻辑)