JVM高频面试题
一、JVM 的内存模型
1.功能区(绿色)
- 类加载器:文件系统加载 class文件
- 垃圾回收器:对方法区,Java堆,直接内存进行垃圾回收
- 字节码执行引擎:将 JVM 可识别的字节码转换为操作系统可识别的机器码
2.线程私有区(蓝色)
- Java栈:创建线程的时候栈被创建
- 本地方法栈:用于本地方法调用
- PC寄存器:指向 JVM 下一条执行指令的地址
3.线程共享区(橙色)
- 方法区:存放类的方法代码、变量名、方法名、访问权限、返回值等
- 堆:用于存放实例对象
- 直接内存:指的是直接去内存条申请内存,而不是在JVM内存中申请,一般用于 NIO
二、栈内存结构
- 栈的概念:每创建一个线程就会创建一个栈,每一个栈有多个栈帧和一个 PC 程序计数器
- 栈帧存储的数据
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
- 栈操作(Operand Stack):记录出栈、入栈的操作。
- 栈帧数据(Frame Data):包括类文件、方法等等。
- 栈帧的工作原理:每执行一个方法都会产生一个栈帧,保存到栈的顶部,顶部栈就是当前方法,该方法执行完毕后会自动将此栈帧出栈。
- 栈溢出异常(StackOverflowError):方法每一次调用都会在栈空间中申请一个栈帧,来保存本次方法执行时所需要用到的数据。但是一个没有退出机制的递归调用,会不断申请新的空间,而又不释放空间,这样就会把当前线程在栈内存中的空间耗尽。
三、堆内存结构
- Eden space 区:主要是生命周期很短的对象来来往往
- 老年代:主要是生命周期很长的对象,例如:IOC容器对象、线程池对象、数据库连接池对象等等
- 幸存者区:为二者之间的过渡地带
堆空间工作机制:
-
新创建的对象会被放在 Eden space 区 -
当 Eden space 区中已使用的空间达到一定比例,会触发 Minor GC -
每一次在 Minor GC中没有被清理掉的对象就成了幸存者 -
幸存者对象会被转移到幸存者区 -
幸存者区分成 from(s0) 区和 to(s1) 区,from区快满的时候,会将仍然在使用的对象转移到to区 然后from和to这两个指针彼此交换位置,口诀:复制必交换,谁空谁为to -
年轻代对象转移到老年代的三种情况
- 如果一个对象,经历15次GC仍然幸存,那么它将会被转移到老年代
- 如果幸存者区已经满了,即使某个对象尚不到15岁,仍然会被移动到老年代
- 当一个对象过大,超过指定的大小的时候,也会跑到老年代
-
老年代的内存空间也要不足是会触发 Full GC 整体清理内存
堆溢出异常(OutOfMemoryError):
- 堆溢出异常,针对新生代、老年代整体进行Full GC后,内存空间还是放不下新产生的对象
- 元空间异常,方法区中加载的类太多了(典型情况是框架创建的动态类太多,导致方法区溢出)
- 栈溢出异常,可能导致 OutOfMemory (申请栈空间不足时)
四、方法区内存结构
本地内存、直接内存、JVM内存三者之间的关系
方法区的作用:存的是静态变量、类信息、常量
不同版本的方法区结构
- 1.6版本:由永久代(PermGen)实现,方法区的内存是向 JVM 申请的,串池在常量池中
- 1.8版本:由元空间(Metaspace)实现,方法区逻辑上属于JVM管理,元空间实际在本地内存中,内存是向内存条申请的,串池在堆中
五、双亲委派机制
1.类加载器的分类
- Bootstrap ClassLoader:启动引导类加载器
- Extension ClassLoader:拓展类加载器
- App|System ClassLoader: 应用(系统)类加载器
- User-Defined ClassLoader: 用户自定义类加载器
2.含义
- 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类
3.工作流程
- 当我们需要加载任何一个范围内的类时,首先找到这个范围对应的类加载器
- 但是当前这个类加载器不是马上开始查找,当前类加载器会将任务交给上一级类加载器
- 上一级类加载器继续上交任务,一直到最顶级的启动类加载器
- 启动类加载器开始在自己负责的范围内查找
- 如果能找到,则直接开始加载
- 如果找不到,则交给下一级的类加载器继续查找,一直到应用程序类加载器
- 如果应用程序类加载器同样找不到要加载的类,那么会抛出 ClassNotFoundException
4.作用
- 避免类的重复加载:父加载器加载了一个类,就不必让子加载器再去查找了。同时也保证了在整个 JVM 范围内全类名是类的唯一标识。
- 安全机制:避免恶意替换 JRE 定义的核心 API
六、谈谈你对 GCRoots (可达性分析)的理解
- 核心原理:判断一个对象,是否存在从『堆外』到『堆内』的引用。
- GC Root 对象:就是从根节点出发,顺着引用路径一路查找到堆空间内,找到堆空间中的对象。
- Java 栈中的局部变量、本地方法栈中的局部变量、方法区中的类变量、常量
七、四大引用
- 强引用(Reference):只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收,否则死都不收
- 软引用(SoftReference):为了降低 OOM 发生的概率,内存够用就保留,不够用就回收(比如高速缓存)
- 弱引用(WeakReference):无论内存是否充足,只要有 GC,一律回收
- 虚引用(PhantomReference):在任何时候都有可能被 GC 回收,必须配合引用队列使用,主要作用是跟踪对象被垃圾回收的状态,当一个对象已经进入 finalization 阶段可以被回收时,可以实现比 finalization 机制更灵活的回收操作
八、垃圾回收算法
1、基本算法:引用计数法
- 通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
- 回收时不移动对象, 所以会造成内存碎片问题,不能解决对象间的循环引用问题
2、基本算法:标记清除法
- 当堆中的有效内存空间被耗尽的时候,就会暂停、挂起整个程序(也被称为stop the world),然后从根对象开始遍历所有的对象,将所有存活的对象标记为可达的对象,再将没有标记的对象全部清除掉。
- 实现简单,但是效率低,会造成内存碎片问题,会造成应用程序暂停
3、基本算法:标记压缩法
- 与标记清除法相比,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象移动到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。
- 解决了碎片化的问题,效率更低
4、基本算法:复制算法
- 复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,并依次排列,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
- 解决了碎片化的问题,新生代效率较高,老年代效率较差,浪费了一半的内存空间
5、综合算法:分代算法
- 分代收集算法是将对象的生命周期按长短划分为两个部分
- 新生代适合使用复制算法
- 老年代适合使用标记清除或标记压缩算法
6、综合算法:分区算法
- 分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间。
- 在相同条件下,堆空间越大。一次GC耗时就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标停顿时间每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。
九、常用的 JVM 参数
|