【JVM】内存结构(上)

15 篇文章 0 订阅

1.内存结构概述

简述图

简述图

详细图-英文

详细图-英文

详细图-中文

详细图-中文

如果自己手写一个Java 虚拟机,主要考虑 类加载器、执行引擎。

2. 类加载器子系统

2.1 类加载器

2.1.0 准备工作 — 安装 JClassLib ByteCode Viewer

① 独立版

软件 github 下载地址 (.exe)

源码 github 下载地址

② IDEA 插件版

搜索、安装、重启IDE

2.1.1 类加载器子系统作用

  • 类加载器子系统负责从 文件系统/网络 中加载Class文件,Class文件在文件开头有特定的文件标识。
  • ClassLoader 只负责 Class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

2.1.2 类加载器ClassLoader角色

类加载器

  1. class file 存在于本地硬盘上,在执行时加载到JVM中。根据这个文件实例化出n个一模一样的实例。
  2. class file 加载到JVM中,被称为 DNA元数据模板,放在方法区。
  3. 经 .class 文件 -> JVM -> 元数据模板,此过程就要一个运输工具(类装载器 Class Loader),扮演一个快递员的角色。

2.2 类的加载过程

类加载过程

① 加载 Loading:
  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载.class文件的方式
  • 从本地系统中直接加载
  • 通过网络获取(Web Applet)
  • 从zip压缩包中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 由其他文件生成(JSP应用)
  • 从专有数据库中提取.class文件,较为少见
  • 从加密文件中获取,典型的防Class文件被反编译的保护措施
② 链接
(1) 验证(Verify)
  • 目的在于确保 Class文件 的字节流中包含的信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
  • 主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
(2) 准备(Prepare)
  • 类变量 分配内存,并且设置该类变量的默认初始值,即零值。
  • 这里不包含用final 修饰的static常量,因为final在编译的时候就已经分配了,准备阶段会显式初始化(确定数值)
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆区。
public class Demo{
    private static int a = 1;	//prepare:a = 0  ---> initial:a = 1
}
(3) 解析(Resolve)
  • 将常量池内的符号引用转换为直接引用的过程。
  • 事实上,解析操作往往伴随着JVM在执行完初始化之后再执行。
  • 符号引用:一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
  • 直接引用:直接指向目标的指针、相对偏移量/一个间接定位到目标的句柄。
  • 解析动作主要针对类/接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
③ 初始化
  • 初始化阶段就是执行类构造器方法 <clinit>() 的过程
  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
  • 构造器方法中指令按语句在源文件中出现的顺序执行
  • <clinit>() 不同于类构造器。(关联:构造器是虚拟机视角下的<init>())
  • 若该类具有父类,JVM会保证子类的 <clinit>() 执行前,父类 <\clinit> 已经执行完毕
  • 虚拟机必须保证一个类的 <\clinit> 方法在多线程下被同步加锁
静态代码块 - 测试代码:

此方法不需定义,是javac编译器自动收集类中的所有 类变量的赋值动作 + 静态代码块中的语句 合并而来。

public class ClassInitTest {
    private static int num = 1;

    // number:按语句在源文件中出现的顺序执行
    static {
        num = 2;
        number = 20;
    }
	
    private static int number = 10;          //linking--prepare:number = 0 --> initial:20 --> 10

    public static void main(String[] args) {
        System.out.println("num : " + ClassInitTest.num);
        System.out.println("number : " + ClassInitTest.number);
    }
}

测试结果:

测试结果

找到“ 方法 -> <clinit> -> Code”,查看字节码文件:

clinit

非法的前向引用 :可以提前赋值,但无法在静态代码块中 调用 在后面声明的静态变量

非法的前向引用

无静态代码块 & 静态变量(类变量) - 测试代码:

此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

public class ClinitTest {

    private int a = 1;

    public static void main(String[] args) {
        int b = 2;
    }
    
}

显然,当未定义 静态代码块时,并不会生成 <clinit> 方法。

不会自动生成clinit

父子类 - 测试代码:

若该类具有父类,JVM会保证子类的 <clinit>() 执行前,父类 <\clinit> 已经执行完毕。

public class ClinitTest1 {

    static class Father {
        public static int A = 1;

        static {
            A = 2;
        }
    }

    static class Son extends Father {
        public static int B = A;
    }

    public static void main(String[] args) {
        // 加载Father类,其次加载Son类。
        System.out.println(Son.B);  //2
    }
}

测试结果:

结果

多线程 - 测试代码
public class DeadThreadTest {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "开始");
            DeadThread dead = new DeadThread();
            System.out.println(Thread.currentThread().getName() + "结束");
        };

        Thread t1 = new Thread(runnable, "线程1");
        Thread t2 = new Thread(runnable, "线程2");

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

class DeadThread {
    static {
        if (true) {
            System.out.println(Thread.currentThread().getName() + "初始化当前类");
            // 加锁(模拟出现问题,使得其他线程进入阻塞状态)
            while (true) {

            }
        }
    }
}

测试结果表明,该类只会被加载一次:

结果

2.2 类加载器分类

  • JVM支持两种类型的类加载器,分别为 引导类加载器(BootStrap ClassLoader)自定义类加载器(User-Defined ClassLoader)
  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这样定义,而是将 所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器
  • 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有三个(BootStrap ClassLoader、Extension ClassLoader、Application ClassLoader)。

类加载器分类

测试代码:

public class ClassLoaderTest {
    public static void main(String[] args) {

        // 获取系统当前类加载器:应用程序类加载器(ApplicationClassLoader)
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);      // sun.misc.Launcher$AppClassLoader

        // 获取其上层类加载器:扩展类加载器(ExtensionClassLoader)
        ClassLoader extensionClassLoader = systemClassLoader.getParent();
        System.out.println(extensionClassLoader);   // sun.misc.Launcher$ExtClassLoader

        // 获取其上层类加载器:引导类加载器加载(BootStrapClassLoader)
        ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
        System.out.println(bootstrapClassLoader);   // null

        // 对于用户自定义类:默认使用系统类加载器加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);            // sun.misc.Launcher$AppClassLoader

        // String类(Java 核心类库):默认使用引导类加载器加载
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader);
    }
}

测试结果:

结果

2.2.1 sun.misc.Launcher类

sun.misc.Launcher它是一个Java虚拟机的入口应用。这个类里面又继承了ExtClassLoader、AppClassLoader、bootstrap加载 类 + 资源 的url路径(bootClassPath)。

Launcher

2.2.2 类加载器分类

  • 启动类加载器(引导类加载器,Bootstrap ClassLoader)[虚拟机内置的加载器]
    • 这个类加载使用 C/C++语言 实现的,嵌套在JVM内部。
    • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar [rt表示RunTime] + resource.jarsun.boot.class.path路径下的内容),用于提供JVM自身需要的类
    • 并不继承自java.lang.ClassLoader,没有上级加载器
    • 加载 扩展类类加载器(ExtClassLoader)、应用程序类加载器(AppClassLoader),并指定为它们的上级加载器
    • 出于安全考虑,Bootstrap启动类加载只加载包名为java、javax、sun等开头的类。
  • 扩展类加载器(Extension ClassLoader)[虚拟机自带的加载器]
    • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
    • 派生于ClassLoader类
    • 上级加载器为启动类加载器
    • java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录的 jre/lib/ext子目录(扩展目录)下加载类库。 如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
  • 应用程序加载器(Application Class Loader)[虚拟机自带的加载器]
    • Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
    • 派生于ClassLoader类
    • 上级加载器为扩展类加载器
    • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
    • 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
    • 通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器
  • 用户自定义类加载器
    • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器互相配合执行的,在必要时,我们还可以自定义类加载器,来制定类的加载方式。
    • 为什么要自定义类加载器?
      • 隔离加载类(多用于中间件,对类做仲裁以防止类冲突)
      • 修改类加载的方式
      • 扩展加载源
      • 防止源码泄露

测试代码:

public class ClassLoaderTest1 {
    public static void main(String[] args) {
        System.out.println("================启动类加载器================");
        // 获取BootStrapClassLoader能够加载的API路径
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urls) {
            System.out.println(element.toExternalForm());
        }
        // 从上面的路径中随意选择一个类,来看看它的类加载器 : 引导类
        System.out.println("*********从上面的路径中随意选择一个类 Provider,来看看它的类加载器*********");
        ClassLoader classLoader1 = Provider.class.getClassLoader();
        System.out.println(classLoader1);

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

        // 从上面的路径中随意选择一个类,来看看它的类加载器 : 扩展类
        System.out.println("*********从上面的路径中随意选择一个类 CurveDB,来看看它的类加载器*********");
        ClassLoader classLoader2 = CurveDB.class.getClassLoader();
        System.out.println(classLoader2);
    }
}

测试结果:

结果

2.2.3 用户自定义类加载器实现步骤

  1. 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  2. 在JDK1.2之前,在 自定义类加载器时,总会继承ClassLoader类并重写loadClass()方法,从而实现自定义类加载器但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义类的类加载逻辑写在findClass()方法中。
  3. 在编写自定义类加载器时,如果没有太过复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

2.2.4 ClassLoader 常用方法 & 获取途径

① 常用方法

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

方法名称描述
getParent()返回该类加载器的超类加载器(上级类加载器)
loadClass(String name)加载名称为 name 的类,返回结果为 java.lang.Class 类的实例
findClass(String name)查找名称为 name 的类,返回结果为 java.lang.Class 类的实例
findLoadedrClass()查找名称为 name 的已经被加载过的类,返回结果为 java.lang.Class 类的实例
defineClass(String name, byte[] b,int off, int len)把字节数组 b 中的内容转换为一个Java类,返回结果为 java.lang.Class 类的实例
resolveCLass(Class<?> c)连接指定的一个Java类

加载类方式:

  • loadClass(String name)
  • findClass(String name) + defineClass(String name, byte[] b,int off, int len)
② 获取途径
  • 获取当前类的ClassLoader
    • clazz.getClassLoader()
  • 获取当前线程上下文的ClassLoader
    • Thread.currentThread().getContextClassLoader()
  • 获取系统的ClassLoader
    • ClassLoader.getSystemClassLoader()
  • 获取调用者的ClassLoader
    • DriverManager.getCallerClassLoader()

2.3 双亲委派机制

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

2.3.1 工作原理

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器(父 类加载器 / 上级加载器)去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。

类加载

所以,如果我们尝试自己写一个java.lang.String类,如果没有放在合适的路径,是不会被加载的(被加载的是JDK自带的java.lang.String类)

举例

举例

2.3.2 优势

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
    • 自定义java.lang.*已存在的类:java.lang.String
    • 自定义java.lang.*不存在的类:java.lang.LjwStart

自定义java.lang.*不存在的类,测试结果:

java.lang.SecurityException: Prohibited package name: java.lang

加载失败

2.3.3 沙箱安全机制

自定义java.lang.String类(写main方法),但是在加载自定义类java.lang.String类的时候会率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载JDK自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是 沙箱安全机制

沙箱安全机制

2.4 补充

2.4.1 判断两个class对象是否为同一个类

  • 在JVM中表示两个class对象是否为同一个类存在的两个必要条件:
    • 类的完整类名必须一致,包括包名。
    • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
  • 在JVM中,即使两个类对象(class对象)来源于同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

2.4.2 对类加载器的引用

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会 将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用时,JVM需要保证 这两个类型的类加载器时相同的。

2.4.3 类的主动使用、被动使用

Java程序对类的使用方式分为:主动使用、被动使用

  • 主动使用 7种情况:
    • 创建类的实例
    • 访问某个 类/接口 的静态变量,或对该静态变量赋值
    • 调用类的静态方法
    • 反射(比如:Class.forName(“com.xxx.Test”))
    • 初始化一个类的子类
    • Java虚拟机启动时被标明为启动类的类
    • JDK 7 开始提供的动态语言支持:
      java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
  • 除7种情况外,其他使用Java类的方式都看作是对类的 被动使用,都 不会导致类的 初始化

3. 运行时数据区 & 线程

3.1 运行时数据区

内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统、应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行 不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。

运行时数据区

非堆空间:MetaSpace (元数据区) + CodeCache (JIT编译产物)


Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区。

  • 其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。
  • 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

灰色区域为单线程私有的,红色区域为多个线程共享的。

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

堆区 - 方法区

关于线程间共享说明

线程间共享说明

每个JVM只有一个Runtime实例,即运行时环境。

3.2 线程

  • 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
  • 在HotSpot JVM里,每个线程都与操作系统的本地线程直接映射。
    • 当一个Java线程准备好执行后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用 Java线程 中的 run()方法。

当线程中最后一个非守护线程停止时,JVM也随之停止。

  • 若使用jconsole或其他调试工具,可以看到后台有许多线程在运行。这些后台线程不包括调用public static void main(String[] args)的main线程以及所有这个main线程自己创建的线程。
  • 这些主要的后台系统线程在HotSpot JCM里主要是以下几个:
    • 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是它们都需要JVM到达安全点,这样堆才不会变化。这种线程的执行类型包括 “stop-the-world” (让所有应用程序中止) 的垃圾收集、线程栈收集、线程挂起、偏向锁撤销。
    • 周期任务线程:这种线程时时间周期事件的体现(比如中断),它们一般用于周期性操作的调度执行。
    • GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
    • 编译线程:这种线程在运行时会将字节码编译成本地代码。
    • 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

4. 程序计数器(PC寄存器)

JVM 中的程序计数器(ProgramCounter Register)中,Register 的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。

这里,并非是广义上的物理寄存器,或许将其翻译为PC计数器(指令计数器)会更为贴切(也称程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

程序计数器(PC寄存器)

4.1 作用:

PC寄存器用来存储指向下一条指令的地址(相当于行号标识),也即将要执行的指令代码。由执行引擎读取下一条指令。

4.2 介绍

  • 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
  • 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的 当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)。
  • 它是程序控制流的治时期,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 字节码解释其工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一个在 Java虚拟机规范 中没有规定任何 OutOfMemoryError 情况的区域(也没有GC)。

4.3 举例说明

测试代码 - JClassLib ByteCode Viewer:

public class PCRegisterTest {
    public static void main(String[] args) {
        MinusTest test = new MinusTest();
        test.minus();
    }

    public int minus() {
        int c = 3;
        int d = 4;
        return c - d;
    }
}

结果

测试代码 - javap -v PCRegisterTest1.class

public class PCRegisterTest1 {
    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;

        String s = "abc";
        System.out.println(i);
        System.out.println(k);
    }
}

解释图

4.4 常见问题

4.4.1 使用寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?

因为CPU需要不停地切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

4.4.2 PC寄存器为什么会被设定为线程私有?

由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器/多核处理器中的一个内核,只会执行某个线程中的一条指令。

多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确纪录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,不会出现相互干扰的情况。

CPU时间片

CPU时间片,即CPU分配个各个程序的时间,每个线程被分配一个时间段,称作它的时间片。

  • 在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。

  • 在微观上:由于只有一个 CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行 。

CPU时间片

5. 虚拟机栈

5.1 虚拟机栈概述

5.1.1 虚拟机栈出现的背景

由于跨平台的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的(基于寄存器架构 与具体CPU耦合度较高)。

  • 优点:跨平台,指令集小,编译器容易实现
  • 缺点:性能下降,实现同样的功能需要更多的指令

5.1.2 内存中的栈/堆

栈是运行时的单位,而堆是存储的单位。

  • 栈是解决程序的运行问题,即程序如何执行,即如何处理数据。
  • 堆解决数据存储问题,即数据怎么放、放在哪。

栈/堆

5.1.3 虚拟机栈基本内容

  • Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应着一次次的Java方法调用。
  • 线程私有
  • 生命周期与线程一致
① 作用

主管Java程序的运行,它保存方法的局部变量 (8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

  • 局部变量 vs 成员变量 (属性)
  • 基本数据类型 vs 引用类型变量 (类、数组、接口)
② 特点(优点)
  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接堆Java栈的操作只有两个:
    • 每个方法执行,伴随着进栈(入栈、压栈)
    • 执行结束后的出栈工作
  • 对栈来说不存在垃圾回收问题
    • 一般两方面考虑:GC、OOM

5.1.4 栈中可能出现的异常

Java虚拟机规范允许 Java栈的大小是动态的或是固定不变的

  • 如果采用固定 大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,Java虚拟机将会抛出一个 StackOverflowError 异常。
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建的新线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常。

测试代码 - StackOverflowError:

public class StackErrorTest {
    public static void main(String[] args) {
        main(args);
    }
}

测试结果:
结果

5.1.5 设置栈内存大小

我们可以使用参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

设置栈大小

/*
 * -Xss 用来设置栈空间初始大小
 *      X :JVM 运行参数
 *      ss:stack start 缩写
 */

测试代码 - 默认栈大小(Windows):

public class StackTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}

测试结果:

测试结果

默认情况下约为11404,设置栈大小:-Xss256k。

设置

设置后,再次测试:

再次测试

结果约为2461,显然,设置生效。

5.2 栈的存储单位

5.2.1 栈中存储单位——栈帧(Stack Frame)

  • 每个线程都有自己的栈,栈中的数据都是以 栈帧(Stack Frame)的格式存在
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

复习

OOP基本概念:类、对象

类中基本结构:field(属性、字段、域)、method

5.2.2 栈运行原理

  • JVM直接对Java栈的操作只有两个,就是对栈帧的 压栈出栈遵循 “先进先出 / 后进先出” 原则

  • 在一条活动线程中 ,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为 当前栈帧(Current Frame),与当前栈帧相对应的方法就是 当前方法(Current Method),定义这个非法的类就是 当前类(Current Class)

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

  • 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在站的顶端,成为新的当前帧。

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

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

  • Java方法有两种返回函数的方法:

    一种是正常的函数返回,使用return指令;另一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

栈运行原理

测试代码:

public class StackFrameTest {

    public static void main(String[] args) {
        StackFrameTest test = new StackFrameTest();
        test.method1();
    }

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

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

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

测试结果(忽略main,当前栈帧不断变化):
测试结果

使用IDEA环境DEBUG程序:

DEBUG

5.2.3 栈帧的内部结构

每个栈帧中存储着:

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

解释图

d动态链接

解释图2

① 局部变量表(Local Variables)
  • 局部变量表也被称之为 局部变量数组 / 本地变量表。
  • 定义一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此 不存在数据安全问题(不涉及数据共享)。
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code属性 的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。

测试代码:

public class LocalVariablesTest {
    
    String str = "ABC";

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

测试结果:

结果

结果


<1> LineNumberTable

在 PC中生效行号 与 代码中生效行号见建立映射关系。

结果

<2> LocalVariableTable

用以描述 本地变量的详细信息。

本地变量表

举例说明:

  • 举例1:举例1
    举例1

  • 举例2:

    举例

    举例2


※ 关于Slot的理解
  • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度-1的索引结束。
  • 局部变量表,最基本的存储单元是Slot(变量槽)
  • 局部变量表中存放编译期可知的各种基本数据类型(8种)、引用类型(reference)、returnAddress 类型的变量。
  • 在局部变量表里,32位以内的类型只占用一个slot(包括 returnAddress 类型),64位的类型(long 和 double)占用两个slot。
    • byte、short、char 在存储前被转化为 int,boolean 也被转化为 int(0 表示 false、非0 表示 true)。
    • long 和 double 则占据两个 Slot。

Slot

  • JVM会为局部变量表中的每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

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

  • 如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问 long / double 类型变量)

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

    展示

  • 如果当前帧是由 构造方法/实例方法 创建的,那么 该对象引用this将会存放在index为0的Slot处,其余的参数按照参数表顺内需继续排列。

问:为什么static 方法 不允许使用 this变量?

  • 答:因为this变量不存在于当前方法的局部变量表中。

问:为什么 构造方法/实例方法 允许使用 this变量?

  • 如果当前帧是由 构造方法/实例方法 创建的,那么 该对象引用this将会存放在index 为 0 的 Slot处,所以可以使用。
  • 若未接收另一个方法返回值,则它不会在本地变量表中出现。

    public void test1() {
        Date date = new Date();
        String name1 = "com.ljw";
        /
        // test2 有 String类型 返回值
        test2(date, name1);
        /
        
        //String name = test2(date, name1);
        System.out.println(date + name1);
    }
    

    展示

    展示

※ Slot的重复利用
  • 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用过期局部变量的槽位, 从而 达到节省资源的目的
public class SlotTest {
    public void localVar() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        // 变量c使用之前已经销毁的变量b占据的slot的位置(复用)
        int c = a + 1;
    }
}

解释

解释


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

变量的分类:

  • 按照数据类型分:
    • 基本数据类型
    • 引用数据类型
  • 按照在类中声明的位置分:
    • 成员变量:在使用前,都经历过默认初始化赋值
      • 类变量:linking的prepare阶段:给类变量默认赋值 —> initial阶段:给类变量显式赋值(静态代码块赋值)
      • 实例变量:随着对象的创建,会在对空间中分配实例变量空间,并进行默认赋值
    • 局部变量:在使用前,必须要进行显式赋值!否则,编译不通过!
※ 补充说明
  • 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
② 操作数栈(Operand Stack)
  • 每一个独立的栈帧中除了包含局部变量表以外,还包含一个 后进先出(Last-In-First-Out)的 操作数栈(Operand Stack),也可以称之为 表达式栈(Expression Stack)。
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据/提取数据,即入栈 (push) / 出栈 (pop)。(结合执行引擎,帮助执行逻辑过程)
    • 某些字节码指令将值压入操作数栈,其余字节码指令将操作数取出栈。使用它们后再把结果压入栈。
    • 比如:执行复制、交换、求和等操作。

操作数栈

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

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

  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。

    操作数栈

  • 栈中的任何一个元素都可以是任意的Java数据类型。

    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 操作数栈 并非采用访问索引的方式来进行数据访问 的,而是只能通过标准的入栈 (push) 和 出栈 (pop) 操作来完成一次数据的访问。

  • 如果被调用的方法带有返回值,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

  • 另外,我们说Java虚拟机的 解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈。

※ 代码追踪
public class OperandStackTest {
    public void testAddOperation() {
        // byte、short、char、boolean:都以int型来保存
        byte i = 15;
        int j = 8;
        int k = i + j;
    }
}

JClasslib ByteCode Viewer:

展示

javap -v OperandStackTest.class:

javap

动图展示

其中 iadd 操作,需要执行引擎将其翻译为机器指令,由CPU运算。


方法调用 - 接收变量:

public int getSum(){
    int m = 10;
    int n = 20;
    int k = m + n;
    return k;
}

public void testGetSum() {
    // 获取上一个栈帧返回的结果,并保存在操作数栈中
    int i = getSum();
    int j = 10;
}

加载结果:

加载结果


补充

public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;

    int m = 8;
    // int m = 800;
    // int m = 800000;
}

bipush代表由byte类型承载转为int类型。

bipush

sipush代表由short类型承载转为int类型。

sipush

ldc(Push item from run-time constant pool)代表将值放入运行时常量池。

ldc

5.2.4 栈顶缓存 (Top-of-Stack-Caching) 技术

基于栈架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数 和 内存读/写次数。

由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(ToS, Top-of-Stack-Caching)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

5.2.5 动态链接(Dynamic Linking)

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

帧数据区:方法返回地址 + 动态链接 + 一些附加信息

  • 每个栈帧内部都包含一个指向 运行时常量池该栈帧所述方法的引用。包含这个引用的目的是为了支持当前方法的代码能够实现 动态链接(Dynamic Linking)。比如:invokedynamic 指令。
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么 动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用

动态链接

测试代码:

public class DynamicLinkingTest {

    int num = 10;

    public void methodA() {
        System.out.println("methodA()...");
    }

    public void methodB() {
        System.out.println("methodB()....");

        methodA();

        num++;
    }

    public void methodC() {
        System.out.println("methodC()....");

        methodA();

        num++;
    }
}

测试结果:

结果

结果

问:为什么不直接将方法放在栈帧中?

  • 答:节省空间。方法的结构是固定的,它们就像器械一样,集中存放在运行时常量池,只需要一份,当我们想要使用的时候,指向它就可以了。

问:为什么需要运行时常量池?

  • 答:常量池的作用:为了提供一些符号、常量,便于指令的识别。

5.2.6 方法的调用

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

  • 静态链接 :

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

  • 动态链接 :

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

对应的方法的绑定机制为:早期绑定(Early Binding)和 晚期绑定(Late Binding)。绑定是一个字段、方法、类在符号引用被替换为直接引用的过程,这仅仅发生一次。

  • 早期绑定 :

    早期绑定就是指被调用的 目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,依然你也就可以使用 静态链接的方式将符号引用转换为直接引用。

  • 晚期绑定 :

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

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 {
    @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 huntable) {
        huntable.hunt();  //表现为:晚期绑定
    }
}

虚方法

虚方法

随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承、多态等面向对象特征,既然 这一类编程语言具备多态特性,那么自然也就具备 早期绑定、晚期绑定 两种绑定方式。

Java中任何一个普通的方法起始都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。Java中的抽象方法则对应纯虚函数。

※ 虚方法 & 非虚方法

非虚方法:

  • 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为 非虚方法
  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  • 其他方法称为虚方法。

子类对象的多态性的使用前提:

  1. 类的继承关系
  2. 方法的重写

虚拟机中提供了以下几条方法调用指令:

  • 普通调用指令
    1) invokestatic:调用静态方法,解析阶段确定唯一方法版本
    2) invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
    3) invokevirtual:调用所有虚方法(final修饰的除外)
    4) invokeinterface:调用接口方法
  • 动态调用指令:
    5) invokedynamic:动态解析出需要调用的方法,然后执行

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

class Father {
    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 普通方法");
    }
}

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("com.ljw");
        // invokestatic
        super.showStatic("good!");
        // invokespecial
        showPrivate("hello!");
        // invokespecial
        super.showCommon();
        // invokevirtual(final:不认为其为虚方法[隐式])
        showFinal();
        // invokespecial(final:不认为其为虚方法[显式])
        super.showFinal();

        /* 虚方法 */
        // invokevirtual
        showCommon();
        // invokevirtual
        info();

        MethodInterface in =  null;
        /* 虚方法 */
        // invokeinterface
        in.methodA();

    }

    public void info() {

    }
}

interface MethodInterface {
    void methodA();
}

JClasslib Bytecode Viewer:

展示

方法调用:关于invokedynamic指令
  • JVM字节码指令集一直比较稳定 ,直到Java7增加了 invokedynamic 指令,这是 Java为了实现「动态类型语言」支持而做的一种改进。

  • 但是在Java7中并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 Java8 的 Lambda 表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。

  • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这里相对来讲较为复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。

动态类型语言静态类型语言

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

  • 静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个特征。

方法调用:方法重写的本质

Java语言中方法重写的本质:

  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
  2. 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.alng.IllegalAccessError 异常。
  3. 否则,按照继承关系从下往上,以此对 C 的各个父类进行第 2 步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。

IllegalAccessError 介绍:

  • 程序试图访问/修改一个属性或调用一个方法,这个属性或方法,若没有权限访问,一般会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
方法调用:虚方法表
  • 在面向对象的编程中 ,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标,就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个 虚方法表(virtual method table)(非虚方法不会出现在表中) 来实现。使用索引表来代替查找。
  • 每个类都有一个虚方法表,表中存放着各个方法的实际入口。
  • 虚方法表的创建时机:
    • 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。

虚方法表

interface Friendly {
    void sayHello();
    void sayGoodbye();
}

class Dog {
    public void sayHello() {
    }

    @Override
    public String toString() {
        return "Dog";
    }
}

class Cat implements Friendly {

    public void eat() {
    }
    
    @Override
    public String toString() {
        return "Cat";
    }

    @Override
    public void sayHello() {
    }

    @Override
    public void sayGoodbye() {
    }

    @Override
    protected void finalize() {
    }
}

class CockerSpaniel extends Dog implements Friendly {

    @Override
    public void sayHello() {
        super.sayHello();
    }

    @Override
    public void sayGoodbye() {
    }
}

public class VirtualMethodTable {
}

虚方法表

虚方法表

虚方法表

5.2.7 方法返回地址(Return Address)

  • 存放调用该方法的PC寄存器的值。
  • 一个方法的结束,有两种方式:
    • 正常执行完成
    • 出现未处理的异常,非正常退出
  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常 表来确定,栈帧中一般不会保存这部分信息。

当一个方法开始执行后,只有两种 方式可以退出这个方法:

  1. 执行引擎遇到任意一个方法返回的字节码指令 (return),会有返回值传递给上层 的方法调用者,简称 正常完成出口
    • 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
    • 在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short、int类型时使用)、lreturn、freturn、dreturn、areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
  2. 在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口
    • 方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便再发生异常的时候找到处理异常的代码。

异常处理表

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

5.2.8 一些附加信息

栈帧中还允许携带与 Java虚拟机的实现 相关的一些附加信息(对程序调试提供支持的信息)。

5.3 相关问题

  • 举例栈溢出的情况(StackOverflowError)
    • 通过-Xss设置栈的大小
    • 动态变化 ( 自动扩容 ),无法扩容 OOM
  • 调整栈大小,就能保证不出现溢出吗?
    • 不能保证一定不出现溢出
  • 分配的栈内存越大越好吗?
    • 不一定,会延长StackOverflowError出现的时间,但会使得空间可容纳线程数变少、内存可用空间变小
  • 垃圾回收是否会涉及到虚拟机栈?
    • 不会的
  • 方法中定义的局部变量是否线程安全?
    • 具体问题具体分析

何为线程安全?

  • 如果只有一个线程才可以操作此数据,则比是线程安全的。
  • 如果有多个线程操作此数据,则此数据是共享 数据。如果不考虑同步机制,会存在线程安全问题。
/* 涉及 “逃逸分析” 问题 */
public class StringBuilderTest {

    // s1的声明方式:线程安全的(内部产生 -> 内部消亡)
    public static void method1() {
        //StringBuilder:线程不安全
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        //...
    }

    // stringBuilder的操作过程:线程不安全
    public static void method2(StringBuilder stringBuilder) {
        stringBuilder.append("a");
        stringBuilder.append("b");
    }

    // s1的操作:存在线程不安全问题(可能有多个线程操作此返回值)
    public static StringBuilder method3() {
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1;
    }
    // s1的操作:线程安全的(内部产生 -> 内部消亡)
    // 返回后的 s1.toString():存在线程不安全问题(可能有多个线程操作此返回值)
    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);

    }
}
ErrorGC
程序计数器××
虚拟机栈×
本地方法栈×
方法区

6. 本地方法接口

6.1 什么是本地方法?

简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现(比如C)。这个特征并非Java所特有,很多其他的编程语言都有这一机制,比如在C++中你可以用 extern “C” 告知 C++ 编译器去调用一个C的函数。

“A native method is a Java Method whose implementation is provided by non-java code.”

在定义一个 Native Method 时,并不提供实现体(有些像定义一个Java Interface),因为其实现体是由非Java语言在外面实现的。

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++ 程序。

public class IHaveNatives {
    public native void native1(int x);

    public native static long native2();

    private native synchronized float native3(Object o);

    native void native4(int[] arr) throws Exception;
}

标识符native可以与所有其他java标识符连用(abstract 除外)

6.2 为什么要使用Native Method?

Java 使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

  • 与Java环境外交互:
    • 有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。当Java需要与一些底层系统(Java应用与 操作系统/某些硬件 交换信息时)
    • 本地方法正是这样一种交流机制:他为我们提供了一个非常简介的接口,而且我们无需去了解Java应用之外的繁琐细节。
  • 与操作系统的交互:
    • JVM支持着 Java语言本身 和 运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些链接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。
    • 通过使用 本地方法,我们的一用Java实现了jre与底层系统的交互,甚至JVM的一些部分就是用C写的。
    • 还有,如果我们要使用一些Java本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
  • Sun’s Java:
    • Sun的解释器是用C写的,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。
    • 例如:类java.lang.Thread 的 setPriority() 方法是用Java实现的,但是它实现调用的是该类里的本地方法 setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在 Windows 95 的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

6.3 现状

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等。

7. 本地方法栈

本地方法栈(Native Method Stack / C Stack)

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
  • 本地方法栈,线程私有。
  • 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 StackOverflowError 异常。
    • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个 OutOfMemoryError 异常。
  • 本地方法是使用C语言实现的。
  • 它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库。

本地方法栈

  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
    • 它甚至可以直接使用本地处理器中的寄存器。
    • 直接从本地内存的堆中分配任意数量的内存。
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
  • 在 HotSpot JVM 中,直接将 本地方法栈 和 虚拟机栈 合二为一。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值