JVM 面试题【初级】

953 阅读8分钟

JDK/JER/JVM 的关系

  • JVM: JVM 是java虚拟机,是一个软件,是java class 文件能在操作系统平台上运行起来的那个软件
  • JRE: JRE 是Java的运行环境,包括JVM标准实现及Java核心类库,仅仅依靠JVM是无法让java class文件跑起来的,还是需要一些额外的类库,这些加起来就是JRE java运行环境了
  • JDK: JDK 是java的开发工具包,在JRE的基础上加上了java的开发工具和类库

3者之间是一级一级包含关系:

  • JDK=JRE+开发工具
  • JRE=JVM+辅助类库

从类库的角度看:

结合实际深化理解

  1. sun 官方的 java 虚拟机一直都是 HotSpot,HotSpot 只是一个名字,具体的 HotSpot 虚拟机都会随着 JDK 版本的叠加升级的
  2. 日常开发中,我们安装 JDK 的目的是为了编写 java 代码生成 class 文件,和实际运行是有差异的
  3. 我们写的 java 代码在 android 系统上运行,是依托于 android 上安装的 JRE 运行环境的,和我们编写代码时用的 JDK 不是一回事,android 系统上安装的是哪个版本的JVM就按哪个版本的JVM去运行
  4. android 系统 JRE 环境中,实际运行的是自己开发的 Dalvik、ART 虚拟机,所以运行时数据区会跟我们学的 JVM 有些区别的

搞清楚这些区别其实很重要,要不你再学习完JVM后,再看android内存分配你会有些懵


什么是垃圾

垃圾是指在运行程序中没有任何指针指向的对象,这些对象就是需要被回收的垃圾

GC 的回收范围仅仅只有堆和方法区


哪些对象可以作为 GC Roots

  • 虚拟机栈中的对象
    • 比如:栈帧里局部变量表里的对象
  • 本地方法栈JNI里的对象
  • 静态对象
  • 常量对象
    • 比如:字符串常量池中的字符串对象
  • 被同步锁 aynchronized 持有的对象
  • java 虚拟机内部的对象
    • 比如:所有基本数据类型对应的class对象,系统类加载器

应用分析工作必须在一个能保证一致性的内存快照中进行,所有必须停止用户线程,堆用户线程的停顿叫:STW - stop the world,即便是在之前的并行垃圾回收器CMS中,枚举根节点时一样会有STW操作


异常和错误

有的面试官会挖这个坑:你说说遇到的错误

  • Exception - 是异常,是代码里常常写的
  • Error - 是错误,像内粗溢出 OOM 这样的就是

坏就坏在你可能没注意说成另一个了,的确有这样坏心眼的家伙(╯‵□′)╯︵┴─┴


怎么判断2个类是同一个类

  • 类名+包名相同
  • 加载这2个类的类加载器对象必须是同一个

类主动使用方式

类的使用可以分主动使用、被动使用,区别是被动使用类时,不会导致类的初始化,这会导致很多难以查找的 Bug 的

  • 通过new关键字、反射、clone、反序列化机制实例化对象
  • 调用类的静态方法时
  • 使用类的静态字段或对其赋值时
  • 通过反射调用类的方法时
  • 初始化该类的子类时(初始化子类前其父类必须已经被初始化)
  • JVM启动时被标记为启动类的类(简单理解为具有main方法的类)

创建对象的几种方式

  • new用new语句bai创建对象,这是最常du用的创建对象的方式。
  • Class.newInstance(),JDK9 以后过时了,只能使用public的无参构造方法
  • Construtcor.newInstance(xx) 有参无参都可以,也没有访问限制,这就是反射的好处
  • clone()
  • 反序列化手段: 比如IO,ObjectInputStream 的readObject() 方法,从本地文件,或者网络上获取
  • 第三方库Objenesis 利用字节码技术,动态生成class信息

这个大家了解,知道有这么回事就行,注意反射的2种方式差别,实际场景会用上


从字节码的角度看对象的创建

public class Max {
    public void run() {
        String name = new String();
    }
}

字节码:

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: invokespecial #3                  // Method java/lang/String."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
  • 先走了一个new指令,会判断这个类加载没加载,加载完后去堆内存申请一块内存空间并返回该地址到操作数栈栈顶
  • dup 指令会把刚刚申请到的内存地址复制一份并保存到栈帧中操作数栈栈顶,这样之后操作数栈实际上就上下有2个相同的内存地址了
  • 执行String对象的初始化方法,这需要地址以便找到对象去执行方法,所以这里会消耗调一个内存地址的操作数
  • 然后把这个堆内存地址赋值给局部变量表里的name属性,所以才会有dup指令把内存地址复制一边

通常这个问题是和 volitale 保证有序性、单例模式的双判断写法为什么还要加上 volitale 关键字这个问题联系在一起问的


对象创建的步奏

  1. 判断对象对应的类是否被加载、链接、初始化了 也就是new指令会去方法区常量池中定位到类的符号引用,检查这个符号引用是否被加载了
  2. 给对象分配内存空间,计算对象需要占用多大的内存空间
    • 堆内存规整:指针碰撞
    • 堆内存不规整:JVM要维护一个空闲列表,记录哪些内存块可用的,哪块是用过的,碎片化的问题,对应的是标记清除算法,伊甸园区回收完垃圾之后不做规整整理就这样,比如早期的CMS垃圾回收器
  3. 并发处理 堆内存是共享区域,就会有并发问题
    • CAS 失败重试,区域加锁保证原子性
    • 堆内存给每个线程预先分配一块TLAB空间,也就是针对每个线程预先给一块专属的内存空间,以防止并发问题,问题是这块空间不大,所以还需要上面CAS的配合,可以通过-XX:UseTLAB来设置
  4. 初始化分配到的内存空间(默认初始化) 也就是给属性赋一个初始的默认值,即便该属性在代码里设置值了,在这一步也会先给一个默认值,在之后的步奏里再赋指定值
  5. 设置对象头 对象头保存有对象的hashcode,GC信息,锁信息,所属类(方法区元数据地址)
  6. 显示初始化,执行init方法并初始化 具体可以看下一个问题的解答,init就是类的构造器

实际到代码

Dog dog = new Dog();

new 关键字可以理解为1-5步,Dog() 相当于第6步

指针碰撞:

用过的内存在一边,没用过的内存在另一边,中间临界点有一个指针。要是需要新分配内存空间了,只需要把指针移动执定大小即可。不过JVM是否采用这种分配方式,的看采用的垃圾回收器选择的是不是 Serial、ParNew 这样的压缩算法(标记整理)了。核心就是垃圾回收之后新生代伊甸园区的是规整过的才能这么玩


对象属性赋值过程

  1. 属性的默认初始化
    给对象中的属性赋一个默认值,比如 int age = 100,在这一步 age=0
  2. 显示初始化/代码块初始化
    还是拿 age 说事,到这里 age=100 了
  3. 构造器中初始化

上个问题6里面的init方法对应的就是这里的2、3部


对象访问的2种方式

有:句柄式、直接指针 2种

1. 句柄式

在JVM堆内存中,维护一个列表,存储每个对象对应的内存地址,好比我们在数据库搞一个索引表出来,大家要理解句柄的含义,就是索引,句柄是C++的内容,但是在java里好多地方还有她的身影

句柄的好处在这个索引表的内存地址是不变的,不管是对象的地址怎么变,对于别人来说其实都是不变的

缺点是真用内存,访问效率低一些

2. 直接指针

这是目前 htospot 虚拟机访问对象的方式,不需要索引表,我直接记录你的内存位置,缺点是地址变了需要通知使用者

这道题还是知道的好,还真就有人问。具体就是涉及到 GC 的标记清除算法上的优缺点,句柄式的可以并发的GC垃圾回收,因为句柄是不变的,我恩始终可以找到对象。直接指针GC我们就必须停止其他线程,因为引用地址会变


空对象占内存多少

这是一道常问的面试题,我们只考虑一般对象,对象实体是空的,Mark Word 占64位8个字节

类型指针默认是8个字节的,但是在开启指针压缩时会变成4个字节,JDK8 是默认开启的

参考对齐方式,对象的大小必须能被8整除。所以一个空对象默认占用内存大小是12个字节,算上对齐方法的化是16个字节