JVM相关面试题总结

一个关于内存的连环炮

内存分为哪几部分,这些部分分别都存储哪些数据?

名称 特征 作用
虚拟机栈 线程私有,使用一段连续的内存空间 存放原始类型的值、对象引用、局部变量表、操作栈、动态链接、方法出口
程序计数器 线程私有,占用内存小 存储字节码行号,用于分支、循环、跳转、异常、线程恢复等
线程共享,生命周期与虚拟机相同 保存对象实例
方法区 线程共享 存储类加载信息、常量、静态变量等

一个对象从创建到销毁都是怎么在这些部分里存活和转移的?

  1. 对象的创建
    • 定位到方法区常量池的类,检查类是否已经被加载、验证、准备、解析。
    • 检查类是否已经进行过初始化。
    • 在内存中分配空间
    • 设置对象的默认初始值(0,false,null)
    • 执行()方法(类变量的赋值和静态代码块)
    • 执行构造
    • 在线程栈中设置对象引用
  2. 对象在内存中的布局
    对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。
    • 对象头:每一个堆中的对象在HosPost虚拟机中都有一个对象头,对象头里存储两部分信息:一部分是运行时对象自身的哈希码值(HashCode)、GC分代年龄(Generational GC Age)、锁标志位(占用2个bit的位置)等信息,另一部分是指向方法去中的类型信息的指针。如果是数组对象的话,还会有额外一部分用来存储数组长度。
    • 实例数据:是程序代码中定义的各种类型的字段内容。
    • 对齐填充:并不是必然存在的,仅仅在对象的大小不是8字节的整数倍时起占位符的作用。
  3. 对象的访问方式
    • 句柄:java堆中划出一块空间单独存放句柄池,java栈中的reference指向句柄池中的某个句柄,句柄包含两部分信息:堆中实例对象地址和方法区中对象类型信息。
    • 直接指针:java栈中的reference指向java堆中的实例对象,每个实例对象上有一部分信息是用来指向该对象在方法区中的类型信息。
    • 句柄的优势:当对象发生改变时,reference的值不用变,只需要改变句柄的实例指针,reference自身不需要改变。直接指针的优势:节省了一次指针定位的开销,速度更快。HosPot虚拟机采用第二种方式。
  4. 销毁
    对象完成使命后,等待GC进行垃圾回收。销毁对象即清理对象所占用的内存空间,会调用对象的finalize()方法。

内存的哪些部分会参与GC的回收?

主要是堆,还包括方法区(针对常量池的内存回收和对已加载类的卸载)

Java的内存模型是怎么设计的?

共享内存模型。线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本

为什么要这么设计?

一方面要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能的放松。

结合内存模型的设计谈谈volatile关键字的作用?

volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障(Memory Barrier )指令实现的。
Memory Barrier可以强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本,如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:

  1. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
    在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。
  2. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

一个GC部分的连环炮

GC是在什么时候,对什么东西,做了什么事情

什么时候

GC 分为minor gc/full gc
eden满了触发minor gc,升到老年代的对象大于老年代剩余空间触发full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM。

对什么东西

超出作用域的对象/从gc root开始搜索,搜索不到的对象/弱引用、软引用

做了什么事情

GC机制的基本算法是:分代收集
删除不使用的对象,腾出内存空间,停止其他线程执行、运行finalize
新生代做的是复制回收、from survivor、to survivor是干啥用的、老年代做的是标记整理、复制整理和标记回收有有什么优劣

堆分成三个区域:

  1. 新生代(Young Generation):用于存放新创建的对象,采用复制回收方法,如果在s0和s1之间复制一定次数后,转移到年老代中。这里的垃圾回收叫做minor GC;
  2. 年老代(Old Generation):这些对象垃圾回收的频率较低,采用的标记整理方法,这里的垃圾回收叫做 full GC。
  3. 永久代(Permanent Generation):存放Java本身的一些数据,当类不再使用时,也会被回收。

在新生代中,分为三个区:Eden, from survivor, to survior。

  • 当触发minor GC时,会先把Eden中存活的对象复制到to Survivor中;
  • 然后再看from survivor,如果次数达到年老代的标准,就复制到年老代中;如果没有达到则复制到to survivor中,如果to survivor满了,则复制到年老代中。
  • 然后调换from survivor 和 to survivor的名字,保证每次to survivor都是空的等待对象复制到那里的。

复制整理和标记回收有有什么优劣

  • 复制整理不涉及到对象的删除,只是把可用的对象从一个地方拷贝到另一个地方,因此适合大量对象回收的场景,比如新生代的回收
  • 标记回收:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续,适用于低频操作、数量少,大对象回收,如老年代,但会增加停顿时间

参考链接

类加载机制的简单连环炮。

Java的类加载器都有哪些?

启动类加载器、扩展类加载器、系统类加载器。类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。

每个类加载器都加载哪些类?

  • 启动类加载器主要加载的是JVM自身需要的类,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。
  • 扩展类加载器负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。
  • 应用程序类加载器负责加载用户类路径classpath下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用,一般情况下该类加载是程序中默认的类加载器。

什么是双亲委派模型?

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载

为什么Java的类加载器要使用双亲委派模型?

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
安全性考虑,系统定义的类只能由顶层类加载器加载

如何自定义自己的类加载器,自己的类加载器和Java自带的类加载器关系如何处理?

继承ClassLoader,重写findClass方法,实现加载自定义路径下class文件的逻辑,