JAVA面试题 —————— JVM篇01

JAVA面试题 —————— JVM篇01

1、请你简述一下JAVA内存结构(运行时数据区)

如图所示:

4vh8Yt.png

1.1、程序计数器

  • 程序计数器线程私有。一块较小的内存空间,程序计数器用于保存JVM中下一条所要执行的字节码指令的地址!如果正在执行的是Native方法,则这个计数器值为空,程序计数器在硬件层面是通过寄存器实现的。

JAVA指令执行流程

  • .java代码源文件经过编译为.class二进制文件
  • .class文件中的每一条二进制字节码指令(JVM指令)通过解释器转换成机器码,然后就可以被CPU执行了
  • 解释器将一条JVM指令转换成机器码后,同时会向程序计数器递交下一条JVM指令的执行地址

如图所示:

4xI0aQ.png

1.2、虚拟机栈

  • 虚拟机栈线程私有,它的生命周期与线程相同。虚拟机栈是JAVA方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
    • 每个栈由多个栈帧(Frame)组成,对应着每个方法运行时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法,当方法执行压入栈,方法执行完毕后弹出栈。
    • 方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上

实例代码:

/**
 * @Description: 演示栈帧
 */
public class Demo01 {
    public static void main(String[] args) {
        methodA();
    }

    private static void methodA() {
        methodB(1, 2);
    }

    private static int methodB(int a, int b) {
        int c = a + b;
        return c;
    }
}

流程分析:
在这里插入图片描述

我们来打断点来Debug一下看一下方法执行的流程:

4x7V8s.png

接着往下走,使得方法B执行完毕:

4x74Ig.png

然后方法A执行完毕,其对应的栈帧出栈,main方法对应的栈帧为活动栈帧;最后main方法执行完毕,栈帧出栈,虚拟机栈为空,代码执行结束!

1.3、本地方法栈

  • 本地方法栈线程私有。本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行JAVA方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

一些带有native关键字修饰的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法!

1.4、堆

  • 线程共享。JAVA堆是JAVA虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,JAVA堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
    • 通过new关键字创建的对象都会被放在堆内存。
    • 方法体中的引用变量和基本数据类型的变量都在栈上,其他都在堆上
    • JAVA堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC堆(Garbage)。
    • -Xmx -Xms :JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64

1.5、方法区

  • 方法区线程共享。方法区用于存储已被虚拟机加载的类信息(构造方法、接口定义)、常量、静态变量、即时编译器编译后的代码等数据。
    • 方法区在JVM中启动的时候被创建,并且它的实际的物理内存和JAVA堆空间一样都是可以不连续的、关闭JVM就会释放这个区域的内存。
    • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:(java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:MetaSpace)
  • 注意:方法区是一种规范,而永久代和原空间是它的两种实现方式

方法区的演进:

  • 1.6版本方法区是由永久代实现(使用堆内存的一部分作为方法区),且由JVM管理。由Class、ClassLoader、常量池(包括StringTable)组成。

在这里插入图片描述

  • JDK1.7版本仍有永久代,但已经逐步 去永久代,StringTable、静态变量从永久代移除,保存在堆中。
  • JDK1.8版本后,方法区交给本地内存管理,而脱离了JVM,由元空间实现(元空间不再使用堆的内存,而是使用本地内存,即操作系统的内存),由Class、ClassLoader、常量池(StringTable被移到了堆内存)组成。

1.6、运行时常量池

  • 常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
    • 常量池是.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实内存地址
  • 运行时常量池:是方法区的一部分、
String str = new String("hello");

上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而hello这个字面量是放在常量池中。

为什么要用原空间取代永久代?

因为永久代有以下几个弊端:

  • 字符串常量存在于永久代中,在大量使用字符串的情况下,非常容易出现OOM的异常。
  • JVM加载的class总数,方法的大小等都很难确定,因此对永久代大小的指定难以确定。太小的永久代容易导致永久代溢出,太大的永久代则容易导致虚拟机内存紧张,空间浪费。
  • 永久代进行调优很困难:方法区的垃圾收集主要两部分,常量池中废弃的常量不再使用的类。而不再使用的类或者类的加载器回收比较复杂,FULL GC的时间长。

2、请问JVM垃圾回收是否涉及栈内存?

  • 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无须通过垃圾回收机制去回收内存。

3、虚拟机栈内存的分配越打越好吗?

  • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的调用,但是可执行的线程数就会越少。

我们来看一张图:

在这里插入图片描述

  • 举例:如果物理内存是500M(假设),如果一个线程所能分配的栈内存为2M的话,那么可以有250个线程。而如果一个线程分配栈内存占5M的话,那么最多只能有100个线程同时执行!

4、从JVM角度,方法内的局部变量是否是线程安全的?

我们通过两张图去分析一下:

  • 情况一:

在这里插入图片描述

  • 情况二:
    在这里插入图片描述

从图中可以得出:局部变量如果是静态的可以被多个线程共享,那么就存在线程安全问题。如果是非静态的只存在于某个方法作用范围内,被线程私有,那么就是线程安全的!

再来看一个案例

/**
 * 局部变量的线程安全问题
 */
public class Demo02 {
    public static void main(String[] args) {// main 函数主线程
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(() -> {// Thread新创建的线程
            m2(sb);
        }).start();
    }

    public static void m1() {
        // sb 作为方法m1()内部的局部变量,是线程私有的 ---> 线程安全
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static void m2(StringBuilder sb) {
        // sb 作为方法m2()外部的传递来的参数,sb 不在方法m2()的作用范围内
        // 不是线程私有的 ---> 非线程安全
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }

    public static StringBuilder m3() {
        // sb 作为方法m3()内部的局部变量,是线程私有的
        StringBuilder sb = new StringBuilder();// sb 为引用类型的变量
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;// 然而方法m3()将sb返回,sb逃离了方法m3()的作用范围,且sb是引用类型的变量
        // 其他线程也可以拿到该变量的 ---> 非线程安全
        
        // 如果sb是非引用类型,即基本类型(int/char/float...)变量的话,逃离m3()作用范围后,则不会存在线程安全
    }
}

所以,该面试题答案是

  • 如果方法内局部变量没有逃离方法的作用范围,则是线程安全的。
  • 如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题。

5、虚拟机栈内存溢出的情况有哪些?

  • 虚拟机栈中,栈帧过多(方法无限递归)导致栈内存溢出,这种情况比较常见
  • 每个栈帧所占用内存过大(某个/某几个栈帧内存直接超过虚拟机栈最大内存),这种情况比较少见

如图所示,就是栈中栈帧过多的情况:

在这里插入图片描述

7、JAVA虚拟机中有哪些类加载器?

以JDK8为例:

名称加载哪的类说明
Bootstrap ClassLoader(启动类加载器)JAVA_HOME/jre/lib无法直接访问
Extension ClassLOader(扩展类加载器)JAVA_HOME/jre/lib/ext上级为Bootstrap,显示为null
Application ClassLoader(应用程序类加载器)classpath上级为Extension
自定义类加载器自定义上级为Application

类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在JAVA_HOME/jre/lib目录中的,或者被Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,比如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME/jre/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开着可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下就是程序中默认的类加载器。
  • 自定义类加载器:用户自定义的类加载器。

8、请你说一下类的加载的过程?

类加载的过程包括:加载、验证、准备、解析、初始化。其中验证、准备、解析统称为连接

  • 加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。
  • 验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值通常情况下是数据类型的零值。
  • 解析:将常量池内的符号引用替换为直接引用。
  • 初始化:到了初始化阶段,才真正开始执行类中定义的JAVA初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。

9、请你说一下什么是双亲委派机制?

如图所示:

在这里插入图片描述

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载器请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)的时候,子加载器才会尝试自己去加载。

为什么要使用双亲委派模型呢?(好处)

避免重复加载 + 避免核心类篡改

  • 采用双亲委派模式的好处是JAVA类随着它的类加载一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父加载器已经加载了该类的时候,就没有必要子加载器再加载一次。
  • 其次是考虑到安全因素,JAVA核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器再核心JAVA API发现这个名字的类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer,而直接返回Integer.class,这样便可以防止核心API库被随意篡改。

10、说一下虚拟机栈和堆的区别?

  • 物理地址方面的区别:
    • 的物理地址分配的对象是不连续的。因此性能慢些。
    • 虚拟机栈使用的是数据结构中的栈,先进后出的原则,物理地址是连续的。所以性能快。
  • 内存分配方面的区别:
    • 因为是不连续的,所以分配的内存是再运行期确认的,因此大小不固定。一般堆大小远远大于虚拟机栈。
    • 虚拟机栈是连续的,所以分配的内存大小要再编译期就确认,大小是固定的。
  • 存放的内容区别:
    • 存放的是对象的实例和数组。因此该区更关注的是数据的存储。
    • 虚拟机栈存放的局部变量,操作数栈,返回结果、该区更关注的是程序方法的执行。

注意:静态变量放在方法区,而静态的对象还是放在堆。

  • 线程共享方面的区别:
    • 对于整个应用程序都是共享、可见的。
      不连续的,所以分配的内存是再运行期确认的,因此大小不固定。一般堆大小远远大于虚拟机栈。
    • 虚拟机栈是连续的,所以分配的内存大小要再编译期就确认,大小是固定的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值