【Java开发岗面试】八股文—Java虚拟机(JVM)

声明:

  1. 背景:本人为24届双非硕校招生,已经完整经历了一次秋招,拿到了三个offer。
  2. 本专题旨在分享自己的一些Java开发岗面试经验(主要是校招),包括我自己总结的八股文、算法、项目介绍、HR面和面试技巧等等,如有建议,可以友好指出,感谢,我也会不断完善。
  3. 想了解我个人情况的,可以关注我的B站账号:东瓜Lee

Java程序的运行环境(Java二进制字节码的运行环境

好处:

  1. 一次编写, 到处运行
  2. 自动内存管理,垃圾回收机制(GC)

【Java开发岗面试】八股文—Java虚拟机(JVM)_第1张图片
【Java开发岗面试】八股文—Java虚拟机(JVM)_第2张图片


面试题:

JVM组成:

  1. JVM有什么作用?

    JVM 其实就是一个虚拟机,它在计算机上模拟了一台真正的计算机,而且是与平台无关的,这意味着 Java 代码可以在任何支持JVM的平台上运行,而不需要考虑平台的具体细节。JVM 是 Java 的核心,它为 Java 语言提供了强大的功能和平台无关性。

    JVM 的主要作用包括:

    1. Java 代码的编译和解释执行:Java代码首先被编译成字节码,然后由JVM解释执行字节码。JVM 使用即时编译技术(Just-In-Time Compilation,JIT)将频繁执行的代码编译成本地机器代码,从而提高执行效率。
    2. 内存管理:JVM 提供了垃圾回收机制,自动管理内存分配和释放,避免了内存泄漏和空指针异常等问题。
    3. 安全管理:JVM 可以对 Java 代码进行安全性检查,防止恶意代码对系统造成损害。
    4. 多线程支持:JVM 可以处理多个线程的并发执行,保证线程安全。
    5. 跨平台兼容性:JVM 实现了一种平台无关的执行环境,使 Java 代码可以在不同平台上运行。

  1. 什么是程序计数器

    1. 线程私有的(没有线程安全问题),内部保存的是字节码的行号,存储 正在执行的字节码指令地址。
    2. 把Java编译后的class文件,反编译成汇编语言代码后就能看到,程序的执行是按照命令的行号来的,程序计数器记录的就是这个行号(比如线程1执行到了第10行,然后cpu切换到了其他线程,其他线程执行完后继续来执行线程1,就会从程序计数器中得到第10行继续执行)。

  1. 什么是Java虚拟机栈(Java方法栈)

    1. 栈的特点就是先进后出 后进后出的数据结构
    2. 每个线程运行时所需要的内存,称为虚拟机栈(多个线程运行,就会创建多个虚拟机栈,线程安全的)
    3. 每个栈由多个栈帧(frame) 组成,对应着每次方法调用时所占用的内存(但是每个线程只能有一一个活动栈帧,对应着当前正在执行的那个方法)

  1. 什么是本地方法栈?

    本地方法就是native method,它底层并不是由java实现的,一般是和操作系统底层比较相关的方法。

    本地方法栈就是用来支持本地方法的调用逻辑的

    本地方法栈和虚拟机栈一样 都是用于支持当前线程的方法调用的,所以都是线程安全的。


  1. 垃圾回收是否涉及栈内存?

    1. 垃圾回收主要指就是堆内存,对象存储在堆内存,垃圾回收器回收的主要是对象
    2. 对于栈而言,当栈帧弹栈以后(当前方法执行完毕),内存就会释放,所以不需要被GC回收

  1. 栈内存分配越大越好吗?

    1. 未必,默认的栈内存通常为1024k(1m)
    2. 栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半

  1. 什么情况下会导致栈内存溢出?

    1. 栈帧过多(方法调用过多)导致栈内存溢出,典型问题:递归调用
    2. 栈帧过大(每个方法分配的空间太大)导致栈内存溢出
    3. StackOverflowError

  1. 方法内的局部变量是否线程安全?

  1. 如果方法内局部变量没有逃离方法的作用范围(不作为形参也不作为返回值),它是线程安全的
  2. 如果是局部变量引用了对象,并逃离方法的作用范围(比如作为形参和返回值),则需要考虑线程安全问题

  1. 你能给我详细的介绍下Java的堆吗?

    1. 是所有线程共享的内存空间(有线程安全问题),可以被垃圾回收器回收

    2. 存储对象实例和数组

    3. 当堆内存不够时,就会抛出OutOfMemoryError异常

    4. 堆是由年轻代和老年代组成的(比例为1:2)

      年轻代又被分为了三个区域:Eden、survivor(from+to)

    5. 不同于数据结构中的堆(最大堆,最小堆)


  1. 能不能介绍一下方法区

    1. 方法区(Method Area)是各个线程共享的内存区域(具有线程安全问题)
    2. 方法区是一个抽象的概念,永久代(before)和元空间(now)是实现
    3. 主要存储类的信息、运行时常量池
      1. 运行时常量池可以看作是一张表, 虚拟机指令根据这张常量表找到要执行的信息
    4. 虚拟机启动的时候创建方法区,关闭虚拟机时释放方法区
    5. 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace

  1. 堆和栈的区别是什么

    1. jvm上:

      1. 堆内存是用来存储Java对象和数组的,堆内存会使用GC来执行垃圾回收,所有线程共有的(有线程安全问题)
      2. 栈内存一般会用来存储局部变量和方法调用,栈内存不会使用GC,不同的线程对应有不同的栈(没有线程安全问题)
      3. 两者出现内存溢出,都会抛异常,一个是OutOfMemoryError,一个是StackOverFlowError
      4. 栈的空间大小远远小于堆的空间
    2. 数据结构上:

      1. 栈是一种线性表,符合先进后出,后进先出的特点
      2. 堆是一种树形结构,是一种特殊的完全二叉树
        1. 如果所有的节点都>=它的父节点,那么这个堆就是最小堆
        2. 如果所有的节点都<=它的父节点,那么这个堆就是最大堆

  1. 你听过直接内存吗

    1. 直接内存:并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高
    2. 普通IO也叫做BIO,NIO的效率更高(比如作文件拷贝的时候)

  1. Java从源代码文件.java 到代码执行的过程

    1. **编译:**编译器将.java源代码文件编译成.class字节码文件(编译过程会对源代码做语法分析、语义分析、注解处理等等)
    2. **加载:**类加载器会将.class字节码文件加载到JVM中(加载过程又可以分为:装载、连接、初始化)
    3. **解释:**JVM可以把字节码转换为操作系统可以识别的指令
    4. **执行:**操作系统就可以调用CPU来执行指令了

  1. 什么是类加载器,类加载器有哪些?

    JVM只会运行二进制文件,类加载器的作用就是将.class字节码文件加载到JVM中,从而解释执行字节码文件以启动Java程序。

    1. 启动类加载器(BootStrap ClassLoader):加载JAVA HOME/jre/lib目录下的库(底层是c++实现的)
    2. 扩展类加载器(ExtClassLoader):加载JAVA HOME/jre/lib/ext目录中的类
    3. 应用类加载器(AppClassLoader):加载classPath下的类
    4. 自定义类加载器(CustomizeClassLoader):继承自ClassLoader,实现自定义类加载规则

  1. 什么是双亲委派模型?

    加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果上级已经加载了这个类了,就直接返回给下级子加载器,如果该类委托的上级没有被加载,子加载器才会加载该类。

【Java开发岗面试】八股文—Java虚拟机(JVM)_第3张图片

比如一个Student类,首先应用类加载器 向上委托到 扩展类加载器,扩展类加载器 继续向上委托到 启动类加载器,发现没有加载过这个类,那么子加载器(应用类加载器)就会加载Student类。

如果是String类,应用类加载器 一直向上委托到 启动类加载器,启动类加载器中已经加载过String类了,那么就可以直接返回给子加载器(应用类加载器)。

  1. JVM为什么采用双亲委派机制?

    1. 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,以保证类加载的唯一性。
    2. 为了安全,保证类库API不会被修改(比如String类已经被加载了,又去定义一个String类就会报错)。

  1. 说一下类装载的执行过程?【待学习】

    1. 加载:查找和导入.class文件
    2. 验证:保证加载类的准确性
    3. 准备:为类变量分配内存并设置类变量的初始值
    4. 解析:把类中的符号引用转换为直接引用
    5. 初始化:对类的静态变量、静态代码块执行初始化操作
    6. 使用:JVM开始从入口方法执行用户的程序代码
    7. 卸载:当用户程序代码执行完毕后,JVM便开始消耗创建的class对象

  1. 对象什么时候可以被垃圾回收器GC回收?

    简单的来说就是对象没有被引用了,也就是被定义为了垃圾对象,就可以被垃圾回收器回收了。

    那怎么定义对象是否为垃圾对象呢,一般会有两种方式:引用计数器法、可达性分析算法

    1. 引用计数法:每个对象都有一个引用计数器,记录了它被引用的次数,如果为0,就被定义为垃圾对象。

      虽然用起来简单,但是具有一个致命的问题,就是循环引用的问题,比如一个对象A有个属性引用了对象B,对象B有个属性引用了对象A,那么即使他们俩没有被其它的变量引用了,他们对应的引用计数器的值还是都为1,那实际上这两个对象都不需要用了,但是垃圾回收器也回收不了他们,就会引发内存泄露的问题。所以Java不是采用这种机制。

    2. 可达性分析算法:会有个GC Roots对象作为根节点,沿着这个节点向下走,如果可以被扫描到的对象,就是可达对象,也就是不能被回收的,不可达的对象就说明是垃圾对象,要被回收。

      可以作为GC Roots的对象一般有:

      1. 虚拟机栈中引用的对象
      2. 方法区中类静态属性引用的对象
      3. 方法区中常量引用的对象

  1. JVM垃圾回收算法有哪些?

    1. 标记-清除算法:分为标记和清除两个阶段,首先使用可达性分析算法标记所有存活的对象,标记完成后 统一回收所有没有被标记的对象(也就是垃圾对象)。

      缺点:标记清除之后会产生大量不连续的内存碎片,如果内存不连续了,则有可能不能存储一些占用内存大的对象或者数组。

    2. 标记-整理算法:对标记-清除算法的改进,也是先标记出所有存活的对象,然后把所有没有被标记的对象进行清除,但是它清除之后呢,会将目前存活的对象往一端移动,也就是进行整理,这样就使得内存是连续的,没有内存碎片了。

      但是因为多了个整理的过程,所以垃圾回收的效率会受到影响。

    3. 复制算法:把内存分为大小相等的两块区域,一片区域B不存放对象,另一片区域A使用可达性分析算法标记出存活的对象,统一放置到区域B,然后再把区域A中的剩余对象(也就是要被回收的对象)给全部回收了。

      优点:效率比较高,而且也没有内存碎片

      缺点:实际可使用的内存空间缩小为原来的一半,内存利用率较低。


  1. 说一下JVM中的分代回收?

    1. 堆的区域划分
      1. 堆被分为了两份:新生代和老年代(1: 2)
      2. 对于年轻代,内部又被分为了三个区域,Eden区,幸存者区survivor(分成from和to)(8:1:1)
    2. 对象回收分代回收的策略
      1. 新创建的对象,都会先分配到Eden区
      2. 当Eden区内存不足时,使用可达性分析算法标记出Eden区和from区存活的对象,将存活的对象采用复制算法复制到to区中,复制完毕后,将Eden区和from区的内存释放
      3. 经过一段时间后,Eden区又出现内存不足,标记Eden区和to区的存活对象,将其复制到from区
      4. 当from区对象经过了很多次垃圾回收(默认15次),直接放到老年代

  1. 说一下JVM有哪些垃圾回收器?

    1. 串行垃圾回收器

      是指使用单线程来进行垃圾回收,也就是垃圾回收时,只有一个线程在工作,适合堆内存较小的,比如说PC电脑。

      而且一个线程在进行垃圾回收的时候,其他所有线程都要暂停,等待垃圾回收的完成,

      具体来说包含Serial和Serial Old串行垃圾回收器:

      1. Serial作用于新生代,采用复制算法
      2. Serial Old作用于老年代,采用标记-整理算法
    2. 并行垃圾回收器

      JDK8默认采用的就是并行的垃圾回收器,也就是说有多个线程同时进行垃圾回收,而且其他的线程也都要暂停,等待垃圾回收的完成,才能继续执行。

      具体来说包含Parallel New和Parallel Old并行垃圾回收器:

      1. Parallel New作用于新生代,采用复制算法
      2. Parallel Old作用于老年代,采用标记-整理算法
    3. 并发(CMS)垃圾回收器

      CMS是一个并发的、使用标记-清除算法的垃圾回收器,该回收器主要是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。

    4. G1垃圾回收器


  1. 强引用、软引用、弱引用、虚引用是什么?有什么区别?

    引用主要是用来引用对象的,被分为四种类型,区别主要就是在对对象进行垃圾回收的时候,会有不同

    1. 强引用:最普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其引用的对象进行回收。
    User user = new User();
    
    1. **软引用:**如果一个对象时被软引用的,那么在垃圾回收的时候,如果内存空间还足够,这个对象就不会被回收,如果再次垃圾回收的时候,发现内存空间不足了,才会被回收软引用的对象。
    User user = new User();
    SoftReference softReference = new SoftReference(user);
    
    1. 弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。
    User user = new User();
    WeakReference weakReference = new WeakReference(user);
    

​ 4. 虚引用:配合引用队列使用


  1. JVM调优的参数可以在哪里设置

    1. 使用SSM做开发的时候,war包部署在tomcat服务器中,可以在tomcat的文件中设置

      修改TOMCAT_HOME/bin/catalina.sh文件

    2. 使用SpringBoot做开发的时候,是使用的jar包,在启动命令中设置

      Java -Xms512 -Xmx1024m -jar


  1. 用的JVM调优的参数都有哪些?

    JVM调优主要就是调整新生代、老年代的堆内存空间大小,虚拟机栈空间,元空间大小,还可以调整垃圾回收器的类型。

    新生代中的对象什么时候进入老年代?

    在对象被创建的时候,一般就是放在新生代,如果有这么几种情况就会放到老年代:

    1. 达到晋升的年龄:新生代对象在经历GC的时候,如果没有被回收,就会年龄+1,如果年龄到达阈值,就会进入老年代,默认情况下阈值为15,当然也可以通过JVM调优参数来设置。
    2. 如果创建的对象很大,也可能会直接进入老年代,具体的大小也可以通过JVM调优参数来设置。

  1. 说一下JVM调优的工具?

    1. jdk自带的命令工具:

      jps:进程状态信息
      jstack:查看Java进程内线程的堆栈信息
      jmap:查看堆转信息
      jhat:堆转储快照分析工具
      jstat:JVM统计监测工具

    2. 可视化工具
      jconsole:用于对jvm的内存,线程,类的监控

      VisualVM:能够监控线程,内存情况


  1. Java内存泄露的排查思路?

    (在实际开发中,遇到内存溢出的情况,怎么解决?借助什么工具?解决流程?

    内存泄漏有几种情况:

    1. 虚拟机栈:StackOverFlowError,一般就是递归造成的,方法调用过多,栈帧过多,虚拟机栈空间有限,一般为1024k,就容易爆栈,可以去看下递归的边界条件有没有设置好
    2. 方法区:OutOfMemoryError:Metaspace,一般是动态加载的类太多了,可以从这个方面去排查
    3. :OutOfMemoryError:java heap space,内存泄漏通常是指的堆内存,一般是出现了一些大对象没有被回收的情况
      1. 通过jmap或设置m参数 来获取堆内存的快照dump文件
      2. 通过VisualVM可视化工具工具 去分析dump文件,VisualVM可以加载离线的dump文件
      3. 通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
      4. 找到对应的代码,通过阅读上下文的情况,进行修复即可

  1. CPU飙高排查方案与思路?

    1. 在linux中可以使用top命令查看cpu的使用情况,可以看到是哪一个进程占用cpu较高
    2. 使用ps命令查看这个进程中的线程信息
    3. 使用jstack命令查看进程中哪些线程出现了问题,最终定位问题(可能的情况比如说,一个线程里面开了死循环)。

【后续继续补充,敬请期待】

你可能感兴趣的:(---Java开发岗面试---,java,面试,jvm,校招,秋招,春招)