整理:JVM基本结构

JVM基本结构

      • JRE、JDK、JVM之间的关系
      • 编译和运行过程
        • JVM加载class文件图解
      • JVM基本结构
        • 1. 类加载器子系统
        • 2. 运行时数据区
          • 2.1 方法区
            • 2.1.1常量池
            • 2.1.1.1 class常量池(class constant pool)
            • 2.1.1.2 运行时常量池
            • 2.1.1.3 全局字符串池(string pool也有叫做string literal pool)
          • 2.1.2 元数据空间:
            • JVM中OOP-KLASS模型
          • 2.2 堆内存
          • 2.4 程序计数器
          • 2.5 本地方法栈
      • 类加载机制
        • 一、类加载过程
        • 二、类加载时机
        • 三、类加载器
        • 四、类加载机制:
      • 扩:多态重载为什么static方法没有重载?
        • Java 的方法调用方式
        • Answer:为什么static方法没有重载?
        • 常量池(constant pool)
          • 常量池各表的关系
        • 实例:
          • Boy 和 Girl 的方法表
          • 多态调用
        • 实例二:[反编译出代码偏移量来理解多态](https://blog.csdn.net/u011552404/article/details/80081405)
          • 静态方法和实例方法加载时的相同点
          • [ 扩:当子类和父类的返回值不一致时, 会在子类中产生一个桥接方法](https://blog.csdn.net/jiaobuchong/article/details/83722193)
          • 扩:JVM是如何执行方法调用的


这个总结也很好,14年写的jdk版本1.7之前,但是不妨碍我偷图:Java虚拟机原理图解
Java JVM 底层机制,类加载器、堆、栈、方法区等详解

JRE、JDK、JVM之间的关系

JVM(Java Virtual Machine) : Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心。
java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳。它会装载jvm.dll(windows下),这个动态连接库才是java虚拟机的实际操作处理所在。java.exe程序只负责查找和装载jvm.dll动态库,并调用它进行class文件执行处理。
“java”命令与“javac”命令其实都是launcher,负责启动JVM并把启动参数传给JVM而已。

JRE(Java Runtime Enviroment):
Java运行时环境。面向Java程序的使用者,而不是开发者。JRE是运行Java程序所必须环境集合,包含JVM标准实现及Java核心库。它包括Java虚拟机、Java平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

JDK(Java Development Kit):
Java开发工具包,它提供了Java的开发环境(编译器javac等工具,用于将java文件编译成class文件)和运行环境(包括JVM和Runtime辅助包,用于解析class文件使其运行)。如果安装了JDK,那么不仅拥有了Java开发环境,也拥有了运行Java程序的平台。实际上JDK=开发工具tools+JRE+标准类库。

编译和运行过程

//MainApp.java
public class MainApp {
    public static void main(String[] args) {
        Animal animal = new Animal("Puppy");
        animal.printName();
    }
}
//Animal.java
public class Animal {
    public String name;

    public Animal(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println("Animal [" + name + "]");
    }
}
  1. 在编译好java程序得到MainApp.class文件后,在命令行上敲java
    MainApp。系统就会启动一个jvm进程,(由java命令启动jvm,jvm启动就相当于启动了一个进程)。 jvm
    进程从classpath路径中找到一个名为MainApp.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp
    类的加载 。

  2. 加载完MainApp类之后,Java虚拟机做的第一件事情就是在区中为一个新的MainApp实例分配内存,然后调用构造函数初始化MainApp实例,这个MainApp
    实例持有着指向方法区
    MainApp类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用(直接引用)。最后会把堆内存中MainApp的地址给中MainApp。

  3. 然后JVM进程找到AppMain的主函数入口,JVM进程 创建一个主线程,并为这个线程创建虚拟机栈
    去调用main方法,将方法压入虚拟机栈中。

  4. main函数的第一条命令是Animal animal = new
    Animal(“Puppy”);就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中

  5. 加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存,然后调用构造函数初始化Animal实例,这个Animal
    实例持有着指向方法区的 Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用(直接引用)。最后会把堆内存中Animal的地址给栈中animal

  6. 当使用animal.printName()的时候,JVM根据animal 引用
    找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数字节码的地址,方法的字节码组建一个栈帧,压入到主线程的虚拟机栈中。

  7. 开始运行printName()函数的字节码(可以把字节码理解为一条条的指令),运行后printName()函数的栈帧弹出。

  8. main函数栈帧弹出

  9. 垃圾回收器工作时对堆内存进行回收

  10. jvm进程结束。

JVM加载class文件图解

原文:java代码字节码执行时编译成机器码 以及虚拟机栈
一次编译,到处运行
整理:JVM基本结构_第1张图片
java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。
Windows下jvm启动Java程序过程
整理:JVM基本结构_第2张图片
java代码通过编译之后生成字节码文件(class文件),通过:java HelloWorld执行,此时java根据系统版本找到jvm.cfg。通过jvm.cfg文件找到对应的jvm.dll,jvm.dll则是java虚拟机的主要实现。接下来会初始化JVM,并且获取JNI接口,通过JNI接口(它还常用于java与操作系统、硬件交互),从硬盘上找到这个class文件后并装载进JVM,然后找到main方法,最后执行。

JVM基本结构

整理:JVM基本结构_第3张图片

java8内存模型图

整理:JVM基本结构_第4张图片

1. 类加载器子系统

在JVM中负责加载.class文件。

2. 运行时数据区

整理:JVM基本结构_第5张图片

2.1 方法区

整理:JVM基本结构_第6张图片

类加载器(ClassLoader),方法区结构,堆中实例对象结构的详解
方法区是对于虚拟机的规范。类加载时,通过类加载器(ClassLoader)去主机硬盘上将A.class二进制文件的字符流转换成字节码,装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader。这样方法区会记录一个class自己的class对象引用和一个加载自己的ClassLoader引用。方法区存放的内容还包括被虚拟机加载的类信息(名称、方法信息、字段信息)、常量、静态变量(static修饰的,非static的叫做实例变量),编译器编译后的代码。

Java8, HotSpots使用元数据空间代替了永久代去实现方法区
整理:JVM基本结构_第7张图片

2.1.1常量池

jvm(常量池,线程栈),拆装箱角度分析==
字符串常量池、class常量池和运行时常量池
JVM规范中并没有常量池这一说法,都是各种不同的jvm实现为了便于处理加以区分的,常量池中运行时常量池是方法区的一部分
详细介绍:太深了常量池元素结构和作用关系详解
整理:JVM基本结构_第8张图片

字面量,符号引用,直接引用

  1. 字面量

字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。

java int i = 1; String s = "abc";
把整数1赋值给int型变量i,整数1就是Java字面量
abc也是字面量。

  1. 符号引用
    符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可.

例如, 在Java中, 一个Java类将会编译成一个class文件. 在编译时, Java类并不知道所引用的类的实际地址,
因此只能使用符号引用来代替. 比如org.simple.People类引用了org.simple.Language类,
在编译时People类并不知道Language类的实际内存地址,
因此只能使用 符号 org.simple.Language来表示Language类的 地址.

  1. 直接引用
    直接引用可以是:
    直接指向目标的指针.(指向方法区中类对象, 类变量和类方法的指针)
    相对偏移量. (指向实例的变量, 方法的指针)
    一个间接定位到对象的句柄.
2.1.1.1 class常量池(class constant pool)

当java文件被编译成class文件之后,就会生成class常量池。当 .java文件被转译成.class文件之后的字节码包含的一系列描述信息、符号引用和字面量信息在jvm启动时,会被加载到class常量池中。所谓的class常量池并不会真的需要分配一个内存空间(常量池),直接从本地磁盘上加载转换也是可行的,这主要取决与JVM的版本和一些参数的配置处理。

2.1.1.2 运行时常量池

而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中:运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Tabel),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入该常量池。运行时常量池并不仅仅局限于加载类时产生常量,可以在运行期间添加各种数据到这个区域,例如jvm会将代码中直接声明的字符串放置到常量池中,这些字符串被称为字面量。

2.1.1.3 全局字符串池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值也就是符号引用存到全局字符串池中(记住:全局字符串池中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

String str = "abc"的内部工作: (1)先定义一个名为str的对String类的对象引用变量:String str;
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o,o的引用存放在字符串常量池中。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。

2.1.2 元数据空间:
JVM中OOP-KLASS模型

OOP-Klass模型
java对象在jvm中的模型是OOP-Klass 模型;即:
1.jvm在加载class时,会创建instanceKlass,表示其元数据,包括常量池、字段、方法等,存放在方法区;instanceKlass是jvm中的数据结构;
2.在new一个对象时,jvm创建instanceOopDesc,来表示这个对象,存放在堆区,其引用,存放在栈区;它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象;instanceOopDesc对应java中的对象实例;
3.HotSpot并不把instanceKlass暴露给Java,而会另外创建对应的instanceOopDesc来表示java.lang.Class对象,并将后者称为前者的“Java镜像”,klass持有指向oop引用(_java_mirror便是该instanceKlass对Class对象的引用);
4.要注意,new操作返回的instanceOopDesc类型指针指向instanceKlass,而instanceKlass指向了对应的类型的Class实例的instanceOopDesc;有点绕,简单说,就是Person实例——>Person的instanceKlass——>Person的Class。

  • 元数据—— instanceKlass 对象会存在元空间(方法区)。klass对应元数据,包括常量池、字段、方法等。是在加载class阶段创建instanceKlass。

  • 对象实例—— instanceOopDesc 会存在Java堆。Java虚拟机栈中会存有这个对象实例的引用。instanceOopDesc,只包含数据信息,它包含三部分:

  1. 对象头,也叫Mark Word,主要存储对象运行时记录信息,如hashcode, GC分代年龄,锁状态标志,线程ID,时间戳等;
  2. 元数据指针,即指向方法区的instanceKlass实例 (虚拟机通过这个指针来群定这个对象是哪个类的实例。)
  3. 实例数据;
  4. 另外,如果是数组对象,还多了一个数组长度

整理:JVM基本结构_第9张图片
OOP-Klass 模型实例

class Model
{
    public static int a = 1;
    public int b;

    public Model(int b) {
        this.b = b;
    }
}

public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}

整理:JVM基本结构_第10张图片

metaspace的组成

metaspace其实由两大部分组成

  • Klass Metaspace

1.用来存klass的(class文件在jvm中运行时的数据结构);

2.一个紧接着Heap的连续的存储空间;

3.这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这时候Klass信息存在NoKlass Metaspace中。

  • NoKlass MetaSpace:

1.专门来存klass相关的其他的内容,比如method,constantPool等;

2.这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的;

3.NoKlass Mestaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。

Metaspace内存管理
在metaspace中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。
每个加载器有单独的存储空间。
省掉了GC扫描及压缩的时间。
当GC发现某个类加载器不再存活了,会把对应的空间整个回收。

2.2 堆内存

作用:
Java程序在运行时创建的所有类型对象和数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。

堆区中不存放基本数据和对象引用,只存放对象本身
每个对象包含一个与之对应的class信息–class的目的是得到操作指令
堆的优势时可以动态地分配内存大小,生存期也不必告诉编译器,Java垃圾回收器会自动收走不用的对象
缺点时由于在运行时动态分配内存,存取速度较慢
堆的大小是可扩展的,通过-Xms(设置堆内存初始大小)和-Xmx(设置堆内存最大值)控制。如果在堆中没有内存完成实例分配,并且也无法再扩展时,将会抛OutOfMemoryError异常
2.3 虚拟机栈
整理:JVM基本结构_第11张图片
整理:JVM基本结构_第12张图片

作用:
每启动一个线程,JVM都会为它分配一个Java每启动一个栈,用于存放方法的局部变量,操作数以及异常数据等。线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧。栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进…F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
遵循“先进后出”/“后进先出”原则。

-每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中 。
-每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
-栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

2.4 程序计数器

作用:
用于保存当前线程执行的内存地址。

由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先的状态,就要一个独立的计数器,记录之前中断的地方,可见程序计数器是线程私有的。

2.5 本地方法栈

和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个栈也有着C栈这一称号。


类加载机制

jvm之java类加载机制和类加载器(ClassLoader)的详解
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
整理:JVM基本结构_第13张图片

一、类加载过程

1.加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
通过网络加载class文件。
把一个Java源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

2.链接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

四种验证做进一步说明:

文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

3)解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

3.初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

二、类加载时机

创建类的实例,也就是new一个对象
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(Class.forName(“com.lyj.load”))
初始化一个类的子类(会首先初始化子类的父类)
JVM启动时标明的启动类,即文件名和类名相同的那个类
除此之外,下面几种情形需要特别指出:

 对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
三、类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

类加载器加载Class大致要经过如下8个步骤:

检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
从文件中载入Class,成功后跳至第8步。
抛出ClassNotFountException异常。
返回对应的java.lang.Class对象。

四、类加载机制:

1.JVM的类加载机制主要有如下3种。

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
2.这里说明一下双亲委派机制:
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:
可以避免重复加载,当父类已经加载了,则就子类不需再次加载;
安全因素,如果不用这种,则用户可以随意的自定义加载器来替代Java核心API,则就会带来安全隐患。
2.3 类加载实例的过程
当在命令行下执行:java HelloWorld(HelloWorld是含有main方法的类的Class文件),JVM会将HelloWorld.class加载到内存中,并在堆中形成一个Class的对象HelloWorld.class。
基本的加载流程如下:
1)寻找jre目录,寻找jvm.dll,并初始化JVM
2)产生一个Bootstrap Loader(启动类加载器)
3)Bootstrap Loader,该加载器会加载它指定路径下的Java核心API,并且再自动加载Extended Loader(标准扩展类加载器),Extended Loader会加载指定路径下的扩展JavaAPI,并将其父Loader设为BootstrapLoader
4)Bootstrap Loader也会同时自动加载AppClass Loader(系统类加载器),并将其父Loader设为ExtendedLoader
5)最后由AppClass Loader加载CLASSPATH目录下定义的类,HelloWorld类

扩:多态重载为什么static方法没有重载?

这里首先需要了解两个概念:一个是方法调用的方式,二是java是如何通过常量池的内容找到需要调用的方法的。

Java 的方法调用方式

在jvm中,我们有5种方法调用的指令,分别是:

invokestatic:调用静态方法;

invokespecial:调用实例构造方法,私有方法和父类方法,以及使用super关键字调用父类的实例方法或构造器;

invokevirtual:调用虚方法(非私有实例方法);

invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象;

invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法(jdk1.8lamada表达式);

这里,我们简单介绍一下这几种指令,对于invokestatic指令和invokespecial指令而言,java虚拟机能够直接识别目标方法,也就是我们所说的静态绑定。

invokevirtual和invokeinterface指令则需要在执行的过程中才能找到目标方法,也就是我们所说的动态绑定。

总结一下静态绑定和动态绑定的概念就是:

静态绑定:在程序执行之前就已经被绑定、也就是说再编译阶段就已经知道这个方法是属于哪个类的方法。

1.private修饰的方法,不能被子类调用 2. 被final修饰的方法 3.被static修饰的方法

动态绑定:在运行过程中根据调用者的动态类型来识别目标方法的情况。

动态绑定中,我们会记录方法对应的实际引用的地址,也可以理解为索引值,这里我们把它叫做方法表

方法表使用了数组的数据结构,每个数组元素指向了当前类以及其祖先类中非私有的实例方法。

Answer:为什么static方法没有重载?

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。对于可以静态绑定的方法调用而言,通过符号引用解析出的实际引用是一个指向方法的指针。Java虚拟机中的静态绑定指的是解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别方法的情况。

其实在这里已经可以理解静态方法和普通方法的多态区别,即静态方法通过invokestatic父类直接进行了静态解析,而普通方法是在invokevirtual之后还要去动态寻找方法的实例,然后下面举例说明普通方法的动态解析过程。

常量池(constant pool)

常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。

常量池在逻辑上可以分成多个表,每个表包含一类的常量信息,本文只探讨对于 Java 调用相关的常量池表。
java多态与常量池之间关系

如下就是class文件中的符号引用
CONSTANT_Utf8_info

字符串常量表,该表包含该类所使用的所有字符串常量,比如代码中的字符串引用、引用的类名、方法的名字、其他引用的类与方法的字符串描述等等。其余常量池表中所涉及到的任何常量字符串都被索引至该表

CONSTANT_Class_info

类信息表,包含任何被引用的类或接口的符号引用,每一个条目主要包含一个索引,指向 CONSTANT_Utf8_info 表,表示该类或接口的全限定名。

CONSTANT_NameAndType_info

名字类型表,包含引用的任意方法或字段的名称和描述符信息在字符串常量表中的索引。

CONSTANT_InterfaceMethodref_info

接口方法引用表,包含引用的任何接口方法的描述信息,主要包括类信息索引和名字类型索引。

CONSTANT_Methodref_info

类方法引用表,包含引用的任何类型方法的描述信息,主要包括类信息索引和名字类型索引。

常量池各表的关系

整理:JVM基本结构_第14张图片
可以看到,给定任意一个方法的索引,在常量池中找到对应的条目后,可以得到该方法的类索引(class_index)和名字类型索引 (name_and_type_index), 进而得到该方法所属的类型信息和名称及描述符信息(参数,返回值等)。注意到所有的常量字符串都是存储在 CONSTANT_Utf8_info 中供其他表索引的。

实例:
//Person
 class Person { 
 public String toString(){ 
    return "I'm a person."; 
	 } 
	 public void eat(){} 
 	 public void speak(){} 
	
 } 
//Boy
 class Boy extends Person{ 
 	public String toString(){ 
   		 return "I'm a boy"; 
	 } 
 	public void speak(){} 
 	public void fight(){} 
 } 
//Girl
 class Girl extends Person{ 
 	public String toString(){ 
    	return "I'm a girl"; 
	 } 
 	public void speak(){} 
	public void sing(){} 
 }
Boy 和 Girl 的方法表

整理:JVM基本结构_第15张图片

可以看到,Girl 和 Boy 的方法表包含继承自 Object 的方法,继承自直接父类 Person 的方法及各自新定义的方法。注意方法表条目指向的具体的方法地址,如 Girl 的继承自 Object 的方法中,只有 toString() 指向自己的实现(Girl 的方法代码),其余皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法实现和本身的实现。

多态调用
 class Party{void happyHour(){ 
 		Person girl = new Girl(); 
	 	girl.speak();} 
 }

调用图解:
整理:JVM基本结构_第16张图片
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。对于可以静态绑定的方法调用而言,通过符号引用解析出的实际引用是一个指向方法的指针。Java虚拟机中的静态绑定指的是解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别方法的情况。

JVM 首先查看 Party 的常量池索引为 12 的条目(应为 CONSTANT_Methodref_info 类型,可视为方法调用的符号引用),进一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要调用的方法是 Person 的 speak 方法(注意引用 girl 是其基类 Person 类型),查看 Person 的方法表,得出 speak 方法在该方法表中的偏移量 15(offset),这就是该方法调用的直接引用

当解析出方法调用的直接引用后(方法表偏移量 15),JVM 执行真正的方法调用:根据实例方法调用的参数 this 得到具体的对象(即 girl 所指向的位于堆中的对象),据此得到该对象对应的方法表 (Girl 的方法表 ),进而调用方法表中的某个偏移量(父类和子类的偏移量是一样的吗 )所指向的方法(Girl 的 speak() 方法的实现)。

实例二:反编译出代码偏移量来理解多态
静态方法和实例方法加载时的相同点

当第一次使用类时,JVM会通过类加载器,加载类对象,从而初始化静态属性,并装入类的方法,包括静态方法和实例方法(方法不会被调用,只是加载,从这个意义上来说,静态方法和实例方法是类似的)。

扩:当子类和父类的返回值不一致时, 会在子类中产生一个桥接方法
扩:JVM是如何执行方法调用的

你可能感兴趣的:(java,jvm,java)