- 前言
- 一、JAVA内存区域
- 1.1、说一下 JVM 的主要组成部分及其作用?
- 1.2 、说一下 JVM 运行时数据区
- 1、程序计数器(PC寄存器)
- 2、JAVA虚拟机栈(重要)
- 3、本地方法栈(Native Method Stack)
- 4、Java 堆(Java Heap)
- 5、方法区(Methed Area)
- 1.3、详细介绍下Java虚拟机栈
- 1.3.1、局部变量表
- 1.3.2、操作数栈
- 1.3.3、动态链接(或指向运行时常量池的方法引用)
- 1.3.4、方法返回地址
- 栈和堆的区别?
参考牛客博客:https://www.nowcoder.com/discuss/916441
前言其中内存模型,类加载机制,GC是重点,性能调优部分更加偏向应用方面,重点出实践能力,编译器优化和执行模式部分偏向理论基础,这些都是重要的知识点。
了解:
- 内存模型各个部分的作用,保存哪些数据。
- 类加载双亲委派加载机制,常用加载器分别加载哪种类型的类。
- GC分代回收的思想和依据以及不同的垃圾回收算法的回收思路和适合场景。
- 性能调优常有JVM优化参数作品,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法。
- 执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景
- 针对的是server模式,优化更激进.新技术方面Java10的graal编译器。
- 编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化。
一、JAVA内存区域 1.1、说一下 JVM 的主要组成部分及其作用?
JVM内存结构 = 类加载器 + 执行引擎 + 运行时数据区域 。
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎):执行classes中的指令。
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JAVA程序运行机制步骤:
- 首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
- 再利用编译器将源代码编译成字节码文件,字节码文件的后缀名为.class;
- 运行字节码的工作是由解释器(java命令)来完成的。
java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
1.2 、说一下 JVM 运行时数据区Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
- 作用:记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 意义:JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器(为什么要线程计数器?因为线程是不具备记忆功能)。
- 存储内容:当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。当线程中执行的是一个本地方法时,程序计数器中的值为空。
- 可能出现异常:此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。
使用程序计数器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就要知道接着从哪开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设计为线程私有?
多线程在一个特定的时间段内只会执行其中的一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复,为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是为每一个线程都分配一个PC寄存器这样一来各个线程之间便可以进行独立的运算,从而不会出现相互干扰的情况。
- 作用:描述Java方法执行的内存模型。每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,成为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息。
- 意义:JVM是基于栈的,所以每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
- 存储内容:局部变量表(编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。值得注意的是:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
- 可能出现的异常:Java虚拟机规范允许Java栈的大小是动态的或者十固定不变的,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。
使用 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
栈的特点有哪些?
- 栈是一种快速分配存储方式,访问速度仅次于程序计数器。
- JVM直接对Java栈的操作只有两个:一是每个方法执行,伴随这进栈。二是执行结束后的出栈。
- 对于栈来说不存在垃圾回收问题,但是存在OOM。
- 作用:为JVM所调用到的Native即本地方法服务。(虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码)。
- 可能出现的异常:和虚拟机栈出现的异常很相像。
- 作用:所有线程共享一块内存区域,在虚拟机开启的时候创建。
- 意义:1、存储对象实例,更好地分配内存。2、垃圾回收(GC)。堆是垃圾收集器管理的主要区域。更好地回收内存。
- 存储内容:存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。值得注意的是:在JIT编译器等技术的发展下,所有对象都在堆上进行分配已变得不那么绝对。有些对象实例也可以分配在栈中。
- 可能出现的异常:实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。 如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。
- 作用:用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 意义:对运行时常量池、常量、静态变量等数据做出了规定。
- 存储内容:运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 可能出现的异常:当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
- Java虚拟机是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表也被称之为局部变量数组或者本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、方法引用。以及returnAddress类型。
在局部变量表中,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
byte,short,char在存储前被转换位int,boolean也被转换为int,0表示false,非零表示true。
方法嵌套的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用的次数越多。
局部变量表中的变量只在当前方法调用中有效。当方法调用结束的后,随着方法栈的销毁,局部变量表也会随之销毁。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
局部变量表所需的容量大小是在编译期就确定下来的,并且在运行期间不会改变,为保存在方法的Code属性的maxinum local variables 数据项。
局部变量表Slot的理解:
- 参数存放总是在局部变量数组的index0开始,到数组长达-1的索引结束。
- 局部变量表,最基本的存储单元是Slot(变量槽)
关于Slot的理解:



