面试题
对象
对象的创建
【创建方式】:
1. new 关键字
2. 通过反射机制,使用 Constructor 类的 newInstance
【创建过程】:
1. 在常量池查找类的符号引用,确定类是已经被加载,否则执行类加载过程。
2. 为对象分配内存并赋初始值 。
3. 设置对象头信息。
4. 对象初始化。
分配内存
【确定内存大小】对象需要分配的内存在类加载年阶段就已经确定
1. 对象头
2. 实例数据
3. 对齐填充
【分配内存】
指针碰撞(用于内存规整的情况):只需将内存指针移动相应大小的位置即可。
空闲列表(用于内存非规整的情况):记录内存的使用情况
【线程安全】
CAS+失败重试
本地线程分配缓冲(TLAB):为每个线程分配一块私有的内存。
对象头
三部分组成:Mark Word、指向类元对象的指针、数组长度(对象为数组时存在)
【Mark Word】
1. 锁信息
2. GC 分代年龄
3. 线程ID
4. HashCode
内存溢出
内存溢出与内存泄漏
- 内存溢出:系统无法再分配内存空间。
- 内存泄漏:分配的内存未释放持续占用,会导致内存溢出。
JVM 哪些区域会内存溢出
1. 堆 java.lang.OutOfMemoryError: Java heap space
2. 元数据区 java.lang.OutOfMemoryError: Metaspace
3. 直接内存 java.lang.OutOfMemoryError: Direct buffer memory
内存泄漏排查
1. 生成dump文件。
1. jmap 命令主动 dump。 jmap -dump:live,format=b,file=/data/program/micro/dump.hprof 1343
2. jvm 参数 OOM 时自动 dump。 -XX:+HeapDumpOnOutOfMemoryError
2. Eclipse MAT 工具分析 dump 文件
1. Overview 显示内存占用情况
2. Dorminator Tree(支配树) 显示对象与其持有的所有对象的内存情况
1. shallow heap:指的是某一个对象所占内存大小
2. retained heap:指的是一个对象的 retained se t所包含对象所占内存的总大小
3. retained set: 指的是这个对象本身和他持有引用的对象和这些对象的 retained set所占内存大小的总和
3. List Objects 罗列出对象在引用树中的节点,可以顺着节点查看具体是哪些类出现泄漏
4. OQL 对象查询语句
1. select * from com.example.MyClass
2. select * from instanceof java.lang.String
GC收集
GC Roots
可达性分析算法:以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
- 虚拟机栈/本地方法栈中引用的对象
- 静态变量、常量对象
元数据区的垃圾回收条件
- 该类所有的实例都已经被回收
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 Class 对象没有在任何地方被引用
引用类型
- 强引用:不会被回收
- 软引用:内存不足时,会被回收
- 弱引用:下一次 GC 会被回收
- 虚引用:不对对象的 GC 构成影响
垃圾回收算法
【标记-清除】
1. 标记存活的对象,清除无标记对象
2. 会产生内存碎片,在无法分配内存时触发 FullGC
【标记-整理】
1. 标记存活对象,清除无标记对象
2. 移动存活对象进行内存整理
3. 不会产生内存碎片
【复制】
1. 将内存划分为 2 块区域,每次只使用 1 块区域
2. 将存活的对象复制到另 1 块区域,清除当前区域
3. 内存空间利用率低,不会产生内存碎片
【HotSpot】
1. 内存划分为 1 个 Eden 和 2 个 Survivor 8:1:1,即内存利用率 90%
2. 优先使用 1 个 Eden 和 1 个 Survivor,将存活对象复制到另 1 个 Survivor
3. 依赖于老年代进行空间分配担保,担保失败触发 FullGC
垃圾收集器
内存分配策略
内存分配策略
- 对象优先在 Eden 分配。 Eden 空间不够时,发起 Minor GC
- 大对象直接进入老年代。
- 长期存活的对象进入老年代。-XX:MaxTenuringThreshold 设置年龄阈值。
- 动态对象年龄判定。 某年龄对象数超过 Survivor 空间的一半,则所有>=该年龄对象进入老年代。
- 空间分配担保。担保失败触发 Full GC。
分代GC
- 【MinorGC/YoungGC】对新生代 GC
- 【OldGC】对老年代 GC
- 【FullGC】对所有 GC
FullGC 触发条件
- 调用 System.gc() 建议 JVM FullGC
- 老年代空间不足
- 空间分配担保失败
GC 日志
[ GC [ PSYoungGen: 1351K -> 288K (18432K) ] 1351K -> 288K (59392K), 0.0012389 secs ] [ Times: user=0.00 sys=0.00, real=0.00 secs ]
[ Full GC (System) [ PSYoungGen: 288K -> 0K (18432K) ] [ PSOldGen: 0K -> 160K (40960K) ] 288K -> 160K (59392K) [ PSPermGen: 2942K -> 2942K (30720K) ], 0.0057649 secs ] [ Times: user=0.00 sys=0.00, real=0.01 secs ]
Heap
PSYoungGen
total 18432K, used 327K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
eden space 16384K, 2% used [0x00000000fec00000,0x00000000fec51f58,0x00000000ffc00000)
from space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
to space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
PSOldGen
total 40960K, used 1184K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
PSPermGen
total 30720K, used 2959K [0x00000000fa600000, 0x00000000fc400000, 0x00000000fc400000)
object space 30720K, 9% used [0x00000000fa600000,0x00000000fa8e3ce0,0x00000000fc400000)
-
新生代
- PSYoungGen : Parallel Scanvenge
- ParNew : ParNew
- defNew : Serial
-
老年代
- ParOldGen : Parallel Old
- PSOldGen : Serial Old
虚拟机类加载机制
类加载包括以下 5 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
JVM 加载 Class 文件的过程
1. 【加载】读入二进制 .class 文件字节流
2. 【验证】对二进制验证完整性与 JVM 兼容性
3. 【准备】为类变量分配内存并赋初值 0
4. 【解析】符号引用替换为直接引用
5. 【初始化】执行类初始化语句
类加载时机
1. new、getstatic、putstatic、invokestatic 这四条字节码指令时,即 new 对象、查询/设置类静态字段、执行静态方法
2. 对类进行反射调用时
3. 加载一个类的时候,如果发现其父类还没有加载时,先对父类进行加载。
4. jvm 启动时,对 main 方法所在类加载
*特殊场景*
1. 通过子类引用父类的静态字段,不会导致子类初始化
System.out.println(SubClass.value); // value 字段在 父类中定义 中定义
2. 通过数组定义来引用类,不会触发此类的初始化
SuperClass[] array = new SuperClass[10]; //该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
3. 常量引用
System.out.println(ConstClass.HELLOWORLD); // 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载器分类
1. 启动类加载器(Bootstrap ClassLoader)
启动类加载器无法被 Java 程序直接引用,是虚拟机自身的一部分。负责将存放在 <JRE_HOME>\lib 或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中
2. 扩展类加载器(Extension ClassLoader)
负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中。
3. 应用程序类加载器(Application ClassLoader)
ClassLoader.getSystemClassLoader() 获取,负责加载用户类路径(ClassPath)上所指定的类库
双亲委派模型
该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。
这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
1. 一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
2. 通过双亲委派模型加载自定义 java.lang.String 类时,由于顶层为启动类加载器,因此会加载到 <JRE_HOME>\lib 中的 java.lang.String ,而不是自定义的 java.lang.String 类。
3. 自定义类加载器时,父类 loadClass() 实现了双亲委派,子类只需重写 Class<?> findClass(String name); (通过权限定类名 获取 Class 对象) 即可,通过 defineClass 方法 将 byte 数组转为 Class 对象。
// 自定义类加载器
public class FileSystemClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 通过全权限定类名读取 byte[]
byte[] classData = getClassData(name);
// defineClass 方法将 byte[] 转为 Class<?> 对象
return defineClass(name, classData, 0, classData.length);
}
// 通过全权限定类名读取 byte[]
private byte[] getClassData(String className) {return ...;}
// 返回文件位置
private String classNameToPath(String className) {return ....;}
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
}
JVM调优
JVM 调优的工具
【jps】输出 JVM 进程信息
jps -l 输出 main 方法所在类全限定类名
jps -m 输出 main 方法参数
jps -v 输出 JVM 参数
【jstack】输出线程堆栈信息
jstack -l [pid] 堆栈+锁信息
【jmap】 输出堆信息
jmap -dump:live,format=b,file=${filePath}.hprof [pid]
【jstat】输出 JVM 类加载、内存、垃圾收集等运行数据统计信息
jstat -class [pid] // 类加载统计
jstat -compiler [pid] // 编译统计
jstat -gc [pid] // GC 统计
【JVisualVM】可视化工具,提供了对内存、线程、类加载、GC等信息的可视化数据展示
【JConsole】可视化工具,提供了对内存、线程、类加载等信息的可视化数据展示
排查 CPU 100%
- 定位到 CPU 占用率最高的线程
1. top -o %CPU 按CPU使用率排序查看进程信息
2. top -Hp [pid] 查看进程下所有线程信息,通过 TIME+ 可以确定 CPU 占用最多的线程
3. printf "%x" [线程id] 获得16进制线程id
4. jstack [JVM进程id] | grep [16进制线程id]
5. 通过堆栈信息定位代码
排查内存溢出
- 定位到内存泄漏点
1. 获取 dump 文件 。 jmap -dump:live,format=b,file=${filePath}.hprof
2. 通过 Eclipse MAT 工具载入 dump 文件。
3. 查看 overview、支配树、list objects 查看引用树、OQL语句查询某些类的对象情况。
4. 找到内存泄漏点,通过堆栈信息定位代码。
排查程序无响应
- 可能的原因:线程被阻塞、线程死锁
【线程阻塞】
1. jstack -l [pid] | grep -E 'BLOCKED|TIMED_WAITING'
// block
state = BLOCKED
// sleep(time)
java.lang.Thread.State: TIMED_WAITING (sleeping)
// parkUntil(deadline)
java.lang.Thread.State: TIMED_WAITING (parking)
// wait(time)
java.lang.Thread.State: TIMED_WAITING (on object monitor)
2. 观察是否线程均被 BLOCKED(阻塞) 或 TIMED_WAITING(期限等待) 消耗导致无法执行任务
【线程死锁】
1. jstack -l [pid] | grep 'waiting on condition'
"VM Periodic Task Thread" os_prio=0 tid=0x00007f602c01bc80 nid=0x44f waiting on condition