为什么面试总是会被问到JVM内存区域?

Posted 你这家伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么面试总是会被问到JVM内存区域?相关的知识,希望对你有一定的参考价值。

Java程序是运行在JVM(Java虚拟机)上的,在开发程序之前都要配置Java开发环境,其中首先要做的就是JDK的安装和配置,那么JDK、JVM、JRE到底有何联系和区别呢?如下图

1.JVM概念

**JVM**(Java Virtual Machine的简称),意为Java虚拟机

**虚拟机:**指通过软件模拟的具有完整硬件功能的,运行在一个完全隔离的环境中的完整的计算机系统,常见的虚拟机有:JVM, VMwave, Virtual Box。

JVM和其他两个虚拟机的区别:

  • VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
  • JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪

JVM是一台被定制过的现实当中不存在的计算机。

2.为什么要有JVM呢

因为Java有一个很重要的特性——跨平台

也就是“一次编写,到处运行”,也就是编写一个程序,可以在Windows,Linux,Mac等系统上直接运行,我们都知道不同的计算机有不同的硬件结构(CPU的体系架构不同),也有不同的操作系统,操作系统提供的API也不一样,而JVM就是屏蔽不同计算机上软硬件的差异,也就是代码只需要对虚拟机负责就OK,也就达到了我们的跨平台的目的。JVM也不仅仅是一个程序,不同的系统都会对应一个JVM的版本(如:Windows的JVM是一个版本,Linux的JVM也是一个版本),但是不管是那个版本的JVM,都懂Java语言。

这也就说明为什么我们在官网上面下载的同一个 Tomcat 压缩包,既能在Windows上运行,也能够在Linux上运行,都是靠JVM把其中的差异给屏蔽掉了。

注:JVM发展到现在,不仅仅是为了跨平台,而是已经打造成了一个生态圈,大量的程序/库/编程语言都是基于JVM的,但是对于一个编程语言来说,实现编译器(就是把源代码翻译成可执行指令),直接翻译成机器指令native指令,比较复杂,考虑到系统的差异,CPU的差异,那么工作量也就大了,干脆就将编程语言直接翻译成JVM能够之别的字节码,由JVM去考虑软硬件之间的差异,大大降低工作量。

JVM有没有什么缺点呢?
答:当然有,因为没有什么东西是完美无瑕的,在一个程序中,要先将编程语言翻译成JVM能够识别的字节码,然后再不同的系统中,JVM在屏蔽软硬件的差异再去翻译成不同系统能够识别的程序,那么效率肯定会受到影响。(注:虽然虚拟机能够影响到一定的效率,但是在真正的商业项目中,性能瓶颈往往不是编程语言自身带来的,更主要的瓶颈是网络IO/数据库/磁盘IO/复杂的CPU密集计算……)。

3.JVM中的内存区域划分

JVM运行时数据区

JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用处,各有各的创建与销毁时间,有的区域随着JVM进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建与销毁。一般来说,JVM所管理的内存将会包含以下几个运行时数据区域

  • 线程私有区域:程序计数器、Java虚拟机栈、本地方法栈

  • 线程共享区域:Java堆、方法区、运行时常量池

  • 线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁

  • 线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁

JVM内存模型(JDK1.8)

3.1.程序计数器(线程私有)

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

  • a.如果当前线程执行的是一个java方法, 那么这个计数器记录的是正在执行的虚拟机字节码指令的地址
  • b.如果正在执行的是一个Native方法(本地方法),那么这个计数器值为空

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM清空的区域

什么是线程私有:

由于JVM的多线程是通过线程轮流切换分配处理器执行时间的方式来实现的,因此在任何一个确定的时刻,一个处理器(多多核处理器则指的是一个内核)都只会执行一条线程中的指令,因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。类似这类区域成为“线程私有”。

3.2.java虚拟机栈(线程私有)

相对于基于寄存器的运行环境,JVM是基于栈结构的运行环境。栈结构移植性更好,可控性更强。

虚拟机栈描述的是Java方法执行的内存模型: 每个方法执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程,声明周期和线程相同。

  • a.在活动线程中,只有位于栈顶的帧才是最有效的,成为当前栈帧
  • b.正在执行的方法成为当前方法

在执行引擎运行时,所有指令都只能针对当前栈帧操作,StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法。

**局部变量表:**局部变量表的创建是在方法被执行的时候,随栈帧创建而创建。存放了编译器可知的各种基本数据类型(八大基本数据类型),对象引用。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量表空间是完全确定的,在执行期间不会改变局部变量表大小。

此区域会产生两种异常:

  1. 如果线程请求的栈深度大于所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常(也就是常说的栈溢出)
  2. 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutMemoryError)异常

Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随线程的死亡而死亡。

注意:
人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!这里的“堆”可以这么理解,但这里的“栈”就是现在讲的虚拟机栈,或者说Java虚拟机栈中的局部变量表部分.
真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息.

3.3.本地方法栈(线程私有)

本地方法栈::与虚拟机的作用完全一样,区别在于本地方法栈为虚拟机使用的Native(本地)方法服务,而虚拟机栈为JVM的java方法服务。

在JVM内存布局中,也是线程对象私有的,但是虚拟机栈“主内”,而本地方法栈“主外”。
这个“内外”是针对JVM来说的,本地方法栈为Native方法服务线程开始调用本地方法时,会进入一个不再受JVM约束的世界。本地方法可以通过JNI(Java Native Interface)访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。

当大量本地方法出现时,势必会削弱JVM对系统的控制力,因为它的出错信息都比较黑盒。对于内存不足的情况,本地方法栈还是会拋出native heap OutOfMemory。

3.4.java堆(线程共享)

  • java堆(Java Heap)是JVM所管理的最大的内存区域。
  • java堆是所有线程共享的一片区域,在JVM启动时创建,此内存区域存放的都是对象实例(new test()),JVM规范中说道“所有的对象实例以及数组都要在堆上分配”,而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个
  • java堆是垃圾回收管理的主要区域,因此很多时候成为“GC堆”
  • java堆可以存放处于物理上不连续的内存空间中,java堆在主流的虚拟机都是可扩展的(-Xmx设置最大/小值)——打开命令指示符,输入java -X,就可以找到
  • 如果在堆中没有足够的内存能完成实例分配并且页无法再拓展时,将会抛出OOM(OutOfMemory)

3.5.方法区(线程共享)

**方法区:**与java堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,在JDK8以前的HotSpoot虚拟机中,方法区也被称为“永久代”(JDK8已经被元空间取代)

  • 线程共享
    方法区是线程共享的,方法区也是堆的一个逻辑部分,整个虚拟机只有一个方法区
  • 永久代
    方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,所以方法区仇称为“永久代”
  • 内存回收率低
    java虚拟机规范对方法区的要求比较宽松,可以不实现垃圾回收。方法区中的信息一般需要长期存在,回收一遍内存可能只有少量信息无效,对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载

注:方法区同堆一样,允许固定大小,可扩展大小,还允许不实现GC,但是内存空间分配无法达到需求的时候,也会抛出OOM异常

举例说明:
例如:

class A extends B{
    public B b = new B();
}
class B{
    public int num = 10;
}

public class Demo2 {
    public static void main(String[] args) {
    A a = new A();
    }
}

3.6.运行时常量池(方法区的一部分)

.java文件被编译之后生成的.class文件中除了包含:类的版本、字段、方法、接口等描述信息外,还有一项就是常量池。运行时常量池是方法区的一部分,存放字面量与符号引用。

**字面量:**字符串(JDK1.7后移动到堆中),final常量,基本数据类型的值。
**符号引用:**类和结构的完全限定名,字段的名称和描述符,方法的名称和描述符

运行时常量池可能会抛出的异常:

  1. 因为运行时常量池是方法区的一部分,那么就会收到方法区内存的限制,当常量池无法申请到内存时就会抛出OOM异常
  2. 当我们在一个类中通过static final 来声明一个变量的时候,这个类被编译后,那么这个类的所有信息将会存储在这个class文件中,当这个类被JVM加载之后,class文件中的常量就会存放在运行时常量池中,而且在运行时期也可以向常量池中添加新的常量(如String中的intern()方法)。大部分运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用的时候,就需要垃圾收集器回收。

4.常见面试题

4.1.谈谈基础数据类型和引用类型之间的区别?

答:如果是基础数据类型(如:int double……),当你创建这个静态变量的时候,此时在内存就立即创建了一个空间来保存这个变量,只不过这个变量是在栈上还是在堆上,取决于这个变量是局部变量还是成员变量,如果是引用类型,那么他本质上存的是地址(就是一个整数),所谓地址就是内存按照字节为单位分成了很多小房间,每个房间大小为1字节,每个房间都有编号(从0开始计算,依次累加),对应的房间号就是地址,只不过地址一般都写作十六进制来表示

4.2.如何理解引用和对象?

答:引用保存了对象所在的地址,对象才是组织数据的本体(把对象假设成一个房间,房间里面有各种设施;引用就是开启房间的钥匙) 例如: new Test(); new出来的东西是对象,是在堆上创建的这个内存区域,这个才是对象的本体(真正存储数据的部分) Test t = new Test(); new返回的结果 t 是在堆的内存区域的地址,这个地址要交给一个引用类型来持有 t.num 根据 t 存储的地址,找到堆上对应的房间,进而找到房间中的设施(如果 num 是基础类型,就直接找到值了;如果num是引用类型,还需要根据引用中存储的房间号,继续往下找)

4.3.局部变量/成员变量/静态成员变量所对应的内存区域?

答:局部变量对应的内存区域是在栈上; 成员变量对应的内存区域是在堆上; 静态成员变量对应的内存区域在方法区,因为这个变量也可能是引用类型,只是说这个引用自身,是在方法区,对应的对象仍然是在堆上。(例如:public static String str = new String("hello"));

4.4.递归方法执行过程如何理解?

答:注意栈的变化,每次递归,都会创建新的栈帧,同一个名字的变量可能会在多个栈帧中出现,(虽然同名,但是不是同一个变量),每个方法执行完毕,当前栈帧就被销毁了,进而回到上一层调用位置上继续往下执行。 [栈帧的出栈和入栈可以参考](https://blog.csdn.net/weixin_44663675/article/details/107014923?ops_request_misc=&request_id=&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~all~es_rank~default-2-107014923.pc_search_all_es&utm_term=%E9%80%92%E5%BD%92%E6%96%B9%E6%B3%95%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3jvm&spm=1018.2226.3001.4187)

如:递归就 n 的阶乘

public class Demo2 {
    public static int fanc(int n){
        if(n == 1){
            return 1;
        }
        return n * fanc(n-1);
    }
    public static void main(String[] args) {
        int result = fanc(5);
        System.out.println(result);
    }
}

当我们debug的时候

4.5.普通方法和static方法之间的区别?

答:
  • static方法中,没有this,static方法也可以叫做“类方法”,非static方法也可以叫做“实例方法”。
  • static方法为静态方法,它在jvm运行时不需要实例化,因为在jvm加载时就已经编译过,可以直接通过类名调用方法,静态方法只能调用类中静态变量,不能调用非静态变量;
  • 普通方法也叫非静态方法,它只有在创建时才会被实例化,普通方法既可以调用静态方法,又可以调用非静态方法。
    相比较而言,static方法占用资源比较多,它在jvm加载时就实例化过了,因此一直占用虚拟机内存,在jvm关闭时才会销毁,所以静态方法常用于工具类Utils中经常调用使用,非静态方法因为什么时候使用什么时候实例化,所以占用资源较少,但它每次的使用都要创建对象才能调用方法相比较于静态方法要繁琐一些。

以上是关于为什么面试总是会被问到JVM内存区域?的主要内容,如果未能解决你的问题,请参考以下文章

史上最全!2020面试阿里,字节跳动90%被问到的JVM面试题(附答案)

面试阿里被问到JVM,不逼逼赖赖,直接盘给面试官看!!!

面试阿里被问到JVM,不逼逼赖赖,直接盘给面试官看!!!

看了最新大厂面试,这6道JVM面试题都被问到了

Web前端求职时都会被问到的Redis面试题分享

面试阿里,字节跳动90%会被问到的Java异常面试题集,史上最全系列!