背景

前一阵跟着宋红康的视频学了学JVM,视频没有更新完,所以也没学完,这里记录一下笔记

JVM概述

JVM位置:  运行在操作系统之上

  

 相对于java语言,JVM的位置如下所示

 对于安卓的Davlik虚拟机,他分布在安卓运行时内存区

整体结构:

以HotSpot VM为例,它采用解释器与即时编译器(JIT)并存的架构 

 

 运行时数据区和执行引擎的交互如下图所示:

 java代码执行流程

把java源码编译成class字节码文件的编译器称为前端编译器,即时编译器称为后端编译器,把反复执行的代码(称为热点代码)的字节码指令编译为机器指令,并缓存起来。

解释器响应快,但执行起来慢;JIT响应慢,但执行起来快

架构模型

基于栈(Hotspot VM)

    实现简单,适用于资源受限的系统

    由于只有入栈出栈操作,所以避开了寄存器的分配难题,采用零地址指令分配

    指令集小,编译器容易实现

    不需要硬件支持,可以执行更好

    指令多

基于寄存器(PC、Android的Davlik)

    完全依赖硬件,可移植性差

    性能优秀,执行高效

    指令少

    指令集以一地址、二地址和三地址为主

生命周期

    启动:通过引导类加载器创建的初始类来完成,这个类由具体的JVM具体实现

    执行:执行java程序的过程就是JVM的执行过程,java程序执行完毕,对应的JVM进程随即终止;

    退出:程序正常结束;程序遇到了异常或错误而异常终止;操作系统出现错误;Runtime或System的exit()方法,以及Runtime的halt()方法;JNI也可以加载卸载JVM

类加载器子系统

职责

负责从文件系统或网络中加载class文件

class文件在文件头有特定的文件标识(CA FE BA BE),链接的验证阶段会进行验证

类加载器只负责class文件的加载,至于能否运行,由执行引擎决定。

加载好的类信息(包括对应的类加载器的引用)存放于方法区,此外,方法区还保存运行时常量池信息(字符串字面量和数字常量等)

 

字节码中的常量池实例

以下面代码为例

public class PCTest {
    public static void main(String[] args) {
        int a = 0;

        for (int i = 0; i < args.length; i++) {
            System.out.println(args[i]);
        }

        int b = 1;
    }
}

对应字节码中的常量池如下图所示

 左边的#数字是符号引用,=后面的是常量类型(方法引用、属性引用、类、UTF8为字符串字面量等)

类加载器的角色

 

类加载过程

加载->链接(验证->准备->解析)->初始化

 流程图如下所示(目标类HelloLoader)

 

1、加载阶段:

        通过类的全限定名获取定义此类的二进制字节流,然后把字节流所代表的静态存储结构存入方法区,最后在内存中生成代表这个类的Class对象,作为方法区这个类的访问入口

2、链接阶段:

 3、初始化阶段:

 <clinit>()是针对静态代码的,如果类里没有静态部分,就没有<clinit>()方法。由于<clinit>()的顺序执行,因此下面程序的输出结果为sA = 2, sB = 1

private static int sA = 1;


static {
    sA = 2;
    sB = 2;
}


private static int sB = 1;


public static void main(String[] args) {
    System.out.println("sA = " + sA);
    System.out.println("sB = " + sB);
}

 以sB为例,链接的准备阶段被初始化为0,初始化阶段先被赋值为2,再被赋值为1

另外,由于<clinit>()的同步加锁,因此多个线程加载一个类时,这个类的静态块只会被加载一次。比如以下代码

public class PCTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            new MyThread();
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }, "t1");
        Thread t2 = new Thread(() -> {
            new MyThread();
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }, "t2");


        t1.join();
        t2.join();


        t1.start();
        t2.start();
    }
}


class MyThread {
    static {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "初始化当前类");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            } finally {
                break;
            }
        }
    }
}

t1和t2都会加载类MyThread,但是static块只会运行一次,故而输出如下所示

class文件的获取方式

本地文件系统、网络(web applet)、zip压缩包(jar、war)、运行时计算(动态代理)、其他文件生成(JSP)、专有数据库、加密文件(安卓防止反编译)

类加载器的分类

引导类(Bootstrap)加载器和自定义加载器(Extension、Application、自定义)

这四者的关系是包含关系,不是父类子类关系。

以sun.misc.Launcher(一个JVM的入口应用)为例,它里面类加载器类图如下所示

 用户自定义类都用AppClassLoader,如下所示

ClassLoader classLoader = StackStructTest.class.getClassLoader();
// 用户自定义类使用应用加载器加载
System.out.println("StackStructTest的类加载器为:" + classLoader);

 系统api的类加载器为引导类加载器,由于这是由C实现的,所以java获取到的值为null,如下所示

classLoader = String.class.getClassLoader();
// 系统核心类库(String)使用引导类加载器加载
System.out.println("String的类加载器为:" + classLoader);

 而默认的系统类加载器就是应用类加载器,而其父加载器为扩展类加载器,扩展类加载器的父类就是引导类加载器,如下所示

ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器为:" + systemLoader);

ClassLoader extLoader = systemLoader.getParent();
System.out.println("系统类加载器的父加载器为:" + extLoader);

ClassLoader bootstrapLoader = extLoader.getParent();
System.out.println("扩展类加载器的父加载器为:" + bootstrapLoader);

 我们可以获取引导类加载器的加载路径,如下所示

// 获取引导类加载器能加载的路径
System.out.println("引导类加载器的加载路径:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url.toString());
}

 可见都是系统类库

我们也可以获取扩展类加载器的加载路径,如下所示

System.out.println("扩展类加载器的加载路径");
String extDirs = System.getProperty("java.ext.dirs");
for (String s : extDirs.split(";")) {
    System.out.println(s);
}

 也就是ext目录下的类

用户自定义类加载器

存在意义:隔离加载类(模块和中间件的同步)、修改类加载方式、扩展加载源、防止源码泄露

实现步骤:继承URLClassLoader类,如果需要实现比较复杂的业务逻辑,就继承写ClassLoader类,覆写findClass()方法

class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = getClassFromFilePath(name);
            if (bytes == null) {
                throw new FileNotFoundException();
            } else {
                return defineClass(name, bytes, 0, bytes.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }


        throw new ClassNotFoundException();
    }


    private byte[] getClassFromFilePath(String name) {
        // 略

        return null;
    }
}

其中我们可以在getClassFromFilePath()方法中自定义类加载逻辑,如果类文件是加密文件,可以在这里写上解密逻辑。

自定义类加载器的使用如下所示

Class clazz = Class.forName("className", true, new MyClassLoader());
Object object = clazz.newInstance();

双亲委派机制

类加载器在加载一个类时,会先让最顶层的引导类加载器加载类。如果上层类加载器可以加载类,那么就让那一层类加载器加载,否则再让下层加载。

也就是说,加载一个类调用的类加载器先后顺序为:引导类->扩展类->应用(系统)类->用户自定义类

这样可以保护系统API的安全性,同时也是一种沙箱安全机制

 

两个类对象为同一个类的条件:类的全限定名一样;类的加载器一样

因此,两个类对象即便来源于同一个class文件,被同一个JVM加载,如果加载它们的加载器不一样,这两个类对象也不是一个类。

类的使用方式

主动使用:创建类的实例、访问类的静态部分(变量、方法)、反射、初始化类的子类、JVM启动时被标明为启动类、动态语言支持(REF_getStatic、REF_putStatic、REF_invokeStatic对应的类)

被动使用:除了上面七种方式,剩下都是类的被动使用,不会导致类的初始化

PC寄存器

JVM中的PC寄存器是对物理PC寄存器的抽象模拟,用于存储下一条指令的地址,线程独立

它很小,运行速度最快。如果当前线程正在执行的方法是java方法,他会存储对应方法的JVM指令地址(也就是下一条指令地址);如果执行的是本地方法,则会存储未指定值(undefined)

它是程序控制流的指示器,字节码解释工作制通过改变PC寄存器的值来选取下一条要执行的字节码指令,不存在OOM

例如如下代码

public static void main(String[] args) {
    int a = 0;


    for (int i = 0; i < args.length; i++) {
        System.out.println(args[i]);
    }


    int b = 1;
}

对应的字节码指令为

左边的一排数字就是字节码指令地址,当执行到第22行goto 4时,就会跳转到第4行的iload_2,即取出JVM栈当前栈帧中局部变量表里第2个元素(i)的值

保存字节码指令地址是为了方便线程切换时能够知道从哪儿继续执行。

虚拟机栈

概述

线程私有的,保存方法的局部变量(8种基本数据类型+对象的引用)、部分结果,参与方法的调用和返回。

生命周期

生命周期和线程一致

特点

快速有效,访问速度仅次于PC;操作只有进栈出栈;不存在GC问题

相关异常

如果采用固定大小的栈,当线程请求的栈容量超标,则抛出StackOverFlow异常(递归调用main()方法)

如果采用动态分配大小的栈,当扩展容量时没有足够的内存,则抛出OutOfMemory异常

设置

设置栈大小:-Xss256k(设置栈大小为256K)

内部结构

如上图所示,内部单位为栈帧,一个栈帧对应一个方法的调用。

一个时间点上,只有栈顶栈帧是活动的,是所谓当前栈帧,对应的方法和类分别是当前方法和当前类

不同栈的栈帧不能相互引用

方法的结束方式

1、正常结束return 

2、方法执行中出现未被捕获的异常,以抛出异常的方式结束

栈帧的内部结构

局部变量表

 定义为一个数字数组,用于存储方法参数和方法体内的局部变量。

数据类型包括:八种基本数据类型、对象引用和返回地址

4字节以内的只占一个slot(包括返回地址类型和引用类型),8字节的占用两个slot(long和double)

slot槽:局部变量表的存储单元,也可以重用,以如下代码为例

public void methodB() {
    int a = 1;
    {
        int b = 0;
    }
    int c = a;
}

对应局部变量表如下

可见c用的是已经被销毁的b的槽

局部变量表所需大小在编译期确定

具体可见javap -v中显示的方法LocalVariableTable中的内容,以下为main()方法的局部变量表:

如果是非静态方法,那么局部变量表里会多一个this

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

方法返回地址

方法返回地址就是调用此方法的PC寄存器的值

如果方法正常退出,那么返回地址就是调用此方法的指令的下一条指令的地址;如果异常退出,那么返回地址由异常表确定,栈帧中不保存这种信息

正常退出的指令根据返回类型决定,有return(void返回类型、构造方法、static代码块)、ireturn(int、char、boolean、byte、short返回类型)、dreturn(double返回类型)、freturn(float返回类型)、lreturn(long返回类型)和areturn(引用返回类型)

异常退出(捕获异常)则根据异常表确定返回地址,如以下代码:

public void showException() {
    try {
        int a = 1 / 0;
    } catch (Exception e) {
        System.out.println(e.toString());
    }
    int b = 9;
}

产生的字节码如下所示

绿色方框内就是异常表,里面的数字是字节码指令在字节码指令表中的地址,也就是是第0条到第4条指令出现Exception类型的异常,返回地址就是第7条指令,即astore_1,把异常对象压入局部变量表中

操作数栈

在字节码指令执行过程中进行出栈进栈的栈,用以保存计算的中间结果,并作为变量的临时存储空间。某些字节码指令把值压入栈,另外一些字节码指令把值从栈顶弹出,然后把计算结果压入栈中。

JVM的执行引擎就是基于操作数栈的执行引擎。

以下面代码为例

public void methodC() {
    int i = 15;
    byte j = 9;
    int  k = i + j;
}

 对应字节码指令如下

代码追踪过程如下

bipush 15把15压入操作数栈里,istore_1把操作数栈中的数存入局部变量表中1的槽里

bipush 8和istore_2一样的道理

iload_1和iload_2分别把局部变量表中索引为1和2对应变量的值弹到操作数栈中

iadd指令把操作数栈中的数弹出相加,把结果23入栈。istore_3再把操作数栈中的数压入局部变量表中索引为3的槽中。

其中istore、iload、iadd的前缀i表示数据类型为int(byte、char、boolean、short都以int类型被解释)

而bipush表示把byte类型的数据压入操作数栈中,如果数值超过byte,则用更大的类型存储。例如int a = 200,对应的指令就是sipush 800

如果涉及方法的返回值,例如如下代码

public void methodC() {
    int i = 15;
    byte j = 9;
    int  k = i + j;


    int m = methodD();
}


public int methodD() {
    int i = 15;
    byte j = 9;
    int  k = i + j;


    return k;
}

对应字节码指令如下

methodC()中aload_0把this装载到操作数栈里,然后invokeVirtual()调用栈顶元素this里#3对应的方法methodD。methodD()中最后的ireturn把操作数栈中的值返回到操作数栈里,methodC()的istore 4把操作数栈顶元素存入局部变量表的4号槽中

4字节的类型占用一个栈单位深度,8字节的占用两个栈单位深度。

如果方法有返回值,返回值也会被压入当前栈帧的操作数栈中。

动态链接

每一个栈帧都包含一个指向运行时常量池中此栈帧所属方法的引用,以便当前方法代码的动态链接

在java源文件编译到class文件的过程中,所有变量和方法都会作为符号引用保存到class文件的常量池中,如下所示

动态链接的作用就是把符号引用转换为调用方法的直接引用(如#3转换为StackFrameTest.methodD:()I),如下图所示

常量池的作用就是提供符号和常量,便于指令的识别

附加信息    

可选,略

变量分类

成员变量:使用前都经历过默认初始化赋值

类变量:链接的准备阶段,会给类变量默认赋值;在初始化阶段,会显式赋值,并执行静态代码块

实例变量:随着对象的创建,会在堆空间中分配空间,赋默认值

局部变量:必须显式赋值

栈顶缓存技术

把栈顶元素全部缓存到CPU寄存器中,以降低对内存的IO次数。

方法的调用

符号引用转换为直接引用与方法的绑定机制有关。

静态链接:目标方法编译期可知

动态链接:目标方法运行期才可知

绑定机制

早期绑定:静态链接对应早期绑定

晚期绑定(类的多态、接口):动态链接对应晚期绑定

以以下代码为例

public class DynamicLinkTest {
    public void showEat(Animal animal) {
        animal.eat();
    }


    public void showHunt(Huntable huntable) {
        huntable.hunt();
    }
}


class Animal {
    public void eat() {
        System.out.println("Animal eat");
    }


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


    public static void showStatic() {
        System.out.println("Animal show statically");
    }
}


interface Huntable{
    public void hunt();
}


class Cat extends Animal implements Huntable {


    public Cat() {}


    public Cat(int n) {
        this();
    }


    @Override
    public void eat() {
        super.eat(); // 早期绑定
        System.out.println("Cat eating...");


        showFinal();
        super.showFinal();
        showStatic();
    }


    @Override
    public void hunt() {
        System.out.println("Cat hunting...");
    }
}


class Dog extends Animal implements Huntable {


    @Override
    public void eat() {
        System.out.println("Dog eating...");
    }


    @Override
    public void hunt() {
        System.out.println("Dog hunting...");
    }
}

由于多态,showEat()和showHunt()中的方法调用都用的是晚期绑定,因为编译期无法确定到底是哪个类的对应方法;而Cat类的eat()方法中的super.eat()用的是早期绑定,因为在编译期就确定是Animal类的eat()方法

四种调用方法的指令

invokevirtual、invokeinerface、invokespecial、invokestatic和invokedynamic

调用父类方法定对应的字节码为invokevirtual

如果是接口方法,则更为特殊,指令是invokeinterface

而调用确定类的方法绑定对应字节码为invokespecial

静态方法则为invokestatic

对于父类的final方法,如果直接调用,则为invokevirtual;如果使用super调用,则为invokespecial

invokedynamic则是为实现动态类型语言而做的一种改进,例如lambda表达式

public void showDynamic() {
    Huntable huntable = () -> {
        System.out.println("lambda huntable");
    };
}

对应字节码为

非虚方法与虚方法

非虚方法:静态方法、私有方法、final方法、构造方法、父类方法,这些方法都为早期绑定,编译期就知道调用的是哪个类的方法

由invokestatic和invokespecial指令调用的都是非虚方法,其余的(final除外)都是虚方法

java中方法覆写的本质

1、找到操作数栈顶元素所执行对象的实际类型,记作C

2、如果在类型C找到与常量池中描述符和简单名称都相同的方法,则进行访问权限校验,通过则返回此方法的直接引用;不通过则抛出IllegalAccessError异常

3、否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程

4、如果始终没有找到合适的方法,则抛出AbstractMethodError异常

 

为了提高上述过程的性能,JVM会在类链接阶段,在方法区为所有的虚方法构造一个虚方法表,存放各个虚方法的实际入口位置。类初始化后,类的所有方法表都会初始化完成

逃逸分析

问题:

    方法中的变量是否线程安全?

答案:

    不一定。例如,下面两种情况的stringBuilder是线程安全的

public void methodA() {
    StringBuilder s = new StringBuilder();
    s.append("a").append("b");
}

public String methodB() {
    StringBuilder s = new StringBuilder();
    s.append("a").append("b");

    return s.toString();
}

但下面两种就不安全了,发生了逃逸

public void methodC(StringBuilder s) {
    s.append("a").append("b");
}


public StringBuilder methodD() {
    StringBuilder s = new StringBuilder();
    s.append("a").append("b");


    return s;
}

本地方法栈

本地方法

本地方法就是java调用非java代码的接口

存在意义:与java外环境交互、与OS交互、Sun`s java

本地方法栈

用于管理本地方法的调用,这是和JVM栈唯一的区别

 当一个线程调用本地方法时,他就进入了一个全新的不再受虚拟机限制的世界,他和JVM有同样的权限。

本地方法可以访问JVM运行时数据区,可以直接使用寄存器,可以直接从堆中分配任意数量的内存。

Hotspot虚拟机中,直接把本地方法栈和虚拟机栈二合一

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐