JVM栈帧内部结构-方法返回地址

方法返回地址(Return Address):

存放调用该方法的PC寄存器的值。

方法结束方式:

1) 正常结束
2) 出现未处理异常,非正常退出(通过异常完成出口退出的不会给他的上层调用者生产任何的返回值

无论通过哪种方式退出,在方法退出后到该方法被调用的位置,方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令地址,而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

Return:

        当一个方法开始执行后,执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,称正常完成出口

        字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short、int类型时使用)lreturn、freturn、dreturn、areturn(return指令供声明void的方法、实例初始化方法、类和接口的初始化方法使用)。

JVM栈帧内部结构-动态链接

动态链接(或运行时常量池的方法引用):

        每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令

        在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在class文件的常量池里。

比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换位调用方法的直接引用

方法的调用:

在Jvm中,将符号引用转换位调用方法的直接引用,与方法的绑定机制相关。

静态链接:

        当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变。

        将调用方法的符号引用转换为直接引用的过程称为静态链接

动态链接:

        被调用的目标方法在编译期无法被确定下来,只能够在程序运行期将方法的符号引用转换为直接引用,这种引用转换的过程具备动态性,称为动态链接

方法的绑定机制分为早期绑定(Early Binding)晚期绑定(Late Bingind)。绑定是一个字段、方法或类在符号引用被替换为直接引用的过程。

早期绑定:

被调用的目标方法在编译期可知,且运行保持不变。

晚期绑定:

        被调用方法在编译期无法被确定下来,只能够在程序运行期根据实际类型绑定相关的方法。

虚方法与非虚方法:

        如果方法在编译期就确定具体调用版本,这个版本在运行期间是不可变的。

        静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其它方法都为虚方法

虚拟机中方法调用指令:

普通调用指令:

1) invokestatic:调用静态方法,解析阶段确定唯一方法版本。
2) invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本。
3) invokevirtual:调用所有虚方法。
4) invokeinterface:调用接口方法。

动态调用指令:

5) invokedynamic:动态解析出需要调用的方法,然后执行

invokeinterface:固化在虚拟机内部,方法的调用执行不可人为干预。
invokedynamic:指令支持用户确定方法版本。
invokestatic:指令和invokespecial指令调用的方法称为非虚方法,其余(final修饰除外)称为虚方法

JVM栈帧内部结构-操作数栈

基本概念:

        操作数栈是基于数组的方式实现的。

        在方法执行过程中,根据字节指令,往栈中写入(入栈/push)数据或提取(出栈/pop)数据。

        某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈。(比如:复制,交换,求和等操作)

        操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时存储空间。

        操作数栈是Jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

        每一个操作数栈都会拥有一个明确的栈深度用于存储数据,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
        栈中任何一个元素都是可以任意的Java数据类型
            1) 32bit的类型占用一个单位深度。
            2) 64bit的类型占用两个栈单位深度。
        操作数栈并非采用访问索引的方式进行数据访问的,只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

        如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
        
        操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

操作数栈字节码指令执行分析:

栈顶缓存(Top-Of-Stack Cashing)技术

        由于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作时,需要更多的入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数。

        操作数是存储在内存中的,频繁执行内存读/写操作影响执行速度,HotSpot提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

JVM栈帧内部结构-局部变量表

栈帧内部结构:

1) 局部变量表(Local Variables)
2) 操作数栈(Operand Stack)或表达式栈
3) 动态链接(Dynamic Linking)或指向运行时常量池的方法引用
4) 方法返回地址(Return Address)或方法正常退出或异常退出的定义
5) 一些附加信息

局部变量表(Local Variables):

    局部变量表被称之为局部变量数组或本地变量表。

    定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用(Reference)以及Return Address类型。

    由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题。

    局部变量表所需的容量大小是在编译器确定下来的,并保存在方法Code属性的Maximum local variables数据项中,在方法运行期间是不会改变局部变量表的大小。

    方法嵌套调用的次数由栈的大小决定,栈越大方法调用的次数越多。

    局部变量表中的变量只在当前方法调用中有效,在执行方法时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当前方法调用结束后,随着方法栈帧的销毁,局部变量表也随之销毁

Slot(槽)的理解:

    参数值的存放是在局部变量数组的index 0开始的,到数组长度-1的索引结束。

    局部变量表的基本存储单猥是Slot(变量槽)

    局部变量表中存放编译器可知的各种基本数据类型(8种),引用类型(reference),Return Address类型的变量。

    在局部变量表中,32位以内的类型占用一个Slot(包括Return Address类型),64位类型(long和double)占用两个Slot
    1) byte,short,char在存储前被转换位int,boolean也被转换位int(0表示false,非0表示true)。
    2) long和double占据两个Slot




    Java会位局部变量表中的每一个Slot都会分配一个访问索引,通过这个索引可以访问到局部变量表中指定的局部变量值。
    当一个实例方法被调用的时候,它的方法参数和方法体内部的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
    如果需要访问局部变量表的一个64bit的局部变量值时,只需要使用前一个索引即可。
    如果当前方法是由构造方法或实例方法创建的(非静态方法),那么该对象引用this将会存放在index为0的Slot处,其余参数按照参数表顺序继续排列。




Slot的重复利用:
    栈帧的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量就很有可能会重复用过期局部变量的槽位,达到节省资源的目的

静态变量与局部变量对比:

变量分类(按数据类型区分):1) 基本数据类型   2) 引用数据类型
在类中声明位置区分:
    1) 成员变量:在使用前,都经历过默认初始化赋值,
       类变量:在链接(Linking)的准备(Prepare)阶段,给类变量默认赋值,初始化(initialization)阶段给类变量显式赋值,即静态代码块赋值
    2) 局部变量:在使用前,必须进行显式赋值,否则编译不通过

局部变量表与GC垃圾回收关系:

 

在方法执行时,虚拟机使用局部变量表完成方法的传递。

      局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

JVM运行时数据区-虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack):

基本概述:

      跨平台设计,Java指令根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的

      线程私有的,每个线程创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),一个栈帧对应一个Java方法。

内存中的栈与堆的区别:

    堆:存储数据区域
    栈:运行时数据区域


栈与堆数据存放区别:
   堆:主体数据存放堆区(对象存放堆中)
   栈:局部变量存放在栈空间中(基本数据类型),如果是引用数据类型(在栈空间存放数据对象的引用)。

栈的生命周期:

 栈的生命周期和线程一致。

栈的作用:

栈是Java程序运行时的一块区域,用于保存局部变量,部分结果并参与方法的调用和返回。

局部变量:
基本类型变量:8种基本数据类型变量
引用类型变量:(类、数组、接口)在栈中存放,对象引用地址。

栈的优点:

栈是一种有效的分配存储方式,访问速度仅次于程序计数器(PC寄存器)。

Java栈操作:
    1) 每个方法执行(入栈)
    2) 方法执行结束(出栈)
    3) 遵循先进后出原则

栈不存在GC垃圾回收问题(因为都是入栈/出栈操作)
栈会出现内存溢出问题(可通过设置栈内存大小 -Xss)

栈中异常问题:

Java虚拟机规范允许Java栈大小是动态扩展固定大小的。

固定大小:如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机栈会抛出StackOverflowError(栈溢出)异常

动态扩展:如果扩展时无法申请到足够内存,或在创建新的线程时没有足够的内存去创建对应的虚拟机栈,会抛出OutOfMemoryError(内存溢出)异常

栈的存储单位:

        栈中的数据是以栈帧(Stack Frame)的格式存放。
每个线程都有一个栈,当前线程正在执行的每个方法都对应一个栈帧(方法执行->入站,执行结束->出栈)
栈帧是一个内存区块/数据集,维护方法执行过程中各种数据信息。

栈的运行原理:

        在一条活动的线程中,只会有一个活动栈帧,只有当前正在执行方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为“当前栈帧”(Current Frame),与当前栈帧相对应的方法是“当前方法”(Current Method),定义这个方法的类是“当前类”(Current Class)

        执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

        如果在方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧,然后虚拟机会丢弃当前栈帧,使前一个栈帧重新成为当前栈帧

        不同线程中所包含的栈帧不允许存在互相引用的。

        Java中方法有两种返回方式:1) Return    2) 抛出异常(未try catch处理异常)不管哪种方式返回都会导致栈帧被弹出。

JVM运行时数据区-程序计数器

运行时数据区分为:
        程序计数器(PC寄存器)、虚拟机栈、本地方法栈、方法区、堆区

线程共享区:方法区,堆区。
线程独立区:程序计数器(PC寄存器)、虚拟机栈、本地方法栈。

JVM线程:

1.一个JVM线程对应一个Runtime(运行时数据区)

2.JVM允许一个应用有多个线程并执行。

3.当一个Java线程准备好执行后,此时操作系统的本地线程也同时创建,Java线程执行终止后,本地线程也会回收。

4.当一个Java线程准备好执行后(初始化-> 1) 程序计数器 2)虚拟机栈 3)本地方法栈),此时操作系统的本地线程也同时创建并初始化,本地线程初始化成功后,就会调用Java线程中的run()方法

如果Java线程启动,发现未处理异常,Java线程终止,操作系统线程决定要不要回收取决于该线程是守护线程或普通线程。

程序计数器:(PC寄存器  Program Counter Register)

        寄存器存储指令相关现场信息,CPU只有把数据装载到寄存器中才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

PC寄存器作用:

        PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令

PC寄存器介绍:
1.PC寄存器是一块很小的内存空间,也是运行速度最快的存储区域。没有GC。

2.在JVM规范中,每个线程都有独立的程序计数器,线程私有的,生命周期与线程生命周期保持一致。

3.程序计数器指令会在存储当前线程正在执行的Java方法的Jvm地址,如果是正在执行Native(本地)方法,则是未指定值(undefined)。

字节码解释器工作是通过改变程序计数器值来选取下一条需要执行的字节码指令。

唯一一个在Java虚拟机规范中没有规定任何Out Of Memory(内存溢出)情况的区域。

JVM指令集、类加载子系统介绍

Jvm整体架构图:

Jvm指令集架构:

指令集架构分为两种:
    1.基于栈的指令集架构(Java编译器是基于栈的指令集架构)。
    2.基于寄存器指令架构。

栈的指令架构:
    优势:跨平台、零地址指令、指令集更小、移植性高。(编译器更容易实现)
    劣势:指令多,性能下降(实现同样的功能需要更多指令)
寄存器指令架构:
    优势:性能优秀、执行效率高,指令少
    劣势:移植性差

Jvm生命周期:

跟随线程的结束或终止生命周期就结束(System.exit()方法或Runtime类的Halt方法)。

Jvm类加载子系统图:

 

类加载子系统作用:

类加载子系统负责从文件系统或网络中加载class文件,class文件在文件开头有特定文件标识(Ca Fe Ba Be)。ClassLoad只负责class文件的加载,是否可以运行由Execution Engine决定。

Jvm加载器:

1.启动类加载器(引导类加载器 Bootstrap ClassLoader):
    1) 使用C/C++语言实现,嵌套在Jvm内部。
    2) 用来加载Java核心类库。
    3) 加载扩展类和系统类,指定为它们的父类加载器2.扩展类加载器(Extension ClassLoader):
    1) Java语言编写。
    2) 派生于ClassLoader类。
    3) 从java.ext.dirs系统属性所指定目录中加载或JDK的安装目录jre/lib/ext子目录下加载。
    4) 如果用户创建的Jar放在jre/lib/ext子目录下,自动由扩展类加载器加载3.系统类加载器(App ClassLoader):
    1) Java语言编写。
    2) 派生于ClassLoader类。
    3) 父类为扩展类加载器。
    4) 负责加载环境变量classpath或系统属性java.class.path指定下的类库。
    5) Java应用类,自定义类都是由系统加载器完成加载自定义加载器的好处:
    1) 隔离加载类 2) 修改类的加载方式 3) 扩展加载源 4) 防止源码泄漏

如何自定义加载器:
    1) 通过继承抽象类java.lang.ClassLoader的方式,实现自己的类加载器。
    2) 如果没有复杂的需求,可以直接继承URLClassLoader类,可以避免编写findClass()方法及获取字节码的方式,使自定义加载器更加简洁。
/**
 * 自定义类加载器
 */
public class MyClassLoader extends ClassLoader{

 private String classPath;

 public MyClassLoader(String classPath){
 this.classPath = classPath;
 }

 @Override
 protected Class<?> findClass(String name) {
 byte[] classByte = loadClassByte(name);
 return defineClass(name, classByte, 0, classByte.length);
 }

 private byte[] loadClassByte(String name) {
 // 获取该类在文件系统中保存的格式 (即路径加文件名)
 String fileName = classPath + File.separator + name.replace(".",File.separator) +".class";

 File file = new File(fileName);
 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 try(InputStream is = new FileInputStream(file)){

 int len;
 byte[] b = new byte[1024*8];
 while ((len = is.read(b)) != -1) {
 outputStream.write(b, 0, len);
 }
 } catch (FileNotFoundException e) {
 e.printStackTrace();
 } catch (IOException e) {
 e.printStackTrace();
 }
 return outputStream.toByteArray();
 }
}
public static void main(String[] args) throws Exception {
 //路径请使用classpath外的一个class文件,并且类路径下不能包含此class,才能使用到自定义的类加载器
 MyClassLoader myClassLoader = new MyClassLoader("F:\\chenxi");
 Class clazz = myClassLoader.loadClass("com.MyClassLoading");
 System.out.println(clazz + "-" + clazz.newInstance()+ "-" +clazz.getClassLoader());
}

类加载过程:

分为三个阶段:1.加载  2.链接  3.初始化

加载阶段: 
     1.通过一个类的全限定名获取定义此类的二进制字节流。
     2.将字节流所代表的静态存储结构化为方法区的运行时数据结构。
     3.在内存中生成一个代表这个类的java.lang.class对象作为一个方法区,这个类的各种数据访问入口。

     加载.class文件的方式:
         1.Jar,War格式的基础,从zip压缩包中读取
         2.动态代理技术,运行时计算生成。

链接阶段:
    1.验证:确保class文件中的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全(四种验证:1.文件格式 2.元数据 3.字节码 4.符号引用)。
            
     
    2.准备:为变量分配内存并设置该类变量的默认初始值(零值)(不包含用final修饰的static,因为final在编译时就分配了,准备阶段会显式初始化)。

    3.解析:将常量池内的符号引用转换为直接引用的过程,解析操作随着Jvm在执行空初始化后再执行(解析主要针对于:类、接口、字段、类方法、接口方法、方法类型等)。

初始化阶段:
    执行类构造器的方法<clinit>()的过程。此方法不需要定义,是Javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
    static int a = 1;
    static{
      a = 2;
    }
    构造器方法中指令按语句在源码文件中出现的顺序执行。
    <clinit>()不同于类的构造器。若该类具有父类,Jvm会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
    虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。(保证一个类只加载一次)
    <init>对应类中的构造器。

    IDEA安装jclasslib插件:





双亲委派机制

工作原理:

    1) 如果一个类加载器收到了类加载的请求,它并不会自己去加载,而是先将这个请求委托给父类的加载器去执行。

    2) 如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终达到顶层的引导类加载器(启动类加载器)。

    3) 如果父类加载器可以完成此加载任务,就成功返回,如果父类加载器无法完成此加载任务,子类加载器才会尝试自己加载

优势:
    1) 避免重复加载 
    2) 保护程序安全,防止核心Api被随意篡改(沙箱安全机制)。