栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

深入理解java虚拟机篇二(java内存区域篇)

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

深入理解java虚拟机篇二(java内存区域篇)

java内存区域 双亲委派机制

java虚拟机对于class文件是按需加载也就是说需要该类时才会把它的class文件加载进内存里生成class对象,而且加载某个class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。

双亲委派机制工作原理

1、当一个类加载器收到类加载请求,它不会自己先去加载,而是委派父类加载器去加载这个类。

2、如果父类加载器之上还存在父类加载器,则进一步委托,依次递归,最后到达顶层的引导类加载器(这个加载器是由C/C++书写的,一般是无法访问的。)

3、如果父类加载器可以成功加载这个类,则返回加载成功的数据,如果父类加载器无法加载,那子加载器会尝试加载这个类。

class Father {
    public static void print(String str) {
        System.out.println("father " + str);
    }
    private void show(String str) {
        System.out.println("father " + str);
    }
}
class Son extends Father {
}
public class VirtualMethodTest {
    public static void main(String[] args) {
        Son.print("coder");// father coder
    }
}



双亲委派机制的好处:
1、避免类的重复加载
2、保护程序的安全性,防止核心的API被篡改。

沙箱安全机制
这个机制主要是保护java源代码信息的。如果我们自己建立和源代码相同的包,例如java/lang/String.Class,在我们去使用类加载器去加载此类时,为了防止你自定义的类对源码的破坏,所以他默认不是使用你的String类的本身的系统加载器去加载它,而是选择率先使用引导类加载器去加载,而引导类在加载的过程中会先去加载JDK自带的文件(rt.jar包中的java/lang/String.class),而不是你自己定义的String.class,报错信息会提示没有main方法 ,就是因为加载的是rt.jar包下的String类,这样就可以做到保证对java核心源代码的保护,这即是沙箱保护机制。

package java.lang;


public class String {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

这个程序可以运行,但是它不会加载当前的java.lang.String,而是会加载默认的java.lang.String包,在那个包下没有main方法,而当前程序需要运行main方法,因此会报错。

jvm虚拟机线程

java虚拟机定义了若干程序运行时用到的数据区,其中有一些会随虚拟机的启动而创建,随虚拟机退出而销毁,另外一些则是与线程一一对应,这些与线程对应的数据区域则是随线程的开始和结束而创建和销毁。

1、每一个线程:独立的包括程序计数器,栈,本地栈
2、线程间共享:堆,堆外内存空间(永久代或元空间,代码缓存)

1、线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。 在Hotspot JVM里,每一个线程都与操作系统的本地线程直接映射。

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

3、操作系统负责所有线程的安排调度在一个可用的CPU上,一旦本地线程初始化成功,程序就会调度java线程的run()方法。

jvm的系统线程

如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(string[])的main线程以及所有这个main线程自己创建的线程。这些主要的后台系统线程在Hotspot JVM里主要是以下几个:

虚拟机线程∶这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。

周期任务线程∶这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。

GC线程: 这种线程对在JVM里不同种类的垃圾收集行为提供了支持。

编译线程 :这种线程在运行时会将字节码编译成到本地代码。

信号调度线程∶这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

运行时数据区概述

运行时数据区主要可以划分五种:

1、程序计数器
2、虚拟机栈
3、本地方法栈
4、堆
5、方法区


程序计数器

程序计数器(Program counter Register)是一块较小的内存空间(也是运行速度最快的存储区域),它可以看作当前线程执行的字节码的行号指示器,在java虚拟机的概念模型中,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等都需要这个计数器完成。

Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,它的生命周期与线程的生命周期一致。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空((Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

public class PCRegisterTest {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        String c = "123";
        System.out.println(c);
        System.out.println(b-a);
    }
    
}

PC寄存器的存储字节码指令的用途:当程序运行时,CPU需要不断切换其他的线程,当CPU切换回原来的线程时,CPU就知道从哪里开始执行。

PC寄存器记录当前线程的执行地址:JVM的字节码解析器需要改变PC寄存器的值来明确下一条该执行什么样的字节码指令。

PC寄存器被设定为线程私有:CPU在运行的过程中不断做任务切换,多线程是在一个时间段内只会执行其中的某一个线程的方法,这样必然会有中断和异常,为了准确记录各个线程正在执行的当前字节码指令地址,那就必须每一个线程配备一个PC寄存器,这样线程可以独立计算,不会互相干扰。
由于CPU时间片限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某一个线程的字节码指令。这样需要给每一个线程在创建的时候,分配一个程序计数器和栈帧,程序计数器在各个线程中互不影响。

CPU时间片:CPU分配给各个程序的时间,每一个线程被分配一个时间段,称作时间片。用户可以打开多个程序同时运行,在视觉错觉上以为是并发运行的,但是实际上由于只有一个CPU,一次只能处理程序要求的一部分,如何实现公平处理,那就是引用时间片,让每一个程序轮流执行。

java虚拟机栈

虚拟机栈出现的背景:java语言是跨平台设计的,java的指令是根据栈设计的,不同的平台不同的CPU架构不同,所以java不能基于寄存器设计。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

虚拟机栈:它与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧l (Stack frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧(栈帧是方法运行期很重要的基础数据结构)在虚拟机栈中从入栈到出栈的过程。(java虚拟机栈的访问速度仅次与程序计数器,而且对栈来说不存在垃圾回收问题)

public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);// 本机最大到11420附近
    }
}

虚拟机栈帧的内存分配:有些人认为Java内存区域只有堆内存((Heap〉和栈内存(Stack),这种划分方式直接继承自传统的C、C++程序的内存布局结构(因为当java面世时,C/C++正处于顶峰时期),在Java语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。“栈”通常指虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。

public class StackError {
    private static int a = 0;
    public void stackOutError(){
        a++;
        stackOutError();
    }

    public static void main(String[] args) {
    	// 函数调用的最大深度,一般都是在这附近了。
        StackError error = new StackError();
        try {
            error.stackOutError();
        }catch (Throwable e){
            System.out.println("The last a count is = "+a);
            e.printStackTrace();
        }
    }
}


局部变量槽:这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

局部变量槽的复用对垃圾回收机制的影响之一

为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用。不过,这样的设计除了节省栈帧空间以外,还会伴随有少量额外的副作用,而且在某些情况下变量槽的复用会直接影响到系统的垃圾收集行为。

虚拟机栈扩展小结:在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

模拟StackOverFlowError:

public class StackOverFlowTest {
    private static int a = 0;
    public static void main(String[] args) {
        try{
            new StackOverFlowTest().test1();
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("a最后的值是:"+a);
        }
    }
    public void test1(){
        a++;
        test2();
    }
    public void test2(){
        a++;
        test1();
    }
}


模拟OutOfMemoryError异常:

import java.util.ArrayList;
import java.util.List;

public class OutOfMemory {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        for (int i = 0; ; i++){
            list.add(new String("OutOfMemory异常"));
        }
    }
}

在配置虚拟机内存的区域限制大小,-Xmx1m:(设置大小为1mb)

HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所以在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfM emoryError异常,只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的。

public class VirtualStack01 {
    public void methodA(){
        int a = 1;
        int b = 10;
        methodB();
    }

    public void methodB(){
        int k = 2;
        int m = 20;
    }

    public static void main(String[] args) {
        VirtualStack01 stack01 = new VirtualStack01();
        stack01.methodA();
    }
    

}
运行时栈帧的内存结构

java程序的每一个线程都有自己的栈,栈中的数据都是以栈帧的格式存在,在这个线程上执行的每一个方法都各自对应一个栈帧,栈帧是一个内存块,也可看作一个数据集,存储着方法执行的过程中的各种数据信息。(方法调用对应于栈帧在虚拟机栈的入栈和出栈的过程)

虚拟机栈帧的内存结构:

1、局部变量表
2、操作数栈
3、动态链接
4、方法返回地址
5、附加信息


每一个方法从调用开始至执行结束,都对应着一个栈帧在虚拟机栈中的从入栈到出栈的过程。

编译java程序的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中。换言之,一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码以及具体的虚拟机实现的栈内存布局形式。

以Java程序的角度来看,同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack frame),与这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作,如果在该方法中调用了其他的方法,对应的新的栈帧就会被创建出来,放在栈的顶部,成为新的当前帧。

public class JavaTest {
    public static void main(String[] args) {
        try {
       // 方法的结束方式分为两种:
       // ① 正常结束,以return为代表 
       // ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束
            JavaTest test  =new JavaTest();
            test.methodA();
            
        }catch (Exception e){
            e.printStackTrace();
        }

        System.out.println("main方法结束。。。。。。");
    }

    public void methodA(){
        System.out.println("method1()开始执行...");
        methodB();
        System.out.println("method1()执行结束...");
    }

    public int methodB(){
        System.out.println("method2()开始执行...");
        int i = 10;
        int m = (int) methodC();
        System.out.println("method2()即将结束...");
        return i + m;

    }

    public double methodC(){
        System.out.println("method3()开始执行...");
        double j = 20.0;
        System.out.println("method3()即将结束...");
        return j;
    }
}


不同的线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧。

如果当前方法调用了其他的方法,方法返回之际,当前帧会传回此方法的执行结果给前一个栈帧,然后虚拟机会丢弃当前帧,使得前一个栈帧重新成为当前栈帧。

java方法有两种函数返回的方式:一个是正常的是函数返回,return指令,另外一种是抛出异常,这两种方式都会导致虚拟机的栈帧弹出。

局部变量表(Local Variables Table)

局部变量表被称为局部变量数组和本地变量表,局部变量表被定义为一个数字数组,主要存储方法参数和定义在方法体内的局部变量,这些数据类似是包含了各种的基本数据类型,对象引用(reference)和returnAddress类型。

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

方法嵌套调用的次数由栈大小决定,一般来说,栈越大,方法嵌套调用的次数越多,对于一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求,进而函数调用就会占用更多的栈空间,导致其嵌套调用的次数就会减少。

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

参数值总会存放在局部变量表数组的index0的开始,到数组的长度减一的索引结束。

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

局部变量表中存放编译期可知的8种局基本的数据类型,引用类型(reference)和returnAddress类型的变量。
在局部变量表中,32位以内的类型只是占一个slot(包括returnAdres类型),64位类型的(long和double)占用两个slot。

注意:byte,short,char,在存储前被转换为int,boolean也被转换为int类型,0代表false,1代表true。long和double则占用两个slot。

JVM会为局部变量表中的每一个slot分配一个访问索引,通过这个索引可以访问到局部变量表中指定的局部变量值。

当一个实例方法被调用时,它的方法参数和方法体内定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。

如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引。

局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。
如果当前帧是由构造方法或者实例方法创建的,那么该对象的引用this将会存放在index0的slot处。其余的将会按照参数表顺序排列。

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,(虚拟机使用局部变量表完成方法的传递。)

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

参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

上面的输出会报错,变量m没有初始化。

public class LocalVariablesTest {
    private int count = 0;

    public static void main(String[] args) {
        LocalVariablesTest test = new LocalVariablesTest();
        int num = 10;
        test.test1();
    }

    //练习:
    public static void testStatic(){
        LocalVariablesTest test = new LocalVariablesTest();
        Date date = new Date();
        int count = 10;
        System.out.println(count);
        //因为this变量不存在于当前方法的局部变量表中!!
//        System.out.println(this.count);
    }

    //关于Slot的使用的理解
    public LocalVariablesTest(){
        this.count = 1;
    }

    public void test1() {
        Date date = new Date();
        String name1 = "www.baidu.com";
        test2(date, name1);
        System.out.println(date + name1);
    }

    public String test2(Date dateP, String name2) {
        dateP = null;
        name2 = "woshinibaba";
        double weight = 130.5;//占据两个slot
        char gender = '男';
        return dateP + name2;
    }

    public void test3() {
        this.count++;
    }

    public void test4() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        //变量c使用之前已经销毁的变量b占据的slot的位置
        int c = a + 1;
    }

    
    public void test5Temp(){
        int num;
        //System.out.println(num);//错误信息:变量num未进行初始化
    }

}

操作数栈(Operand Stack)(或表达式栈)

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准
的入栈(push)和出栈(pop)操作来完成一次数据访问。如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。譬如在做算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递。举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。iadd这个指令只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了。


java虚拟机编译代码追踪

public class OperandStackTest {
    public static void main(String[] args) {
        OperandStackTest test = new OperandStackTest();
        System.out.println(test.calc()); // 90000
    }
    public int calc(){
        int a = 100;
        int b = 200;
        int c = 300;
        return (a+b)*c;
    }
}

反编译后的字节码文件

1、首先执行偏移地址为0的指令,Bipush指令的作用是将单字节的整形常量值(-127~128)推入操作数栈顶,跟随有一个参数,指明推送的常量值为100。

2、执行偏移地址为2的指令,istore_1指令的作用是操作数栈顶的整形值出栈并存放到第一个局部变量槽中,后续4条指令(一直到偏移地址为11的指令为止)都是做的一样的事情,也就是在对应的代码中把变量a,b,c赋值100,200,300。

3、执行偏移地址为11的指令,iload_1指令的作用是将局部变量表的第一个变量槽的整形值复制到操作数栈顶。

4、执行偏移地址为12的指令,iload_2指令的作用与iload_1类似,把第二个变量槽的整形值入栈。

5、执行偏移地址为13的指令,iadd指令的作用是将操作数栈顶的头两个栈顶元素出栈,做整形加法,然后把结果入栈,在iadd指令执行完毕,栈中的原有的100,200被出栈,它们的和和300被从新入栈。

6、执行偏移地址为14的指令,iload_3指令把存放在第三个局部变量槽中的300入栈到操作数栈,这时操作数栈的值为两个整形数字300,下一条指令imul将操作数栈顶的两个元素出栈,做整形乘法,然后把结果入栈,与iadd指令的执行过程类似。

7、执行偏移地址为16的指令,ireturn指令是方法返回指令之一,它将结束方法执行并将操作数栈顶的整形值返回给该方法的调用者,整个过程执行结束。

动态链接(Dynamic linking)(指向运行时常量池的引用)

每个栈帧都包含一个指向运行时常量池 中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。


方法返回地址(Return Address)(方法正常退出或者异常退出的定义)

当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(return指令),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。

另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。(需要具体到那一款虚拟机)

一些附加信息
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

方法调用和方法的绑定机制

方法调用:虚方法与非虚方法

在jvm中将符号引用为调用方法的直接引用与方法的绑定机制有关,如果方法在在编译期就确定了调用的具体版本,这个版本在运行的时候是不可变的,这样的方法被称之为非虚方法。静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法。其它的方法被称之为虚方法。
虚拟机中提供了以下几条方法调用指令:

普通调用指令:

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

动态调用指令:

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

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

@FunctionalInterface
interface Func {
    public boolean func(String str);
}

public class Lambda {
    public void lambda(Func func) {
        return;
    }

    public static void main(String[] args) {
        Lambda lambda = new Lambda();

        Func func = s -> {
            return true;
        };

        lambda.lambda(func);

        lambda.lambda(s -> {
            return true;
        });
    }
}

动态类型的语言与静态类型的语言

两者的区别是在对于类型的检查是在编译期还是运行期,满足在编译期对类型的检查的是静态类型语言,反之是动态类型的语言。

private String str= “abcd”// str = abcd
// 判断str的是静态类型语言(强语言,如:java),判断abcd的是动态类型的语言(弱语言,如:js,python)。
静态类型的语言是判断变量自身的类型信息,动态类型的语言
是判断变量值的类型信息,变量没有类型信息,变量值有类型信息。
class Father implements MethodInterface{
    public Father() {
        System.out.println("father的构造器");
    }

    public static void showStatic(String str) {
        System.out.println("father " + str);
    }

    public final void showFinal() {
        System.out.println("father show final");
    }

    public void showCommon() {
        System.out.println("father 普通方法");
    }

    @Override
    public void methodA() {
        System.out.println("我是接口方法!");
    }
}

public class Son extends Father{
    public Son() {
        //invokespecial:调用方法,私有及父类方法,解析阶段唯一确定调用方法
        super();
    }
    public Son(int age) {
        //invokespecial:调用方法,私有及父类方法,解析阶段唯一确定调用方法
        this();
    }
    //不是重写的父类的静态方法,因为静态方法不能被重写!
    public static void showStatic(String str) {
        System.out.println("son " + str);
    }

    private void showPrivate(String str) {
        System.out.println("son private" + str);
    }

    public void show() {
        //invokestatic:调用静态方法,解析阶段唯一确定调用方法
        showStatic("www.baidu.com");
        //invokestatic
        super.showStatic("good!");
        //invokespecial
        showPrivate("hello!");
        //invokespecial
        super.showCommon();

        //invokevirtual:调用所有虚方法
        showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
        //虚方法如下:
        //invokevirtual
        showCommon();
        info();

        MethodInterface in = new Father();
        //invokeinterface:调用接口方法
        in.methodA();
    }


    public void info(){

    }

    public void display(Father f){
        f.showCommon();
    }

    public static void main(String[] args) {
        Son so = new Son();
        so.show();
    }


}

interface MethodInterface{
    void methodA();
}

静态链接

当一个字节码文件被装载进jvm内部时,如果被调用的目标方法在编译期内便可知,且运行时不变,这种情况下将调用方法的符号引用转换为直接引用的过程被称为静态链接。

动态链接

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

class Lesson{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    // 静态方法:非虚方法
    public static void returnName(){
        System.out.println("我的名字是课程!");
    }
}

class Teacher extends Lesson implements Method{
    private String name;
    private int age;

    // 私有方法:非虚方法
    private void TeacherName(){
        System.out.println("我比较害羞,我是私有方法!");
    }
    // final方法:非虚方法
    public final void returnData(){
        System.out.println("我要返回一个常量!Hello Word!");
    }

    // 构造器
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public void teachLesson(String name) {
        super.setName(name);// 静态链接,其他类似
        System.out.println("调用父类构造器方法:"+super.getName());
        this.age = 24;
        this.name = "小红";
        // 调用父类静态方法
        Lesson.returnName();
        returnData();
    }

    @Override
    public String toString() {
        this.TeacherName();
        return "Teacher{" +
                "我是:" + name +
                ",年龄是:" + age +
                ",教授的课程是:"+ super.getName()+
                '}';
    }
}

interface Method{
    void teachLesson(String name);
}

public class VirtualMethodTest01 {
    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        // 运行期才确定的:虚方法。
        teacher.teachLesson("高等数学");// 动态链接,其他类似
        System.out.println(teacher.toString());
    }
}

结果:

方法绑定

对应的方法的绑定可分为早期绑定和晚期绑定,绑定是一个字段、方法、或者类在符号引用被替换成直接引用,这个过程只发生一次。

早期绑定

早期绑定是指被调用的目标方法如果在编译期内可知,且运行期保持不变的时候,即可将这个方法和所属的类型进行绑定,由于明确了被调用的目标方法是哪一个,因此可以将静态连接的方式将符号引用转换为直接引用。

晚期绑定

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

class Animal{

    public void eat(){
        System.out.println("动物进食");
    }
}
interface Huntable{
    void hunt();
}
class Dog extends Animal implements Huntable{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,多管闲事");
    }
}

class Cat extends Animal implements Huntable{

    public Cat(){
        super();//表现为:早期绑定
    }

    public Cat(String name){
        this();//表现为:早期绑定
    }

    @Override
    public void eat() {
        super.eat();//表现为:早期绑定
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,天经地义");
    }
}
public class AnimalTest {
    public void showAnimal(Animal animal){
        animal.eat();//表现为:晚期绑定
    }
    public void showHunt(Huntable h){
        h.hunt();//表现为:晚期绑定
    }
}

线程安全问题

如果只有一个线程可以操作这个数据,那必然是线程安全的。
如果有多个线程可以操作这个数据,这个数据是处于内存共享区域,如果不考虑同步机制,那这个数据是存在线程安全问题。

public class StringBuilderTest {

    int num = 10;

    //s1的声明方式是线程安全的
    public static void method1(){
        //StringBuilder:线程不安全,没有线程同步机制
        // StringBUffer: 线程安全,有线程同步机制,内部方法有synchronized修饰
        StringBuilder s1 = new StringBuilder();
        StringBuffer s2 = new StringBuffer();
        s1.append("a");
        s1.append("b");
        s2.append('c');
        s2.append('d');
        //...
    }
    //sBuilder的操作过程:是线程不安全的
    public static void method2(StringBuilder sBuilder){
        sBuilder.append("a");
        sBuilder.append("b");
        //...
    }
    //s1的操作:是线程不安全的
    public static StringBuilder method3(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1;
    }
    //s1的操作:是线程安全的
    public static String method4(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1.toString();
    }

    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();


        new Thread(() -> {
            s.append("a");
            s.append("b");
        }).start();

        method2(s);

    }

}
本地方法栈

java的虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用。(本地方法栈线程私有)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

本地方法栈的使用语言,方式和数据结构并没有强制的规定,可以根据需要自由实现。有的java虚拟机直接把本地方法栈和虚拟机栈合二为一(Hotspot JVM),与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemory Error异常。

当某个线程调用本地方法时,它就进入了一个全新的并且不受虚拟机限制的,它和虚拟机具有相同的权限。

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
它可以直接使用本地处理器的寄存器。
直接从本地内存的堆中分配任意数量的内存。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/334238.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号