0%

查缺补漏-Jvm

本章是整理知识内容,为强化知识长期更新。

JVM的内存布局

    • 堆(Java Heap) 也叫 Java 堆或者是 GC 堆,它是一个线程共享的内存区域,也是 JVM 中占用内存最大的一块区域,Java 中所有的对象都存储在这里。

      • Java堆区具有下面几个特点:
        • 存储的是我们new来的对象,不存放基本类型和对象引用。
        • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。
        • 线程共享区域,因此是线程不安全的。
        • 能够发生内存溢出,主要有OutOfMemoryError和StackOverflowError。
      • 那么什么时候发生OutOfMemoryError,什么时候发生StackOverflowError?虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常,线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常
      • Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。具体比例参数的话,可以看一下下面这张图。

  1. 方法区

    • 方法区(Method Area) 也被称为非堆区,用于和“Java 堆”的概念进行区分,它也是线程共享的内存区域,用于存储已经被 JVM 加载的类型信息、常量、静态变量、代码缓存等数据。
  2. 程序计数器

    • 程序计数器(Program Counter Register) 线程独有一块很小的内存区域,保存当前线程所执行字节码的位置,包括正在执行的指令、跳转、分支、循环、异常处理等。
  • Java栈具备的特点

  • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。

  • 存放基本数据类型以及对象的引用。

  • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。

  • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。

    • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
  1. 虚拟机栈

    • 虚拟机栈也叫 Java 虚拟机栈(Java Virtual Machine Stack),和程序计数器相同它也是线程独享的,用来描述 Java 方法的执行,在每个方法被执行时就会同步创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。当调用方法时执行入栈,而方法返回时执行出栈。
  2. 本地方法栈

    • 本地方法栈(Native Method Stacks)与虚拟机栈类似,它是线程独享的,并且作用也和虚拟机栈类似。只不过虚拟机栈是为虚拟机中执行的 Java 方法服务的,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
  • JVM 的执行流程是,首先先把 Java 代码(.java)转化成字节码(.class),然后通过类加载器将字节码加载到内存中,所谓的内存也就是我们上面介绍的运行时数据区,但字节码并不是可以直接交给操作系统执行的机器码,而是一套 JVM 的指令集。这个时候需要使用特定的命令解析器也就是我们俗称的执行引擎(Execution Engine)将字节码翻译成可以被底层操作系统执行的指令再去执行,这样就实现了整个 Java 程序的运行,这也是 JVM 的整体执行流程。

Java虚拟机是如何加载Java类的

符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

  • 类的生命周期会经历以下 7 个阶段:

  1. 加载阶段(Loading)
  2. 验证阶段(Verification)
  3. 准备阶段(Preparation)
  4. 解析阶段(Resolution)
  5. 初始化阶段(Initialization)
  6. 使用阶段(Using)
  7. 卸载阶段(Unloading)
  • 具体含义
  1. 加载阶段
    此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。

    • Java语言的类型可以分为两大类:基本类型、引用类型。基本类型是由虚拟机预先定义好的,所以不会经历单独的类加载过程。而引用类型又分为四种:类、接口、数组类、泛型参数。由于泛型参数会在编译的过程中被擦除(关于类型擦除的知识,大家可以查下资料),所以在Java中只有类、接口、数组类三种类型需要经历JVM对其进行连接和初始化的过程。
  2. 验证阶段
    此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。

    • 验证的主要动作大概有以下几个:
      • 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等;
      • 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等;
      • 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑;
      • 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。
  3. 准备阶段
    此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。这些变量所使用的内存都将在方法区(<Jdk1.8)元数据区(>=Jdk1.8)中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在Java堆中。

  4. 解析阶段
    此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。

    1. 所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
    2. 符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。
  5. 初始化
    初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。

    1. JVM 规范枚举了下述多种触发情况:
       1. 当虚拟机启动时,初始化用户指定的主类;
       2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
       3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
       4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;子类的初始化会触发父类的初始化;
       5. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
       6. 使用反射 API 对某个类进行反射调用时,初始化这个类;
       7. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。