JVM面试题

1 说一下Java运行时的数据区域/JVM的内存结构

JVM有5个区域,程序计数器、虚拟机栈、本地方法栈、堆、方法区,其中堆和方法区是所有线程共有的,本地方法栈、虚拟机栈、程序计数器是线程私有的

  • :存放对象实例,是垃圾收集器管理的主要区域,也被称为GC堆。堆又被分为新生代和老年代,而新生代又被分为Eden(伊甸)区、from Survivor区(Survivor0区)、to Survivor区(Survivor1区),默认按照8:1:1的比例进行分配
    • 新生代中一共存在两个Survivor区:S0/S1,也被称为或From/To区,这两个区域在同一时刻,永远有一个是空的,当下次GC发生时,作为存活对象新的“避难所”。但From/To两个名词并不是一个区域固定的称呼,而是动态的,存放对象的Survivor区被称为From区,而空的Survivor区被称为To区
  • 方法区:存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 虚拟机栈:由一个个栈帧组成,而每个栈帧中都有局部变量表、操作数栈、动态链接、方法出口信息
  • 本地方法栈:虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机执行Native方法服务。本地方法被执行时,在本地方法栈也会创建一个栈帧用于存放该本地方法的局部变量表、动态链接、方法出口信息、操作数栈
  • 程序计数器:用于记录当前虚拟机正在执行的线程指令地址,是程序控制流的指示器

2 堆和栈的区别

  • 堆的物理地址分配是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快
  • 堆存放的是对象实例和数组,栈存放的是局部变量、操作数栈
  • 堆是线程共享的,栈是线程私有的

3 对象创建过程

Java中对象创建分为5步

  • 第一步是类加载检查:当虚拟机遇到一条new指令时,首先检查这个指令的参数是否可以在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,则先执行类加载
  • 第二步是分配内存:在类加载检查通过后,接下来虚拟机将为对象实例分配内存
  • 第三步是初始化:将分配到的内存空间都初始化为零
  • 第四步是设置对象头
    • 对象头包括:存储对象自身的运行时数据(哈希码、分代年龄、锁标志)、类型指针和数据长度(数组才有)
  • 最后一步是执行init方法把对象按照程序员的意愿进行初始化
    图片

3.1 创建对象的方法有哪些

  • 使用new关键字
  • 使用反射,调用类class的newInstance方法
  • 调用对象的clone方法
  • 使用反序列化,调用类ObjectInputStream的readObject方法
     图片

3.2 怎么为对象实例分配内存/内存分配方式有哪些

分配方式有指针碰撞和空闲列表两种

选择哪种分配方式由堆是否规整决定,而堆是否规整又由所采用的垃圾回收器是否带有空间压缩整理的能力决定

  • 指针碰撞:在堆内存规整(即没有内存碎片)的情况下,用过的内存全部整合到一边,没用过的内存放在另一边,中间有一个分界指针,分配内存只需把这个指针向没用过的内存方向移动对象内存大小位置即可
  • 空闲列表:在堆内存不规整的情况下,用过的内存空间和没用过的内存空间交错,虚拟机会维护一个列表用于记录哪些内存是可用的,分配内存时需要找到一块足够大的内存划分给对象实例,分配后更新列表记录

3.3 内存分配策略是什么样的/内存分配原则

  • 对象优先在Eden区分配:大多数情况下,对象优先在新生代中的Eden区分配(分配方式在3.2),当Eden内存空间不足时,就会发生Minor GC,当发生Minor GC时,JVM采用垃圾回收算法中的复制算法将存活的对象复制到另一个未使用的Survivor区,当Survivor区也满时,就会使用分配担保机制将对象移动到老年代
  • 大对象直接进入老年代:大对象是指需要连续内存空间的对象,设置JVM的参数-XX:PretenureSizeThreshold,大于此值的对象直接在老年代进行分配
    • 最典型的大对象有长字符串、大数组
  • 长期存活的对象进入老年代:为对象定义年龄计数器,对象在Survivor区每经过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度,就会移动到老年代,通过参数-XX:MaxTenuringThreshol定义年龄阈值
    • 从Survivor区进入老年代
  • 动态对象年龄判定:并非对象的年龄必须达到MaxTenuringThreshol才能移动到老年代,如果Survivor中相同年龄的所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象直接进入老年代,无需达到MaxTenuringThreshold年龄阈值
    • 从Survivor区进入老年代
  • 空间分配担保:分配担保是为了确保在Minor GC之前老年代最大可用的连续空间是否大于新生代所有对象总空间。如果条件成立,那么Minor GC是安全的;如果不成立,虚拟机就会查看HandlePromotionFailure的值是否允许担保失败,如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于就尝试着进行一次Minor GC,如果小于或者HandlePromotionFailure的值为不允许担保失败,那么就进行一次Full GC

3.4 什么是Minor GC,它与Full GC有什么区别

  • Minor GC/Yong GC:回收新生代。因为新生代中的对象存活时间很短,所以Minor GC会频繁执行,执行速度也比较快
  • Full GC:回收整个堆,包括老年代和新生代。因为老年代中的对象存活时间长,所以Full GC很少执行,执行速度比Minor GC慢很多

3.5 说一下类加载的过程

类加载是指JVM将类的class文件中的二进制数据加载进内存,并进行解析生成对应的class对象的过程

JVM不是一开始就把所有的类都加载进内存,而是只有第一次遇到某个需要运行的类时才会加载并且只加载一次
类加载过程主要有三步:加载、连接、初始化,而连接又可以分为三步:验证、准备、解析

  • 加载:通过类加载器把class文件加载进内存,并生成一个代表该类的class对象
  • 验证:确保class文件符合虚拟机规范,主要是文件格式验证、元数据验证、字节码验证、符号引用验证四种验证
  • 准备:在方法区为类变量分配内存空间并设置类变量初始值
    • static int i=5这里只将i初始化为0,至于5的值将在初始化时赋值
  • 解析:虚拟机将常量池内的符号引用替换为直接引用
    • 符号引用用于描述目标,直接引用直接指向目标地址
  • 初始化:执行类构造器对类变量初始化

准备阶段是jvm赋初值,8种基本类型的初值,默认为0;初始化阶段,则根据程序员自己设置的变量赋值;该初始化不同于对象实例的最后一步初始化,该初始化执行clinit方法,而对象实例初始化执行init方法

3.6 类加载器有哪些

有4种类加载器

  • 启动类加载器:加载Java核心类库,由c++实现
  • 拓展类加载器:加载Java扩展类库
  • 应用类加载器/系统类加载器:根据应用的类路径加载类
  • 自定义加载器

3.7 为什么会有自定义类加载器

  • Java代码很容易被反编译,如果需要对自己的代码进行加密,可以对编译后的代码进行加密,然后再通过自定义加载器进行解密,最后再加载
  • 有可能从非标准的来源加载代码,比如网络来源

3.8 双亲委派模型了解吗

双亲委派模型是指当一个类加载器收到一个类的加载请求时,不会自己尝试加载而是把这个请求委派给父类加载器去完成,以此类推,所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载

图片

3.9 采用双亲委派模型的好处/为什么采用双亲委派模型

  • 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载
  • 保证了Java的核心api不被篡改

4 如何判断对象已死

有两种方法判断对象已死:引用计数法和可达性分析

  • 引用计数法:在对象中添加一个引用计数器,每当有一个地方引用这个对象,计数器就加1,当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的
    • 一般主流的虚拟机不采用这种方法,因为它很难解决对象之间互相引用的问题
  • 可达性分析:通过GC root对象作为起点,从这些节点开始向下搜索,搜索所走过的路径叫引用链,当一个对象到GC root之间没有任何的引用链相连,则说明这个对象是不可用的

4.1 GC root可以是哪些对象/有哪些GC root

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
    • 在Java中同步锁通过synchronized关键字和Lock接口的xxxLock实现,具体见并发相关知识

5 垃圾回收算法

垃圾回收算法有四种:标记清除算法、复制算法、标记整理算法、分代收集算法

  • 标记清除算法:该算法分为“标记”和“清除”阶段,首先利用可达性分析遍历内存,把存活对象和垃圾对象进行标记,标记结束后将所有垃圾对象回收
    • 这种垃圾回收算法效率低,并且会产生大量不连续的空间碎片
  • 复制算法:将内存分为大小相等的两块,每次使用其中的一块,当这一块的内存使用完后,将还存活的对象复制到另一块中,然后再把使用的空间一次性清理掉
    • 这个算法的特点是实现简单,效率高,但是可用内存缩小为原来的一半,浪费了空间
  • 标记整理算法:标记过程与标记清除算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象都向一端移动,然后直接清除边界以外的内存
  • 分代收集算法:根据各个年代的特点采用最适当的回收算法:新生代使用复制算法,老年代使用标记清除算法或者标记整理算法。
    • 因为新生代中每次垃圾回收都有大批对象需要回收,只有少量存活,所以采用复制算法,而老年代对象存活率高,适合使用标记清除或者标记整理算法

6 有几种垃圾回收器/GC

有7种垃圾回收器,其中新生代中有Serial、ParNew、Parallel Scavenge,老年代中有CMS、Serial Old、Parallel Old,还有不分年代的G1

6.1 现在jdk默认使用的是哪种垃圾回收器

  • 到jdk8为止,默认的垃圾收集器是Parallel Scavenge和Parallel Old,新生代使用Parallel Scavenge,老年代使用Parallel Old
  • 从jdk9开始,G1收集器成为默认的垃圾收集器

6.2 GC按照类型划分可以分为几类/GC的分类

针对HotSpot VM的实现,GC分为部分收集(Partial GC)和整堆收集(Full GC)两大种,其中部分收集又分为新生代收集(Minor GC/Young GC)、老年代收集(Major GC/Old GC)、混合收集(Mixed GC)

  • Minor GC:只对新生代进行垃圾回收
  • Major GC:只对老年代进行垃圾回收
  • Mixed GC:对整个新生代和部分老年代进行垃圾回收,目前只有G1收集器支持该行为,其他收集器不支持
  • Full GC :回收整个Java堆和方法区

6.3 说一下它们发生的时机/触发条件/垃圾回收时机

  • Minor GC:Eden区空间不足时会触发一次Minor GC
  • Major GC:老年代空间不足时会触发一次Major GC
    • Major GC的出现往往伴随着至少一次的Minor GC,但并非绝对
  • Full GC
    • 调用System.gc(),建议虚拟机执行Full GC,但是虚拟机不一定真正去执行
    • 老年代空间不足
    • 空间分配担保失败
      • 使用复制算法(因为对新生代进行回收)的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC
    • JDK1.7及之前的永久代空间不足
      • 在jdk1.7及以前,虚拟机中的方法区由永久代实现,永久代中存放的为一些class的信息、常量、静态变量等数据
posted @   ycylikestuty  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示