JVM内存面试总结

1.JVM内存结构

程序运行时,java虚拟机将内存划分为以下几个区域。其中,线程私有的有:程序计数器,虚拟机栈,本地方法栈;线程共享的有:堆,方法区,直接内存

  • 程序计数器:获取下一条指令的地址,实现程序的流程控制;在多线程情况下,用来记录当前线程执行的位置,使线程切换回来时能定位到该位置。它是唯一不会发生outofmemory内存溢出的地方。
  • 虚拟机栈和本地方法栈:生命周期和线程一致,栈中存放的是栈帧,一个函数一个栈帧,每个栈帧中存在 局部变量表(基本数据类型和对象引用),操作数栈,动态链接,方法出口信息
    栈中会出现两种异常:
    1.stackoverflowerror 栈溢出:若内存大小不允许动态拓展,且当前线程申请的栈的深度大于栈的最大深度时,会发生栈溢出
    2.OutOfMemery(内存溢出):若内存大小允许动态拓展,当前栈内存不够用,无法动态拓展时,发生内存溢出。
  1. 内存中最大的一块,唯一作用就是存储对象的实例,几乎所有对象和数组在这里分配内存.
  2. 另外,它是垃圾回收的主要地方。为了提升gc效率,通常我们对堆进行分代,新生代和老年代,新生代分为伊甸园区,存活区s0,s1,比例为8:1:1,每次留一块s区作为复制的备份内存,并将老年代作为分配担保区。大部分对象首先被分配到新生代,一次gc后存活对象进入s0或s1区,且年龄加1,默认当年龄加到15时,进入老年代,这可进入老年代的阈值,可以通过设置参数
    -maxtunuringthreshold来调整。另外,大对象和长期存活对象会直接进入老年代。
  3. 不同代采用不同的垃圾回收算法。比如新生代每次gc都会有大量对象被回收,因此选用复制算法,老年代对象存活几率比较高,没有额外的分配担保区,因此选用标记整理法,只有cms采用标记清除。
  • 方法区
    存储被jvm加载的类信息,常量,静态变量和编译后的代码等数据,是线程共享的。JDK1.8后被元空间取代,位于直接内存中。替换的原因,一是方法区有jvm设置了固定大小上限,无法调整,而元空间使用直接内存,之受限于本机内存大小,不会发生OOM。
  • 常量池
    1.1.8以前放在方法区,大小受限于方法区。1.8以后放在堆中,主要由字面量(字符串,基本数据类型的值,常量值)和符合引用组成。
  • 直接内存
    直接内存不是运行时数据区第一部分,也不是jvm规范定义的内存,它的分配不会受到堆的限制,但他受本机内存总大小的和处理器寻址空间的限制。

2. java创建对象的步骤

  1. 类加载检查:先检查对象所属类是否已经被加载,解析,初始化过,如果没有,先进行类的加载
  2. 分配内存:为对象分配内存,两种分配方式:指针碰撞(适用于堆内存规整)和 空闲列表(堆内存不规整)
  3. 初始化0值:除对象头以外,内存空间都初始化为0值。
  4. 设置对象头:设置一些类的信息放在对象头中。如类的元数据信息,这个对象是哪个类的实例,对象的哈希码,对象gc年龄等。
  5. 执行init方法:对对象真正初始化。

3 . 对象的内存布局

主要分为三部分:对象头,实例数据,对象对齐。
对象头一是运行时数据信息,如对象哈希码,对象gc年龄,锁状态等,二是类型指针,指向类元数据。

4. 对象访问方式

  1. 使用句柄:在内存中开辟句柄池,栈中引用存放对象的句柄地址,句柄中包含堆中对象的实例数据,以及方法区中对象的类型数据。
  2. 直接指针:栈中引用存放对象的地址,堆中包含了实例数据,另外对象头中存放了方法区中对象类型数据的地址。
    在这里插入图片描述
    在这里插入图片描述

5. 如何判断对象是否死亡

引用计数法
给对象设置一个计数器,当对象被引用时,计数器+1,当引用失效,计数器-1,当计数器为0时,该对象不再被引用。
可达性分析算法
引用计数法无法解决循环引用产生的问题,即两个对象互相引用对方,虽然对象不能再被引用了,但是计数器不为0,他们无法被回收。
因此采取可达性分析法解决该问题。通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则说明此对象不可达。
在这里插入图片描述

6. GC什么时候回收垃圾

对于方法区中的常量和类,用引用计数判断一个常量没有任何对象引用它,它就可以被回收了。而对于类,如果可以判定它为无用类,就可以被回收了。
对于堆中的对象,主要用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。根据实际对引用的不同需求,又分成了 4 种引用,每种引用的回收机制也是不同的。

  1. 强引用:new的对象,只要强引用还在,gc永远不会回收该引用。
  2. 软引用:描述一些还有用但非必须的对象,只有内存溢出时,才对它进行回收。
  3. 弱引用:程度比软引用弱一点,只要gc发现了该引用,无论内存是否够,都对他进行回收。
  4. 虚引用:引用形同虚设,不会决定对象的生命周期,在任何时候都可以被回收。

7.如何判断一个类是无用的类?

  • 堆中没有该类的任何实例
  • 该类的ClassLoder被回收
  • 该类的java.lang.class对象没有被引用,无法通过反射访问该类的方法

8. 垃圾收集算法

  • 标记-清除算法
    首先标记所有需要回收的对象,然后后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
    但带来两个明显的问题:
    效率不高
    空间问题(会产生大量不连续的碎片)
    在这里插入图片描述
  • 复制算法
    为了解决效率问题,有了“复制”收集算法。它将内存分为大小相同的两块,每次使用其中的一块,使用完后,就将还活的对象复制到另一块去,然后再把使用的空间一次清理掉。
    在这里插入图片描述
  • 标记-整理算法
    标记所有存活的对象,让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。
    在这里插入图片描述
  • 分代收集算法
    堆被分为新生代和老年代,根据各个年代的特点选择合适的垃圾收集算法。

9.类的加载过程

加载->连接->初始化(其中连接包括:验证,准备,解析)
在这里插入图片描述

  • 加载:(主要完成三件事)
  1. 通过全类名获取类的二进制字节流
  2. 将类的静态存储结构转化为运行时数据结构
  3. 在内存中生成类的class对象,作为方法区的入口
  • 验证
    对元数据,文件格式,字节码,符号引用等验证正确性
  • 准备
    在方法区中为类变量分配内存,并初始化为0
  • 解析
    将符号引用转为直接引用
  • 初始化
    执行类的clinit()方法,真正的初始化

10. 知道哪些类加载器?

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:

BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 /lib目录下的jar包和类或者。
ExtensionClassLoader(扩展类加载器) :主要负责加载目录/lib/ext 目录下的jar包和类
AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

11. 双亲委派模型知道吗?能介绍一下吗?

每一个类都有一个对应它的类加载器。系统默认使用 双亲委派模型 。
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
如果没有加载,首先会将类加载转发给父类加载器的 loadClass() 处理,一直转发到启动类加载器 。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,null并不代表没有父类加载器,而是启动类加载器。

优点:

  1. 避免类的重复加载(相同类被不同类加载器加载会产生不同的类),保证程序的稳定运行
  2. 保证核心API不被修改

破坏双亲委派机制:重载loadClass()方法
自定义类加载器:继承loadClass(),重写findClass(),调用defineClass()方法

12. 类什么时候被初始化?

1)创建类的实例,也就是 new 一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6) JVM 启动时标明的启动类,即文件名和类名相同的那个类

13. 类的初始化步骤:

1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。

14. 获得一个类对象有哪些方式?

  1. 类型.class,例如: String.class
  2. 对象.getClass(),例如:” hello” .getClass()
  3. Class.forName(),例如: Class.forName(“java.lang.String” )

15 .创建对象的几种方式

  1. new关键字创建
  2. 调用对象的clone的方法
  3. 利用反射,调用Class类的或者Constructor类newInstance()方法
  4. 用反序列化,调用ObjectInputStream类的readObject()方法