【Java面试学习】JVM图解

目录

1、什么是JVM?

1.1 定义

1.2 比较JVM JRE JDK

1.3 JVM体系

2、JVM内存结构

2.1 程序计数器(PC寄存器)

2.2 栈

2.2.1 定义

2.2.2 相关问题

2.2.3 栈内存溢出

2.2.4线程运行诊断

2.3 本地方法栈(Native Method Stacks)

2.4 堆(Heap)

2.4.1 特点

2.4.2 堆内存溢出

2.4.3 堆内存诊断

2.4 方法区(Method Area)

2.4.1 组成

2.4.2 方法区内存溢出(1.8后元空间内存溢出)

2.4.3 运行时常量池

2.4.4 StringTable特性

2.4.5 StringTable性能调优

2.5 类加载器(ClassLoader)

2.5.1 什么是类加载器

2.5.2 双亲委派模型

3.垃圾回收

3.1 如何判断对象可以回收

3.1.1 引用计数法

3.1.2 可达性分析算法(Java虚拟机使用)

3.2 四种引用

3.3 垃圾回收算法

3.3.1 标记清除 Mark Sweep

3.3.2 标记整理 Mark Compact

3.3.3 复制 Copy

3.4 分代垃圾回收

3.5 垃圾回收器

3.5.1 串行

3.5.2 吞吐量优先

3.5.3 响应时间优先

3.5.4 G1(JDK9之后的默认垃圾回收器)


1、什么是JVM?

1.1 定义

Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境)

好处:

  • 一次编写、到处运行;
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查(不会覆盖其他资源的内存)
  • 多态

1.2 比较JVM JRE JDK

1.3 JVM体系

2、JVM内存结构

2.1 程序计数器(PC寄存器)

作用:记住下一条jvm指令的地址;(在物理上通过寄存器实现)

特点:

  • 线程私有(每个线程有自己的程序计数器)
  • 不会存在内存溢出

2.2 栈

2.2.1 定义

Java Virtual Machine Stack(Java虚拟机栈)

  • 虚拟机栈:每个线程运行需要的内存空间
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占的内润
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

2.2.2 相关问题

  • 垃圾回收是否涉及栈内存?

        不涉及,栈内存中的栈帧在方法执行完后会自动弹出栈。

  • 栈内存分配越大越好吗?

        不是。栈内存越大,会导致线程数减少。默认大小:1024KB

  • 方法内的局部变量是否线程安全

        如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。

        如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。

2.2.3 栈内存溢出

报错:java.lang.StackOverflowError

原因:

  • 栈帧过多导致栈内存溢出(例如不正确的递归调用、Json解析对象循环引用)
  • 栈帧过大导致栈内存溢出

2.2.4线程运行诊断

  • 案例一:cpu占用过多
    • 定位:top命令查看进程占用cpu情况
    • ps H -eo pid,tid,%cpu | grep 进程id (进一步定位哪个线程占用cpu过高)
    • jstack 进程id 定位问题代码源码行数
  • 案例二:程序运行很长时间没有结果(可能产生死锁)
    • jstack命令,Found one Java-level deadlock, 定位问题代码源码行数

2.3 本地方法栈(Native Method Stacks)

作用:给本地方法运行提供内存空间

2.4 堆(Heap)

2.4.1 特点

  • 通过new关键字,创建对象都会使用堆内存。
  • ‘它是线程共享的,堆中对象都需要考虑线程安全的问题。
  • 有垃圾回收机制。

2.4.2 堆内存溢出

报错:java.lang.OutOfMemoryError: Java heap space

原因:正在使用的对象或字符串太多。(jdk1.8后,字符串常量池在堆内存中)

2.4.3 堆内存诊断

工具:

  • jps工具:查看当前系统中有哪些java进程
  • jmap工具:查看堆内存占用情况,快照信息
  • jconsole工具(jdk自带):图形界面,多功能的监测工具,可以连续监测
  • jvisualvm工具:可以检查堆中最大的对象。

案例:垃圾回收后,堆内存占用依旧很大。

可以使用jvisualvm工具查看堆内存中的对象。

2.4 方法区(Method Area)

方法区是一种规范,具体物理空间有不同实现

2.4.1 组成

图中忽略其他内容,仅展示方法区和堆的位置

2.4.2 方法区内存溢出(1.8后元空间内存溢出)

报错:java.lang.OutOfMemoryError: Metaspace

原因:生成了太多的类。

场景:

  • spring cglib代理,运行期间动态生成类的字节码,运行期间可能生成大量的类
  • mybatis cglib代理

2.4.3 运行时常量池

二进制字节码中:类基本信息,常量池,类方法定义,包含了虚拟机指令

反编译HelloWorld.class文件后可以看到:

  • 常量池,就是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息。
  • 运行时常量池,常量池是.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池中,并把里面的符号地址变为真实地址。

2.4.4 StringTable特性

  • StringTable:字符串常量池,底层是HashTable结构,不能扩容
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    • 1.8将字符串对象尝试放入串池,如果有则不会放入,没有则放入串池
    • 1.6将字符串对象尝试放入串池,如果没有则会复制一份放入串池,返回串池中的对象。
  • 在内存紧张时,会触发垃圾回收

面试题:

2.4.5 StringTable性能调优

  • 可以适当修改虚拟机参数 -XX:StringTableSize (桶个数),原理是增大桶的容量,减小hash冲突,提升效率。
  • 考虑将字符串对象是否入池

2.5 类加载器(ClassLoader)

2.5.1 什么是类加载器

作用:JVM只能运行二进制文件,类加载器的作用是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

  • 启动类加载器(BootStrap ClassLoader):该类并不继承ClassLoader类,其是由C++编写实现。负责加载Java的核心库(如JAVA_HOME/jre/lib目录下的类库)。
  • 扩展类加载器(ExtClassLoader):该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
  • 应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
  • 自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。

2.5.2 双亲委派模型

原理:加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。

原因:

  • 可以避免某一个类被重复加载,当父类已经加载后无需重复加载,保证唯一性。
  • 为了安全,保证类库API不会被修改

2.5.3 类装载的执行过程

(1)加载:查找和导入class文件

  • 通过类的全名,获取类的二进制数据流。
  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

(2)验证:保证加载类的准确性

  • 格式检查:文件格式是否错误、语法是否错误、字节码是否合规
    • 文件格式验证
    • 元数据验证
    • 字节码验证
  • 符号引用验证:Class文件在其常量池中通过字符串记录自己将要使用的其它类或者方法,检查它们是否存在

(3)准备:为类变量分配内存并设置类变量初始值

  • static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
  • static变量是final基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
  • static变量是final引用类型,那么赋值也会在初始化阶段完成

(4)解析:把类中的符号引用转换为直接引用

  • 比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。

(5)初始化:对类的静态变量,静态代码块执行初始化操作

  • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
  • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

(6)使用:JVM 从入口方法开始执行用户的程序代码

  • 调用静态类成员信息(比如:静态字段、静态方法)
  • 使用new关键字为其创建对象实例

(7)卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象

  • 最后负责运行的 JVM 也退出内存

3.垃圾回收

3.1 如何判断对象可以回收

3.1.1 引用计数法

3.1.2 可达性分析算法(Java虚拟机使用)

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为GC Root对象虚拟机栈(栈帧的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI (Java Native Interface)引用的对象、活跃线程的引用等。

3.2 四种引用

  • 强引用: 只要有 GC Roots 能找到,就不会被回收
  • 软引用: 需要配合SoftReference来释放软引用自身,当垃圾多次回收,内存依然不够的时候会回收软引用对象
  • 弱引用: 需要配合WeakReference来释放弱引用自身,只要进行了垃圾回收,就会把弱引用对象回收
  • 虚引用: 必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

3.3 垃圾回收算法

3.3.1 标记清除 Mark Sweep

  • 优点:速度快
  • 缺点:容易产生内存碎片

3.3.2 标记整理 Mark Compact

  • 优点:不会产生内存碎片
  • 缺点:内存拷贝移动,效率低

3.3.3 复制 Copy

标记

复制到To区

删除后交换From和To区

  • 优点:不会产生内存碎片
  • 缺点:双倍内存空间。

3.4 分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发Minor GC,伊甸园和from存货的对象使用copy复制到to中,存货的对象年龄加1,并且交换from to
  • minor gc 会引发stop the world,暂停其他用户的线程,等垃圾回收结束后(时间很短),用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 如果新生代老年代空间都不足,首先触发minor gc,如果仍旧空间不足,那么触发Full GC,(STW的时间更长)。

3.5 垃圾回收器

3.5.1 串行

  • 单线程
  • 堆内存较小,适合个人电脑

3.5.2 吞吐量优先

  • 多线程
  • 堆内存较大,多核cpu
  • 让单位时间内,SWT的时间最短( 一小时触发两次:0.2+0.2=0.4)

  • -XX:+UseAdpativeSizePolicy 采用自适应大小调整策略,新生代中比例、晋升阈值、整个堆;
  • -XX:GCTimeRatio=ratio 默认值19
  • -XX:MaxGCPauseMillis=ms 默认值200ms
  • -XX:ParallelGCThreads=n 线程数

3.5.3 响应时间优先

  • 多线程
  • 堆内存较大,多核cpu
  • 尽可能让单次的STW时间变短(一小时触发5次,但单次时间短 0.1+0.1+0.1+0.1+0.1=0.5)

3.5.4 G1(JDK9之后的默认垃圾回收器)

  • 同时注重吞吐量和低延迟
  • 超大堆内存,会将堆划分为多个大小相等的Region,每个区域都可以充当伊甸园、幸存区、老年代。
  • 整体上是标记+整理算法,两个区域之间是复制算法
  • 分为三个阶段:新生代回收、并发标记、混合收集;三阶段循环

阶段一:

阶段二:

阶段三:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值