不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1

1 JVM概述

1.1 前言

本文为本人的JVM的学习记录,适合有一定的java编程基础( J2SE)并希望进一步理解java的程序员,虚拟机爱好者,jvm实践者。

大多数java开发工程师,都是处于使用框架阶段,使用java api开发系统,很少有关注java底层核心技术jvm,对其了解的很少。如果我们把java核心类库的API比做数学公式的话,那么java虚拟机jvm的知识就好比公式的推导过程
不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第1张图片
关于学习资料推荐:

  • 《深入Java虚拟机》
  • 《深入理解Java虚拟机JVM高级特性与最佳实践》
  • 《Java虚拟机规范》

关于官方文档:

https://docs.oracle.com/javase/specs/index.html

1.2 JVM简介

1.2.1 虚拟机是什么?

所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机程序虚拟机

  • 大名鼎鼎的Visula Box,VMware就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一 个可运行完整操作系统的软件平台。
  • 程序虚拟机的典型代表就是java虚拟机,它专⻔为执行单个计算机程序而设计,在java虚拟机中执行的指令我们称之为java字节码指令。

无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。

1.2.2 Java虚拟机

java技术的核心就是java虚拟机**(JVM,Java Virtual Machine)**,因为所有的java程序都运行在java虚拟机内部。

优势

跨平台性、优秀的垃圾回收器,以及可靠的即时编译器。

作用

java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条java指令,java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数, 处理结果存储在哪里。

特点

  1. 一次编译,到处运行
  2. 自动内存管理(内存分配)
  3. 自动垃圾回收功能(内存回收)

1.2.3 JVM的位置

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第2张图片
不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第3张图片
不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第4张图片
JVM是运行在操作系统之上的,它与硬件没有直接的交互。

1.2.4 一次编译,到处运行

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第5张图片

1.2.5 JVM跨语言的平台

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第6张图片

随着java7的正式发布,java虚拟机的设计者们通过JSR-292规范基本实现:在java虚拟机平台上运行非java语言编写的程序。

java虚拟机根本不关心运行在其内部的程序是使用何种编程语言编写的,它只关心“字节码”文件。也就是java虚拟机拥有语言无关性,并不会单纯地与java语言“终身绑定”,只要其他编程语言的编译结果满足并包含java虚拟机的内部指令集、符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能被虚拟机所识别并装载运行。

多语言混合编程

java平台上的多语言混合编程逐渐称为主流,通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。

试想一下,在一个项目之中,并行处理用Clojure语言编写,展示层使用JRuby/Rails,中间层是java,每个应用层都将使用不同的编程语言来完成,而且,接口对每一层的对于开发者都是透明的,各种语言之间的交互不存在任何困难,就使用自己语言的原生API一样方便,因为他们最终都运行在一个虚拟机上。

对这些运行与java虚拟机之上、java之外的语言,来自系统级的、底层的支持正在迅速增强,以 JSR-292为核心的一系列项目和功能改进(如Davinci Machine项目、Nashorn引擎),推动java虚拟机 从**"java语言的虚拟机""多语言虚拟机’’**的方向发展。

1.2.6 字节码

javac Demo.java 
//javac:命令将其编程成字节码文件
//java:命令来执行class字节码文件
javap -v Demo
//javap:是将字节码进行反编译(与javac对应),可以查看java编译器为我们生成的字节码。
package com.nyf;

public class Demo {
    private int a = 1;
    public void testMethod(){
        System.out.println("testMethod");
    }
}

运行出来大概是这样:

~/IdeaProjects/day30                                                                                                                                                        ⍉
▶ cd src/com/nyf/

src/com/nyf                                                                                                                                                                  
▶ javac Demo.java

src/com/nyf                                                                                                                                                                  
▶ javap -v Demo
警告: 文件 ./Demo.class 不包含类 Demo
Classfile /Users/monologuist/IdeaProjects/day30/src/com/nyf/Demo.class
  Last modified 2020年6月16日; size 423 bytes
  SHA-256 checksum fd90b5ec57cf96746d17b0ca80967ccb1b9db4fae909b8cf9a76cbfed216744c
  Compiled from "Demo.java"
public class com.nyf.Demo
  minor version: 0
  major version: 57
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // com/nyf/Demo
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/nyf/Demo.a:I
   #8 = Class              #10            // com/nyf/Demo
   #9 = NameAndType        #11:#12        // a:I
  #10 = Utf8               com/nyf/Demo
  #11 = Utf8               a
  #12 = Utf8               I
  #13 = Fieldref           #14.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #14 = Class              #16            // java/lang/System
  #15 = NameAndType        #17:#18        // out:Ljava/io/PrintStream;
  #16 = Utf8               java/lang/System
  #17 = Utf8               out
  #18 = Utf8               Ljava/io/PrintStream;
  #19 = String             #20            // testMethod
  #20 = Utf8               testMethod
  #21 = Methodref          #22.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #22 = Class              #24            // java/io/PrintStream
  #23 = NameAndType        #25:#26        // println:(Ljava/lang/String;)V
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (Ljava/lang/String;)V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               SourceFile
  #30 = Utf8               Demo.java
{
  public com.nyf.Demo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: iconst_1
         6: putfield      #7                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4

  public void testMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #19                 // String testMethod
         5: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}
SourceFile: "Demo.java"

我们平时说的java字节码指的是用java语言编译成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的。所以应该统称为: jvm字节码

不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的jvm上运行。

java虚拟机与java语言并没有必然的联系,它只与特定的二进制文件格式-class文件格式所关联, class文件中包含了java虚拟机指令集(或者称为字节码,Bytecodes)和符号表,还有一些其他辅助信息。

字节码和机器码的区别

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第7张图片

高级语言:最接近人类语言,但机器是无法执行的,需要最终编译度连接成二进制的机器代码才可被计 算机执行

汇编语言:是将二进制的机器码通过抄助记符的方式让人可以更方便的编写并检查的低级语言,汇编语言接近机器语言,可以看做是机袭器语言的另一种形式,计算机在运行时也需要将其变为机器语言的二进制才可运行

字节码:是一种中间状态(中间码)的二进制代码(文件),需要直译器转译后才能成为机器码。 机器语言:是计算机可以识别并运行的二进制代码,机器码是电脑CPU直接读取运行的机器指令,运行速度最快,但是非常晦涩难懂,也比较难编写,一般从业人员接触不到。

1.2.7 HotSpot VM

Oracle/Sun JDK 中使用的 JVMHotSpot VM.

提起HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。

➜ ~ java -version
 java version "1.8.0_121"
 Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
 Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
  • SUN 的 JDK 版本从 1.3.1 开始采用 HotSpot 虚拟机, 并于 2006 年底开源, 主要使用C++实现, JNI 接 口部分用C实现.
  • HotSpot 是较新的 JVM, 用来替代 JIT (Just in Time, 即时编译), 可以大大提高 Java 的运行性能, 即: Java 起初是把源代码编译为 .class 格式的字节码在虚拟机上执行, 速度较慢;
  • HotSpot 将常用的部分代码编译为本地(native)代码, 显著提高了性能。
  • 其他的VM: JRockit VM,IBM J9 VM,SUN Classic VM等等。

1.2.8 OracleJDK和OpenJDK

Oracle JDK

Oracle JDK由Oracle公司开发,该公司是Sun许可证,基于Java标准版规范实现。它以二进制产品 的形式发布。它支持多种操作系统,如Windows,Linux,Solaris,MacOS等。它支持不同的平台,如 Intel 32位和64位架构,ARM架构和SPARC。它完全基于Java编程语言。之后,该许可证宣布将根据 GPL(通用公共许可证)许可证发布。Oracle JDK包含许多组件作为库形式的编程工具集合。

OpenJDK

历史上的原因是,OpenJDK是JDK的开放源码版本,以GPL协议的形式发布。(General Public License)

在JDK7的时候,OpenJDK已经成为JDK7的主干开发版,SUN JDK7是在OpenJDK7的基础上发布的,其大部分源码都相同,只有少部分源码被替换掉。
使用JRL(Java Research License,Java研究授权协议)发布。

OpenJDK是Java SE平台版的开源和免费实现,它是Sun Corporation(现在的Oracle Corporation)于2006年开始的开发结果。它是根据GNU GPL许可证授权的。它最初于2007年发布。 它由Oracle Corporation,Red Hat,IBM,Apple Inc.,OpenJDK和Java Community等开发。它是使 用C ++和Java编程语言编写的。它支持不同的操作系统,如FreeBSD,Linux,Microsoft Windows, Mac OS X。OpenJDK是Java SE Platform Edition的官方参考实现。

对比

Oracle JDK可用于开发Java Web应用程序,独立应用程序以及许多其他图形用户界面以及其他开发 工具。Oracle JDK执行的所有操作或任务也可以由OpenJDK执行,但只有Oracle与OpenJDK之间的区别 在于Open JDK在现有Oracle JDK之上的许可和其他工具集成和实现。使用OpenJDK的优点是可以根据 应用程序的要求修改性能,可伸缩性和实现,以根据需要调整Java虚拟机。

OpenJDK的优势更多,Oracle JDK的使用在Oracle JDK实现中使用的标准方面也有一些好处,这将确保应用程序稳定和良好维护。

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第8张图片

注意: 对于Java 11之前,两者有少部分的区别,Oracle JDK有一些自己独有的东⻄。但是Java 11 之后这两者几乎没有区别,图中提示了两者共同代码的占比要远高于图形上看到的比例, 所以我们编译 的OpenJDK基本上可以认为性能、功能和执行逻辑上都和官方的Oracle JDK是一致的.

2 内存结构

说明

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

HotSpot VM是目前市面上高性能虚拟机的代表之一。它采用解释器即时编译器并存的架构。在 今天,java程序的运行性能早已脱胎换⻣,已经到了可以和C/C++程序一较高下的地步。

进程与线程

​ 进程(process)是具有一定独立功能的程序,操作系统利用进程把工作划分为一些功能单元。 进 程是进行资源分配和调度的一个独立单位。它还拥有一个私有的虚拟地址空间,该空间仅能被它所包含 的线程访问。 一个应用程序(application)是由一个或多个相互协作的进程组成的。

​ 线程(thread)是进程中所包含的一个或多个执行单元。它只能归属于一个进程并且只能访问该进 程所拥有的资源。 它进程中执行运算的最小单位,是进程中的一个实体,是被进程独立调度和分派的基 本单位。 线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源(计数器、寄存器和栈),但 它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同 一进程中的多个线程之间可以并发执行。 当操作系统创建一个进程后,该进程会自动申请一个名为主线 程(首要线程)的线程。

​ 首先,进程和线程如同列⻋和⻋厢,没有可比性,但是他们有一定的相关性:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  3. 虚拟机分给线程,即真正在虚拟机上运行的是线程。
  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第9张图片

方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot VM 不同版本的两种实现。

jdk1.7及之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generationjdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第10张图片

2.1 运行时数据区

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第11张图片

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

  • 每个线程(线程私有、线程独享):程序计数器、虚拟机栈、本地方法栈
  • 线程间共享(线程共有、线程共享):堆、堆外内存(方法区(永久代或元空间)、代码缓存)

代码缓存:JVM在运行时会将频繁调用方法的字节码编译为本地机器码。这部分代码所占用的内存 空间成为CodeCache区域。Java进行JIT的时候,会将编译的本地代码放在codecache中。

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第12张图片
Runtime

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第13张图片
每个JVM只有一个Runtime实例,即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。

这里源码也可以看到,Runtime采用的是单例模式,也就是说只会new一个Runtime对象,且用private修饰,不允许开发者自己去new,你只能去调用它的静态方法。

2.2 PC寄存器

官方地址: https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第14张图片

JVM中的程序计数寄存器(Program Counter Register),Register的命名源于CPU寄存器,寄存器存储指令地址(或者称偏移地址),CPU只有把数据装载到寄存器才能运行,

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

2.2.1 作用

PC寄存器用来存储指向下一条指令的地址,也就是即将要执行的指令代码。由执行引擎读取下一条指令。

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第15张图片

2.2.2 示例

public class PCRegisterTest {
    public static void main(String[] args){
        int i = 100;
        int j = 200;
        int m = i + j;

        String str = "a";
        System.out.println(m);
        System.out.println(str);
    }
}

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第16张图片

2.2.3 特点

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

2.2.4 面试问题

1 为什么使用PC寄存器记录当前线程的执行地址?

因为CPU需要不停地切换线程,这时候切换回来以后,线程就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

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

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?

为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

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

这样必然导致经常中断或恢复,如何保证分毫不差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

2.3 虚拟机栈

2.3.1 背景

​ 由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

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

2.3.2 内存中的栈与堆

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

即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第17张图片

2.3.3 虚拟机栈基本内容

java虚拟机栈是什么?

​ java虚拟机栈(java virtual Machine Stack),早期也叫java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的java方法调用。它是线程私有的。

生命周期

​ 生命周期和线程一致。

作用

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

2.3.4 栈与队列

栈是一种动态集合,它是一种LIFO(last in first out后进先出)结构
不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第18张图片

队列

与栈不同,它是一种FIFO(first in first out先进先出)结构

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第19张图片

2.3.5 示例

public class StackTest {
    public static void main(String[] args) {
      StackTest stackTest = new StackTest();
      stackTest.methodA(); 
      System.out.println("main方法结束");
    }

    public void methodA(){
        int i = 10;
        int j = 20;
        methodB(); System.out.println("i:"+i+",j:"+j);
    }

    public void methodB(){ 
        int k = 30;
        int m = 40;
        System.out.println("k:"+k+",m:"+m);
    }
}

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第20张图片

2.3.6 特点

  1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

  2. JVM直接对java栈的操作只有两个:

  • 每个方法的执行,伴随着进栈(入栈、压栈)
  • 执行结束后的出栈工作

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第21张图片

  1. 对于栈来说不存在垃圾回收问题

2.3.7 可能出现的异常

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

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

2.3.8 设置栈大小

我们可以使用参数**-Xss**选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 JDK5.0以后每个线程栈大小为1M,以前每个线程栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

/**
 * 演示栈中的异常
 *
 * 默认情况下:count 10823
 * 设置栈的大小: -Xss256k count 1874 */
public class StackErrorTest { private static int count = 1;

    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第22张图片

在IDEA中设置一个类的VM参数:

不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第23张图片
不可错过的JVM深度好文-纯干货详解JVM!-JVM概述&内存结构1_第24张图片
设置完成后点Apply以应用。

2.3.9 栈中存储什么?

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

2.3.10 栈运行原理

​ JVM直接对java栈的操作只有两个,就是对栈帧的压栈出栈,遵循先进后出/后进先出的原则。

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

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
  • 不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧。
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧, 接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧

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

public class StackFrameTest {
    public static void main(String[] args) {
        StackFrameTest test = new StackFrameTest();
        test.method1();
//输出 method1()和method2()都作为当前栈帧出现了两次,method3()一次
//        method1()开始执行。。。
//        method2()开始执行。。。
//        method3()开始执行。。。
//        method3()执行结束。。。
//        method2()执行结束。。。
//        method1()执行结束。。。
    }
    public void method1(){
        System.out.println("method1()开始执行。。。");
        method2();
        System.out.println("method1()执行结束。。。");
            //return 可以省略
    }
    public int method2(){
        System.out.println("method2()开始执行。。。");
        int i = 10;
        int m = (int) method3();
        System.out.println("method2()执行结束。。。");
        return i+m;
    }
    public double method3(){
        System.out.println("method3()开始执行。。。");
        double j = 20.0;
        System.out.println("method3()执行结束。。。");
        return j;
    }
}

2.3.11 栈帧的内部结构

每个栈帧中存储着

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

2.3.12 相关面试题

1.举例栈溢出的情况?(StackOverflowError)

  • 递归调用等,通过-Xss设置栈的大小;

2.调整栈的大小,就能保证不出现溢出么?

  • 不能 如递归无限次数肯定会溢出,调整栈大小只能保证溢出的时间晚一些,极限情况会导致OOM内存溢出(Out Of Memory Error)注意是Error

3.分配的栈内存越大越好么?

  • 不是 会挤占其他线程的空间

4.垃圾回收是否会涉及到虚拟机栈?

  • 不会

5.方法中定义的局部变量是否线程安全?

具体情况具体分析


/** * * * * * *
 题
 *
 面试题:
 方法中定义的局部变量是否线程安全?具体情况具体分析
 何为线程安全?
 如果只有一个线程可以操作此数据,则必定是线程安全的。 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问
 我们知道StringBuffer是线程安全的源码中实现synchronized,StringBuilder源码未实现synchronized,在多线程情况下是不安全的
 * 二者均继承自AbstractStringBuilder *
 */
public class StringBuilderTest {
    //s1的声明方式是线程安全的,s1在方法method1内部消亡了
    public static void method1(){
    StringBuilder s1 = new StringBuilder();
    s1.append("a");
    s1.append("b");
    }

    //stringBuilder的操作过程:是不安全的,因为method2可以被多个线程调用
    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的操作:是线程安全的 ,StringBuilder的toString方法是创建了一个新的String,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);
    }
}

后续内容请看JVM概述&内存结构2
关注作者不迷路,持续更新高质量Java内容~
原创不易,您的支持/转发/点赞/评论是我更新的最大动力!

你可能感兴趣的:(Java学习之路)