java虚拟机篇二(进阶)

81 阅读55分钟

java虚拟机线程

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

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

在这里插入图片描述 需要注意的是:

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

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

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

虚拟机的系统线程

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

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

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

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

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

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

运行时数据区

Java虚拟机所管理的内存将会包括以下几个运行时数据区域。运行时数据区主要可以划分五种:

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

在这里插入图片描述

程序计数器

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

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

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

java虚拟机栈

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

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

本地方法栈

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

《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里笔者写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。

根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

首先来看一下源代码:

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;
    }
}

反编译后的字节码文件 在这里插入图片描述 代码追踪流程:

第一步: 首先执行偏移地址为0的指令,Bipush指令的作用是将单字节的整形常量值(-127~128)推入操作数栈顶,跟随有一个参数,指明推送的常量值为100。如图所示: 在这里插入图片描述 第二步: 执行偏移地址为2的指令,istore_1指令的作用是操作数栈顶的整形值出栈并存放到第一个局部变量槽中,后续4条指令(一直到偏移地址为11的指令为止)都是做的一样的事情,也就是在对应的代码中把变量a,b,c赋值100,200,300。 在这里插入图片描述 第三步: 执行偏移地址为11的指令,iload_1指令的作用是将局部变量表的第一个变量槽的整形值复制到操作数栈顶。 在这里插入图片描述 第四步: 执行偏移地址为12的指令,iload_2指令的作用与iload_1类似,把第二个变量槽的整形值入栈。 在这里插入图片描述 第五步: 执行偏移地址为13的指令,iadd指令的作用是将操作数栈顶的头两个栈顶元素出栈,做整形加法,然后把结果入栈,在iadd指令执行完毕,栈中的原有的100,200被出栈,它们的和和300被从新入栈。

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

第七步: 执行偏移地址为16的指令,ireturn指令是方法返回指令之一,它将结束方法执行并将操作数栈顶的整形值返回给该方法的调用者,整个过程执行结束。 在这里插入图片描述

程序计数器

程序计数器在java代码运行的过程中有着至关重要的功能,如果没有程序计数器,Java程序中的流程控制将无法得到正确的控制,多线程也无法正确的轮换。关于程序计数器的详细解说,请参考这篇文章 实例代码:

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);
    }
}

字节码文件:

 /**
     * Constant pool:
     *    #1 = Methodref          #7.#26         // java/lang/Object."<init>":()V
     *    #2 = String             #27            // 123 (常量池)
     *   #27 = Utf8               123 // 获取123的值
     *   #37 = Utf8               Ljava/io/PrintStream;
     * {
     *   public CrazyTest.PcRegister.PCRegisterTest();
     *     descriptor: ()V
     *     flags: ACC_PUBLIC
     *     Code:
     *       stack=1, locals=1, args_size=1
     *          0: aload_0
     *          1: invokespecial #1                  // Method java/lang/Object."<init>":()V
     *          4: return
     *       LineNumberTable:
     *         line 3: 0
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0       5     0  this   LCrazyTest/PcRegister/PCRegisterTest;
     *
     *   public static void main(java.lang.String[]);
     *     descriptor: ([Ljava/lang/String;)V
     *     flags: ACC_PUBLIC, ACC_STATIC
     *     Code:
     *       stack=3, locals=4, args_size=1
     *          0: bipush        10
     *          2: istore_1
     *          3: bipush        20
     *          5: istore_2
     *          6: ldc           #2                  // String 123
     *          8: astore_3
     *          9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *         12: aload_3
     *         13: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     *         16: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *         19: iload_2
     *         20: iload_1
     *         21: isub
     *         22: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
     *         25: return
     *       LineNumberTable:
     *         line 5: 0
     *         line 6: 3
     *         line 8: 6
     *         line 9: 9
     *         line 10: 16
     *         line 11: 25
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0      26     0  args   [Ljava/lang/String;
     *             3      23     1     a   I
     *             6      20     2     b   I
     *             9      17     3     c   Ljava/lang/String;
     * }
     * SourceFile: "PCRegisterTest.java"
     */

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虚拟机栈的访问速度仅次与程序计数器,而且对栈来说不存在垃圾回收问题) 在这里插入图片描述 模拟栈异常情况:

/**
 *  栈异常情况
 *  默认情况下:count : 11420
 *  设置栈的大小: -Xss256k : count : 2465
 */
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<String> 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();
    }
    /**
     * {
     *   public CrazyTest.VirtualStackTest.VirtualStack01();
     *     descriptor: ()V
     *     flags: ACC_PUBLIC
     *     Code:
     *       stack=1, locals=1, args_size=1
     *          0: aload_0
     *          1: invokespecial #1                  // Method java/lang/Object."<init>":()V
     *          4: return
     *       LineNumberTable:
     *         line 3: 0
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0       5     0  this   LCrazyTest/VirtualStackTest/VirtualStack01;
     *
     *   public void methodA();
     *     descriptor: ()V
     *     flags: ACC_PUBLIC
     *     Code:
     *       stack=1, locals=3, args_size=1
     *          0: iconst_1
     *          1: istore_1
     *          2: bipush        10
     *          4: istore_2
     *          5: aload_0
     *          6: invokevirtual #2                  // Method methodB:()V 调用方法B
     *          9: return
     *       LineNumberTable:
     *         line 5: 0
     *         line 6: 2
     *         line 7: 5
     *         line 8: 9
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0      10     0  this   LCrazyTest/VirtualStackTest/VirtualStack01;
     *             2       8     1     a   I
     *             5       5     2     b   I
     *
     *   public void methodB();
     *     descriptor: ()V
     *     flags: ACC_PUBLIC
     *     Code:
     *       stack=1, locals=3, args_size=1
     *          0: iconst_2
     *          1: istore_1
     *          2: bipush        20
     *          4: istore_2
     *          5: return
     *       LineNumberTable:
     *         line 11: 0
     *         line 12: 2
     *         line 13: 5
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0       6     0  this   LCrazyTest/VirtualStackTest/VirtualStack01;
     *             2       4     1     k   I
     *             5       1     2     m   I
     *
     *   public static void main(java.lang.String[]);
     *     descriptor: ([Ljava/lang/String;)V
     *     flags: ACC_PUBLIC, ACC_STATIC
     *     Code:
     *       stack=2, locals=2, args_size=1
     *          0: new           #3    创建对象              // class CrazyTest/VirtualStackTest/VirtualStack01
     *          3: dup
     *          4: invokespecial #4                  // Method "<init>":()V
     *          7: astore_1
     *          8: aload_1
     *          9: invokevirtual #5                  // Method methodA:()V
     *         12: return   // 设定返回值,默认添加的。
     *       LineNumberTable:
     *         line 16: 0
     *         line 17: 8
     *         line 18: 12
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0      13     0  args   [Ljava/lang/String;
     *             8       5     1 stack01   LCrazyTest/VirtualStackTest/VirtualStack01;
     * }
     */

}

运行时栈帧的内存结构

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();
            /**
             * method1()开始执行...
             * method2()开始执行...
             * method3()开始执行...
             * method3()即将结束...
             * method2()即将结束...
             * method1()执行结束...
             * main方法结束。。。。。。
             */
        }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,在存储前被转换为intboolean也被转换为int类型,0代表false,1代表truelong和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;
    }

    /*
    变量的分类:按照数据类型分:① 基本数据类型  ② 引用数据类型
                按照在类中声明的位置分:① 成员变量:在使用前,都经历过默认初始化赋值
                                                类变量: linking的prepare阶段:给类变量默认赋值  ---> initial阶段:给类变量显式赋值即静态代码块赋值
                                                实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
                                       ② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
     */
    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命令相加的情况。

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

在这里插入图片描述

动态链接(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:调用<init>方法,私有及父类方法,解析阶段唯一确定调用方法
        super();
    }
    public Son(int age) {
        //invokespecial:调用<init>方法,私有及父类方法,解析阶段唯一确定调用方法
        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),与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowErrorOutOfMemory Error异常。

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

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
它可以直接使用本地处理器的寄存器。
直接从本地内存的堆中分配任意数量的内存。

知识点补充:java对象的实例化过程

对象实例化过程,就是执行类构造函数对应在字节码文件中的()方法(实例构造器),()方法由非静态变量、非静态代码块以及对应的构造器组成。

  • < init >()方法可以重载多个,类有几个构造器就有几个< init >()方法;
  • < init >()方法中的代码执行顺序为:父类变量初始化、父类代码块、父类构造器、子类变量初始化、子类代码块、子类构造器。

静态变量、静态代码块、普通变量、普通代码块、构造器的执行顺序如下图: 在这里插入图片描述 具有父类的子类的实例化顺序如下: 在这里插入图片描述 Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢?

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。

除如何划分可用空间之外,还有另外一个需要考虑的问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。

接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

# java堆内存区域的核心概述

堆的核心概述

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java堆是垃圾收集器管理的内存区域。

一个JVM实例只存在一个堆内存,所有的对象实例和数组都应该在运行时分配到堆上。堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。

《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB)。

数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆,是GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。在这里插入图片描述

public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("end...");
    }

}

先选中当前程序,然后编译当前程序,再配置参数: 在这里插入图片描述 配置完参数后,运行当前代码,找到java的安装位置的主目录下,进入bin目录,然后双击运行jvisualvm.exe。 在这里插入图片描述 关于没有VisualGC的解决方法

public class SimpleHeap {
    private int id;//属性、成员变量

    public SimpleHeap(int id) {
        this.id = id;
    }

    public void show() {
        System.out.println("My ID is " + id);
    }
    public static void main(String[] args) {
        SimpleHeap sl = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);

        int[] arr = new int[10];

        Object[] arr1 = new Object[10];
    }
}

在这里插入图片描述

在这里插入图片描述

内存细分

现代垃圾收集器大部分是基于分代收集理论设计的,堆的内存区域区分: 在这里插入图片描述 在这里插入图片描述 其实,在划分的过程中,是影响不到永久代的内存区域的。

设置堆内存大小和OOM

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfM emoryError异常。

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"一Xms"来进行设置。 “-xms:" 用于表示堆区的起始内存,等价于-XX:InitialHeapsize “-xmx": 则用于表示堆区的最大内存,等价于-XX:MaxHeapsize

一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。通常会将-Xms和一Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。 默认情况下,初始内存大小:物理电脑内存大小/ 64 最大内存大小:物理电脑内存大小/4

/**
 * 1. 设置堆空间大小的参数
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *      -X 是jvm的运行参数
 *      ms 是memory start
 * -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * 2. 默认堆空间的大小
 *    初始内存大小:物理电脑内存大小 / 64
 *             最大内存大小:物理电脑内存大小 / 4
 * 3. 手动设置:-Xms600m -Xmx600m
 *     开发中建议将初始堆内存和最大的堆内存设置成相同的值。
 *
 * 4. 查看设置的参数:方式一: jps   /  jstat -gc 进程id
 *                  方式二:-XX:+PrintGCDetails
 */
public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");

//        System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
//        System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

在这里设置的内存大小为600m,但是实际上使用的是575m,首先数据会存放在伊甸园区,Survive0区或者是Survive1区。也就是说总有一个Survive区域是不使用的,这涉及到了垃圾回收算法。 在这里插入图片描述 在这里插入图片描述

OutOfMemoryError异常

1、java堆溢出 Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

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

public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

运行结果如下: 在这里插入图片描述 Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。

2、虚拟机栈和本地方法栈溢出 HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说, -Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈。

1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 2、如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

使用-Xss参数减少栈内存容量。 结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

// -Xss128k
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

运行结果如下: 在这里插入图片描述 对于不同版本的Java虚拟机和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操 作系统内存分页大小。

定义了大量的本地变量,增大此方法帧中本地变量表的长度。 结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

package com.atguigu.java;

public class JavaVMStackSOF {
    private static int stackLength = 0;
    public static void test() {
        long unused1, unused2, unused3, unused4, unused5,
                unused6, unused7, unused8, unused9, unused10,
                unused11, unused12, unused13, unused14, unused15,
                unused16, unused17, unused18, unused19, unused20,
                unused21, unused22, unused23, unused24, unused25,
                unused26, unused27, unused28, unused29, unused30,
                unused31, unused32, unused33, unused34, unused35,
                unused36, unused37, unused38, unused39, unused40,
                unused41, unused42, unused43, unused44, unused45,
                unused46, unused47, unused48, unused49, unused50,
                unused51, unused52, unused53, unused54, unused55,
                unused56, unused57, unused58, unused59, unused60,
                unused61, unused62, unused63, unused64, unused65,
                unused66, unused67, unused68, unused69, unused70,
                unused71, unused72, unused73, unused74, unused75,
                unused76, unused77, unused78, unused79, unused80,
                unused81, unused82, unused83, unused84, unused85,
                unused86, unused87, unused88, unused89, unused90,
                unused91, unused92, unused93, unused94, unused95,
                unused96, unused97, unused98, unused99, unused100;
        stackLength ++;
        test();
        unused1 = unused2 = unused3 = unused4 = unused5 =
                unused6 = unused7 = unused8 = unused9 = unused10 =
                        unused11 = unused12 = unused13 = unused14 = unused15 =
                                unused16 = unused17 = unused18 = unused19 = unused20 =
                                        unused21 = unused22 = unused23 = unused24 = unused25 =
                                                unused26 = unused27 = unused28 = unused29 = unused30 =
                                                        unused31 = unused32 = unused33 = unused34 = unused35 =
                                                                unused36 = unused37 = unused38 = unused39 = unused40 =
                                                                        unused41 = unused42 = unused43 = unused44 = unused45 =
                                                                                unused46 = unused47 = unused48 = unused49 = unused50 =
                                                                                        unused51 = unused52 = unused53 = unused54 = unused55 =
                                                                                                unused56 = unused57 = unused58 = unused59 = unused60 =
                                                                                                        unused61 = unused62 = unused63 = unused64 = unused65 =
                                                                                                                unused66 = unused67 = unused68 = unused69 = unused70 =
                                                                                                                        unused71 = unused72 = unused73 = unused74 = unused75 =
                                                                                                                                unused76 = unused77 = unused78 = unused79 = unused80 =
                                                                                                                                        unused81 = unused82 = unused83 = unused84 = unused85 =
                                                                                                                                                unused86 = unused87 = unused88 = unused89 = unused90 =
                                                                                                                                                        unused91 = unused92 = unused93 = unused94 = unused95 =
                                                                                                                                                                unused96 = unused97 = unused98 = unused99 = unused100 = 0;
    }
    public static void main(String[] args) {
        try {
            test();
        }catch (Error e){
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

运行结果如下: 在这里插入图片描述 无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。可是如果在允许动态扩展栈容量大小的虚拟机上,相同代码则会导致不一样的情况。

3、方法区和运行时常量池溢出 运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。在java7开始去除永久代,并且在java8中完全使用元空间来代替永久代。因此永久代和元空间实现方法区有什么影响呢?

String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

实例代码:运行时常量池导致的内存溢出异常(电脑装的是java8,没有装其他版本的java)

import java.util.HashSet;
import java.util.Set;

// -XX:PermSize=6M -XX:MaxPermSize=6M
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
    // 使用Set保持着常量池引用,避免Full GC回收常量池行为
        Set<String> set = new HashSet<String>();
    // 在short范围内足以让6MB的PermSize产生OOM了
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}

运行结果如下: 在这里插入图片描述 4、本机直接内存溢出 直接内存(Direct Memory)的容量大小可通过-XX: MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致,下面实例代码越过了DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe 的部分功能通过VarHandle开放给外部使用),因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()。

import sun.misc.Unsafe;
import java.lang.reflect.Field;

//  -Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}