面试
JVM
Java Virtual Machine Java二进制字节码的运行环境
好处:一次编写 到处运行和自动内存管理 垃圾回收机制

JVM组成
程序计数器
程序计数器(PC Register)
什么是程序计数器?
线程私有的 内部保存的字节码的行号 用来记录正在执行的字节码的地址
JAVA堆
Java堆是一个线程共享的区域 用来保存对象实例,数组等 当堆中没有内存空间可分配时 就会抛出OutOfMemoryError异常 即OOM

Java8的元空间就是方法区 在Java7中 方法区是保存在堆中的 叫做永久代 但是保存在堆中如果过小OOM 过大浪费 所以为了避免OOM 直接在java8中放到了内存中
你能介绍一下Java堆吗
- 就是线程共享的区域 主要用来保存对象实例 数组等 内存不够就抛出OOM异常 OutOfMemoryError
- 组成:老年代 + 年轻代
- 年轻代被划分为两部分 Eden区存放刚创建的对象数组 和两个幸存者Survivor区
- 老年代主要保存生命周期长的对象 一般是一些老的对象
- JDK1.7和JDK1.8的区别
- 1.7中堆中有个永久代 存储的是类信息 静态变量 常量 编译后的代码
- 1.8移除了永久代 把这块空间存储到了本地内存的元空间中 防止内存溢出OOM
虚拟机栈
Java Virtual Machine Stacks(java虚拟机栈)
- 每个线程运行时需要的内存 就是虚拟机栈 先进后出
- 每个栈由多个栈帧(frame)组成 对应每次发昂发调用时所占用的内存 包括参数,局部变量,返回地址
- 每个线程只能有一个活动栈 就是当前正在执行的方法
垃圾回收是否设计到栈内存?
不涉及 垃圾回收是指堆内存 栈内存回收是把栈帧弹出之后就会释放
栈内存分配越大越好吗?
不是 栈内存默认1024K 如果分配大 就会导致线程数变少
方法内的局部变量是否线程安全
如果方法内部的局部变量没有逃离方法的作用范围 那就是线程安全的
如果方法内部的局部变量引用了对象 并逃出了作用范围 比如返回值 传参 那就不是线程安全的
栈内存溢出情况
栈帧过多导致溢出 递归
栈帧过大导致溢出
堆和栈的区别是什么
- 栈内存一般存储的是局部变量和方法调用 堆内存是存储java对象和数组 堆会GC垃圾回收 栈不会
- 栈内存是线程私有的 堆是线程共享的
- 两者溢出的异常不同
- 栈内存不足是StackOverFlowError 栈溢出
- 堆内存不足是OutOfMemoryError 内存不足
方法区
解释一下方法区?
- 方法区(Method Area) 是线程共享的内存空间
- 主要存储类的信息以及运行时常量池
- 虚拟机启动的时候创建 关闭的时候释放
- 如果方法区内存无法满足所需大小 就会抛OutOfMemoryError:MetaSpace异常
常量池
类似于一张表 虚拟机指令根据这张常量表可以找到要执行的类名 方法名 参数类型 字面量信息
运行时常量池
常量池是.class文件中的 当该类被加载 它的常量池信息就会放入运行时常量池 并把里面的符号地址变为真实地址

直接内存
直接内存:不属于JVM的内存结构 不由JVM管理 是虚拟机的系统内存 常见于NIO操作 用于数据缓冲区 吞吐量大 分配回收成本较高 读写性能好
常规IO操作

常规IO有两个缓冲区 系统缓冲区和Java缓冲区 因为java无法直接操作系统缓冲区 所以需要从系统缓冲区复制一份给java缓冲区 造成了不必要的复制 性能不好
NIO操作

直接内存的存在就不用去管数据的多份复制 java代码和系统都可以访问 减少复制次数 提高性能
你听过直接内存吗
并不属于JVM中的内存结构 不由JVM管理 是虚拟机的系统内存
常见于NIO操作 用于数据缓冲区 分配回收成本较高 但是读写性能好 不受JVM内存回收管理
类加载器
类加载器与双亲委派
什么是类加载器
JVM只会运行二进制文件 类加载器就是将字节码文件加载到JVM中 从而让java程序能够启动起来
类加载器有哪些
顺序是自上而下的
- 启动类加载器(BootStrapClassLoader) 加载JAVA_HOME/jre/lib下的jar包
- 扩展类加载器(ExtClassLoader)加载JAVA_HOME/jre/lib/ext下的jar包
- 启动类加载器(AppClassLoader)加载classPath下的类
- 自定义类加载器(CustomizeClassLoader)自定义加载规则
什么是双亲委派模型
加载一个类 先委托上一级的加载器进行加载 如果上级加载器也有上级 就继续向上委托 如果该类委托上级没有被加载 子加载器尝试加载该类 如果可以加载 就向下派发
为什么JVM采用双亲委派模型
- 通过双亲委派机制可以避免某一个类重复被加载 当父类已经加载后则无需重复加载 保证唯一性
- 为了安全 保证类库API不会被修改
类装载
类从加载到虚拟机开始 直到卸载 生命周期包括了:加载 验证 准备 解析 初始化 使用 和卸载 验证准备解析三部分统称为连接(linking)

加载
通过类的全名 获取类的二进制数据流
解析二进制数据流作为方法区的数据结构
创建类实例 表示该类型 作为方法区这个类的各种数据的访问入口

验证
验证类是否符合JVM规范 安全性检查
文件格式验证 元数据验证 字节码验证都是格式检查 检查格式是否错误 语法是否错误 字节发是否合规
符号引用验证:Class文件在常量池中会通过字符串来记录自己将要使用的其他类或者方法 检查他们是否存在
准备
static变量 只会分配空间 设置默认值 赋值在初始化阶段完成
static final修饰的基本类型或字符串变量 会分配空间 并且赋值
static final的引用类型 分配空间 赋值在初始化阶段完成
解析
把符号引用转换为直接引用
初始化
对类的静态变量 静态代码块进行初始化操作
如果初始化一个类 其父类没被初始化 那就优先初始化其父类
如果包含多个静态变量和静态代码块 则自上而下顺序依次执行
使用
JVM从入口方法开始执行用户的代码
说一下类加载的执行过程
- 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量 静态代码块执行初始化操作
- 使用:JVM从入口方法开始执行用户代码
- 销毁:用户代码执行完毕后 jVM开始销毁创建的Class对象
垃圾回收
什么时候垃圾器回收
如果一个或者多个对象没有任何的引用指向它了 就是垃圾 如果定位了垃圾 就会被垃圾器回收 定位垃圾的方法有两种 引用计数器和可达性分析算法
引用计数法
一个对象被引用了一次 就会在对象头上递增一次引用次数 如果引用次数为0 就代表这个对象可回收
但是当出现了循环引用的时候 引用计数法就会失效 引发内存泄漏

可达性分析算法
现在虚拟机都是通过可达性分析算法来确定哪些是垃圾
通过根节点GC Roots开始扫描堆中的对象 以GC Roots为起点的对象都是正常对象 如果以GC Roots为起点扫描不到 那就是垃圾 代表可以回收
哪些对象可以作为GC Root
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
垃圾回收算法
分为标记清除算法 复制算法 标记整理算法
标记清除算法
分为两个阶段 标记和清除
1.根据可达性算法把的出的垃圾进行标记
2.对这些标记为可回收的内容进行回收

优点:标记和清除速度较快
缺点:内存不连续 碎片化严重
标记整理算法

跟标记清除差不多 就是多了一步整理空间的流程 没有碎片化的同时导致了性能下降
复制算法

优点:在垃圾较多的时候效率较高 清理后内存无碎片
缺点:内存使用率低 因为有两片内存空间只使用了一半
分代回收
说一下分代回收
-
堆的区域划分
在java8中 堆分成了新生代和老年代 新生代占了1/3 老年代占了2/3 新生代中又分了三部分 Eden区中存放的都是刚创建的对象 幸存者区分为from和to 比例是8:1:1
![]()
-
分代回收策略
- 新创建的对象 都会到Eden区
- Eden区内存不足 就会用可达性分析算法标记Eden和From存活的对象
- 将存货对象采用复制算法复制到to中 复制完毕后释放Eden区和from区
- 一段时间后Eden区又不足 继续标记Eden区和to区的存活对象 复制到from区
- 当幸存者区对象经过多次回收(最多15次) 晋升到老年代 如果幸存者区内存不足或者对象过大也会提前晋升
MinorGC MixedGC FullGC区别是什么
- MinorGC发生在新生代的垃圾回收 暂停时间短(STW)
- MixedGC 新生代和老年代部分区域的垃圾回收 G1收集器特有的
- FullGC 新生代+老年代完整垃圾回收 暂停时间长(STW) 应尽力避免
STW:StopTheWord 暂停所有应用程序线程 等待垃圾回收完成
垃圾回收器
垃圾回收器包括串行垃圾回收器 并行垃圾回收器 CMS(并发)垃圾回收器 G1垃圾回收器
串行垃圾回收器
Serial和Serial Old串行垃圾回收器 单线程进行垃圾回收 堆内存较小 适合个人电脑
- Serial作用新生代 采用复制算法
- Serial Old作用老年代 采用标记整理算法
垃圾回收时 只有一个线程在工作 并且Java中所有线程都要暂停(STW) 等待垃圾回收完成

并行垃圾回收器
Parallel和Parallel Old是一个并行垃圾回收器 JDK8默认采用此垃圾回收器
- Parallel作用新生代 采用复制算法
- Parallel Old作用老年代 采用标记整理算法
垃圾回收时 多个线程共同进行垃圾回收 性能要好 但是还是会暂停所有线程 等待垃圾回收线程完成

CMS并发垃圾回收器
全程Concurrent Mark Sweep 是并发的使用标记清除算法的垃圾回收器 该回收器针对老年代垃圾回收 停顿时间短 最大特点就是垃圾回收时 不影响其他线程
<img src="https://img2024.cnblogs.com/blog/3633503/202509/3633503-20250914170807753-297961930.png)
初始标记只标记GC Root的引用 只标记一代 并发标记才开始彻底标记 然后重新标记是为了防止运行时会有新增或删除引用 虽然也有暂停线程 但是时间短
G1垃圾回收器
谈一下G1垃圾回收器
- 应用于新生代和老年代 JDK9之后默认使用G1
- 划分成多个区域 每个区域都可以充当eden survivor old humongous 其中humongous专门为大对象准备
- 采用复制算法
- 响应时间和吞吐量兼顾
- 分为新生代回收 并发标记 混合收集
https://www.bilibili.com/video/BV1yT411H7YK?t=180.8&p=128 - 如果并发失败(回收速度赶不上创建对象速度)就会出发FullGC
强引用 软引用 弱引用 虚引用
- 强引用:new对象 只有所有的GC Roots不通过强引用引用该对象 才会被垃圾回收器回收
- 软引用:配合SoftReference使用 仅有软引用引用该对象时 第一次垃圾回收不会回收该对象 如果第一次之后内存仍不足 就会回收该软引用对象
- 弱引用:配合WeakReference仅有弱引用引用该对象时 无论内存是否充足 都会回收弱引用对象
- 虚引用:配合引用队列使用 被引用对象回收时 会将虚引用入队 由Reference Handler线程调用虚引用相关方法释放直接内存
JVM实践
JVM在哪调优
-
war包部署在tomcat中设置
修改TOMCAT_HOME/bin/catalina.sh文件
![]()
-
jar包部署设置
![]()
JVM调优参数
JVM调优 主要是更改年轻代 老年代 元空间内存大小和垃圾回收器类型
- 设置堆空间大小
-Xms:设置堆初始化大小 -Xmx:设置堆最大大小 最大大小默认是物理内存1/4 初始大小是1/16
堆太小 导致年轻代和老年代频繁回收 产生STW 暂停用户线程
堆太大 如果发生FullGC会扫描整个堆空间 暂停用户线程过长 - 虚拟机栈的设置
每个线程会默认开启1M的内存 用于存放栈帧 调用参数 局部变量等 一般256K 通常减少每个线程的堆栈 可以产生更多的线程
-Xss128k - Eden区和两个Survivor区大小比例
- 年轻代晋升老年代阈值
- 设置垃圾回收收集器
jdk8默认使用并发垃圾回收器 可以通过设置参数改成使用G1垃圾回收器
-XX:+UseG1 GC
JVM调优工具
- 命令工具
- jps 进程状态信息
- jstack 查看java进程内线程的堆栈信息
- jmap 查看堆转信息
- jhat 堆转储快照分析工具
- jstat JVM统计监测工具
- 可视化工具
- jconsole 对JVM内存 线程 类的监控
- VisualVM 监控线程 内存情况
内存泄漏
一般来说 都是堆的内存泄露问的多
解决思路:
-
获取堆内存快照dump
使用jmap命令拿到运行中程序的dump文件 如果文件没启动或者已经退出 jmap就不合适 因为jmap只适合运行中的 就可以通过配置VM参数 设置发生OOM时生成dump文件 从而可以进一步分析堆中的情况 -
使用VisualVM分析dump文件
-
通过查看堆信息的情况 定位内存溢出问题
CPU飙高
CPU飙高的排查与思路
使用top命令查看占用CPU的情况 发现哪一个进程占用CPU较高
通过ps命令查看进程中的线程信息
使用jstack命令查看进程中哪些线程出现了问题 最终定位问题
企业场景
设计模式
工厂设计模式
工厂模式最大的优点就是解耦 不需要跟具体的类打交道 只需要把想要的类交给工厂来创建
简单工厂
就是通过一个工厂来创建 虽然解耦 但是耦合还是存在
工厂方法模式
创建一个工厂接口规定规则 然后通过工厂实现类来明确所要创建的类 彻底解耦

优点:
用户只知道具体工厂名称即可创建所要的类 无需知道类的创建过程
无需对原工厂进行任何修改 满足开闭原则
缺点:
每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类 增加了系统的复杂度
抽象工厂模式
就是遇到华为和小米的这种不同品牌 且都有手机电脑的来看 一个工厂方法是不够的 抽象工厂就是再加一层 先分品牌 再分手机还是电脑 所以就有了抽象工厂模式

优点:保证客户端只使用同一个产品组的对象
缺点:需要新增产品时 所有工厂类都要修改
策略模式
优点:策略类可以自由切换 易于扩展
缺点:客户端必须知道所有的客户类 策略模式会产生很多的策略类
什么是策略模式
策略模式定义了一系列算法 并将眉哥哥算法封装起来 使他们可以相互替换 且算法的变化不会影响使用算法的客户
一个系统需要动态的在几种算法中选择一种时 可以将每个算法封装到策略类中
案例(工厂方法 + 策略)
介绍业务(满减 满300九折 500八折 1000七折)
提供多种策略 都让Spring容器管理
提供一个工厂 准备策略对象 根据参数提供对象
大致思路就是在yml文件中配置参数 yaml中key是前端传的不同的策略 值是策略的对象名
所有策略的对象是交给了spring容器管理 然后在工厂方法中定义map 通过实现Aware接口(ApplicationContextAware)在容器中获取策略对象 然后放到map中 工厂就可以根据策略类型 得到策略对象 然后在service中就可以注入工厂方法 实现开闭自由 解耦合
责任链设计模式
优点:
降低耦合 增强可扩展性 责任分担
缺点:
责任链较长 要涉及多个对象 性能较低
增加了客户端的复杂性

常见技术场景
单点登录
单点登录 Single Sign On(SSO) 只登陆一次 就可以访问所有信任的应用系统
使用JWT
用户发起登录请求 返回给前端一个token 前端把token存到请求头中
用户访问其他服务 就携带了token 由网关进行验证 无效就返回401(认证失败) 跳到登陆页面
校验成功 再由网关路由到其他服务
传递给下游?OpenFeign?
权限认证?
后台管理系统更注重权限控制 最常见的就是RBAC模型
(Role-Based Access Control)
具体实现
五张表(用户表 角色表 权限表 用户角色中间表 角色权限中间表)用户角色多对多 所以需要一张中间表 角色权限多对多 需要一张中间表
七张表(用户表 角色表 权限表 菜单表 用户角色中间表 角色权限中间表 权限菜单中间表)
权限认证如何实现
后台管理的相关经验
RBAC的五张表(用户表 角色表 权限表)
SpringSecurity
数据安全性
对称加密:
文件解密和加密使用同一把密钥
优点:加密速度快 效率高
缺点:不安全
非对称加密
公开密钥加密 私有密钥解密
优点:安全性更高
缺点:加密解密速度慢
上传数据的安全性如何控制
使用非对称加密 给前端一个公钥 把数据加密后传到后端 后端解密后处理数据
文件大用对称加密 不要存敏感信息
文件小 要求安全性 就非对称加密
日志采集
采集日志是为了定位问题
方式有哪些
常规采集:按天保存一个日志文件
ELK:ElasticSearch Logstash Kibana
项目中日志是这么采集的?
搭建了ELK日志采集系统
ElasticSearch:全文搜索数据引擎 可以对数据进行存储 分析 搜索
Logstash:数据收集引擎 可以动态收集数据 可以对数据进行过滤分析 主要是收集日志
Kibana:数据可视化分析平台 来对ES的数据进行分析 查询 图表化展示
查看日志的命令有哪些
Linux:
实时监控日志的变化:tail -f xx.log
按照行号查询:tail -n 100 xx.log 尾部 head -n 100 xx.log 头部
查询日志中含debug:cat -n xx.log | grep 'debug' 出来的是行号 结合上面
按照日期查询:
日志太多 处理方式:

生产问题排查
先分析日志 查看系统日志或日志文件 定位问题
运用远程debug
快速定位系统瓶颈
压测(性能测试)
监控工具 链路追踪工具
线上诊断工具 Arthas(阿尔萨斯)




浙公网安备 33010602011771号