0. 小点:
1. 类成员变量的存储位置:
- 静态的变量和常量,保存在方法区上 -> 方法区永久。
- 普通成员变量作为该对象的一部分, 保存在堆上。
- 成员变量的对象引用作为该对象的一部分, 保存在堆上。
2. 类方法中定义的变量存储位置:
- 基础数据类型,保存在虚拟机栈上。
- 局部变量的对象引用,保存在虚拟机栈上。
- 对象实例以及实例中非静态变量、非常量,保存在堆上。
3. Java的内存溢出与内存泄漏:
- 内存溢出:申请内存时,JVM没有足够的内存空间。主要发生在堆的老年代,方法区和栈也会发生。
- 内存泄漏:申请了内存,但是没有释放,导致内存空间浪费。-> 指对象可达,但是没用了 -> Java自行处理GC,理论上不会发生,但是对象不可达,也会发生,例如:ThreadLocal。
4. 常见的容易导致内存泄漏的点:
- 1. 线程池创建未显示指定阻塞队列大小。
- 2. ThreadLocal 的管理中忘记回收对象。
- 3. 所有涉及资源链接的地方,都不要忘记关闭资源。
- 4. 类的成员变量为集合,或者单例的模式中有集合,引用了大量的其他对象。
- 5. java方法的传值引用,造成的小时间段内,内存没按照预想回收掉。
1. 描述一下jvm内存模型,以及这些空间的存放的内容 ?
按下图说出JVM内存的整体轮廓,然后再细说。详细见《JVM》

JVM Stack:
线程私有(是线程的生命周期);保存基础数据类型和对象引用;栈帧(是方法的生命周期),栈帧的四部分(局部变量表、操作数栈、动态连接、返回地址)。
局部变量表: 等价于 JVM寄存器(PC计数器)也等价于 CPU的储存单元。
操作数栈: 模拟计算机ALU单元(算术逻辑单元),不进行变量存储。
动态连接(常量池指针):用来获取元数据。
Native Method Stack:
线程私有、JVM不能直接调用本地方法(C、C++),所以使用它。
PC计数器:
线程私有;指向下一条需要执行的字节码指令的地址(给执行引擎用);占用较小的内存空间,并且不会OOM。
堆:
线程共有、保存创建的对象、分代、gc回收的区域。

方法区:
线程共有,存储已被JVM加载的类信息、常量、静态变量和编译器编译后的代码等数据。JDK1.7及其以下方法区的实现是永久代,JDK1.8及其以上实现是元空间(Mate Space)。
常量池:
编译期生成的各种字面量和符号引用、运行期间也可能将新常量放入常量池中;常量池在方法区中。
2. 描述一下class初始化过程java类加载和初始化的过程?
过程:(@&@)

加载:通过类全限定名加载二进制字节流,将字节流转化结构存储到方法区。
验证:验证载入.class文件数据的正确性:文件格式、元数据、字节码、符号引用。
准备:为静态变量分配存储空间和初始化零值。
解析:将常量池内符号引用替换为直接引用。
初始化:执行类中定义的Java程序代码。
不会触发初始化的三种情况:**
- 通过子类引用父类的静态字段,不会导致子类初始化;
- 通过数组定义来引用类,不会触发此类的初始化;
- 常量在编译阶段会存入调用类的常量池中,并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
3. 简述一下Java类加载模型?

重要概念:
- 父加载器不是"类加载器的加载器",即,加载器之间不是继承关系,是委派关系。父加载器是加载器中一个属性。
- 双亲委派是一个孩子向父亲方向,然后父亲向孩子方向的双亲委派过程。
- 不存在BootStrap类加载器,源码中是null
双亲委派机制下加载类的过程:
类加载首先查看自己缓存 --> 没有,找父类加载器,还没有,就一直往上找 --> 没有,顶级父类加载器自己加载 --> 没有,顶级的下一级加载,直到自己去加载 --> 没有,抛异常
双亲委派模型的好处:
避免用户自己编写的类动态替换 Java 的一些核心类,更加安全。防止二次加载浪费资源。
4. 堆内存划分的空间,如何回收这些内存对象,有哪些回收算法?
堆内存空间划分如下图:

如何回收这些内存对象:**
- Eden区分配满的时,会触发Young GC,有部分存活对象会晋升到老年代;
- 老年代没有足够的空间时,发生Full GC;
- 准备要触发Young GC时,统计要晋升老年代的对象大于目前老年代剩余的空间时,则不会触发Young GC,而是触发Full GC(除了CMS收集器之外,Full GC至少伴随一次Young GC);
- System.gc()默认触发Full GC;
- heap dump;
- CMS GC时出现Concurrent Mode Failure会导致一次Full GC的产生;
垃圾回收算法:
标记清除、复制(多为新生代垃圾回收使用)、标记整理。
对象的分配过程:(@&@)
- 栈上分配:
- 线程私有小对象、无逃逸、执行标量替换、不需要调整的。
- 线程本地分配TLAB(Thread Local Allocation Buffer):
- 占用eden,默认1%。
- 多线程的时候不用竞争eden就可以申请空间,可以避免多线程冲突,提高效率。
- 小对象、不需要调整的。

5. JVM8为什么要增加元空间,带来什么好处?
原因,永久代容易内存溢出,并且GC复杂:
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。
- 永久代GC带来不必要的复杂度,并且回收效率偏低。
元空间的特点:
- 每个加载器有专门的存储空间。
- 不会单独回收某个类。
- 元空间里的对象的位置是固定的。
- 如果发现某个加载器不再工作了,会把相关的空间整个回收。
6. 简述一下内存溢出的原因,如何排查线上问题?
查看报错信息,根据具体的报错信息去解决问题。
内存溢出可能的原因:
java.lang.OutOfMemoryError: ......java heap space..... :
堆栈溢出,代码问题的可能性极大。
java.lang.OutOfMemoryError: GC over head limit exceeded:
系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前,从而不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC。
java.lang.OutOfMemoryError: PermGen space:
jdk1.7之前才会出现的问题 ,系统的代码非常多等原因导致常量池的膨胀。
java.lang.OutOfMemoryError: Direct buffer memory:
直接内存不足,因为jvm垃圾回收不会回收掉直接内存这部分的内存。比如Netty中使用了ByteBuffer.allocateDirect() 时,没有做clear()。
java.lang.StackOverflowError:
-Xss设置的太小。
java.lang.OutOfMemoryError: unable to create new native thread :
堆外内存不足,无法为线程分配内存区域。
java.lang.OutOfMemoryError: request {} byte for {}out of swap :
地址空间不够。
内存溢出一般发生在堆的老年代中。
7. 怎么提前避免内存泄漏?
- 概念:长生命周期的对象引用短生命周期的对象,没有将无用对象置为null。
- 理论上,JVM自己维护GC不会发生内存泄漏。
- 聊一下ThreadLocal的问题,每次使用完成调用remove()。
- 或者以下例子:

8. 如何解决线上GC频繁的问题?***
思路:***
- 原因:内存不够用,触发频繁GC。
- 查找主线:查看JVM内存相关配置,然后通过Jmap命令或dump文件找大对象,定位到问题代码。
- 技巧:排查出现问题时间点之前,是否有上线、组件更新。先排查元空间打满、内存泄漏等问题。
具体:
- 1. 查看监控,了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)。
jstat -gc 500 // 作用:观察GC相关参数
- 2. 了解该时间点之前有没有程序上线、基础组件升级等情况。
- 3. 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后,分析JVM参数设置是否合理。
-XX:+PrintVMOptions // 作用:打印虚拟机接受到的命令行显示参数。 -XX:+PrintFlagsFinal // 作用:打印所有的系统参数的值
- 4. 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
- 5. 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo命令,并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
jmap -histo // 作用:打印每个class的实例数目、内存占用、类全名信息(内部类名字开头会加上前缀"*").
- 6. 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
9. 系统CPU经常100%,如何调优?
思路:
原因:CPU100%一定有线程在占用系统资源。
解决步骤:
- 1. 找出哪个进程cpu高:使用top命令
- 2. 该进程中的哪个线程cpu高:使用top -Hp
- 3. 导出该线程的堆栈: 使用jstack 命令工具
- jstack主要解决线程间死锁、死循环、请求外部资源导致的长时间等待
- 4. 查找哪个方法(栈帧)消耗时间: 使用jstack命令工具
- 5. 分析工作线程占比高 | 垃圾回收线程占比高
10. 系统内存飙高,如何查找问题?
思路:
飙高不严重,可以使用阿里的Arthas(阿尔萨斯)可视化工具。严重只能通过当时的dump文件分析。
解决步骤:
- 1. 导出堆内存文件(dump文件):使用jmap命令工具
jmap -dump:format=b,file=D:/test/filename.hprof
- 分析:使用 jhat、jvisualvm 、mat 、jprofiler ...
jhat -J -Xmx30g filename.hprof
实战:《参考Hippo测试环境启动项目内存吃满的情况》
11. 对象是如何定位访问?
句柄定位:
Java 堆会画出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。优点:稳

直接指针访问:
java堆对象的不居中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。优点:速度快。

12. JVM有哪些垃圾回收器,实际中如何选择?
相关概念:**
- 并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态。
- 并发(Concurrent):用户线程和垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
- 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

思路:
- 从serial【C瑞欧】聊到G1
- GC从单线程到多线程并行 serial + serial old -> ParNew or Parallel Scavenger+ Parallel Old
- serial:新生代;单线程;复制。
- serial old:老年代;单线程;标记-整理。
- ParNew:新生代;serial多线程并行版;复制。
- Parallel 【啪肉来L】 Scavenger(简称PS):新生代;吞吐量优先收集器,可控制吞吐量的收集器。
- Parallel Old(简称PO):老年代;标记-整理。
- GC从并行多线程到并发多线程,减少STW CMS
- CMS(Concurrent Mark Sweep):
- 并发的“标记-清理”收集器;
- 优点:并发收集、低停顿;缺点:CUP资源敏感、无法处理浮动垃圾、标记-清理产生大量空间碎片;
- 运行步骤:初始标记、并发标记、重新标记、并发清除
- 标记算法:三色标记算法 + Incremental Update
- 垃圾回收的内存越来越大,使用G1
- Serial 几十兆;PS 上百兆~几个G;CMS - 20G;G1 - 上百G;ZGC - 4T - 16T(JDK13)。
13. G1垃圾收集器有了解么,有什么特点?
概念:
G1使用在多核、大内存的机器上,它可以实现指定GC暂停时间,同时还能保持较高的吞吐量。
特点:
- 全代的分代收集器,它在逻辑分代(Region区域组成不同的代),物理不分代。
- 内存分区(Region):将内存划分为一个个相等大小的内存分区,回收时,以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此,G1天然就是一种压缩方案(局部压缩);

- 空间整合:整体采用标记-整理,局部采用(两个Region之间)复制,保证运行期间不会产生碎片。
- G1的收集都是STW,采用混合收集方式(复制、标记-整理),并且可以限制收集范围,从而降低停顿。
- 可预测的停顿时间:每一次的垃圾回收时间都可控,那么对于大堆(16G左右)的垃圾收集会有明显优势。
拓展点:
- 运行步骤:初始标记(STW,但耗时很短)、并发标记(并发)、最终标记(STW,可并行)、筛选收集(可并发)
- 标记算法:三色标记算法 + SATB
14. G1相关问题:
G1是否分代?
- G1在逻辑分代,物理不分代。
- G1将Java堆划分为多个大小相等的独立区域(Region),由一个或多个连续Region组成新生代或老年代。Region可以动态改变,但在同一个时刻只能是一种。
- 整体基于“标记—整理”算法,局部(两个Region之间)基于“复制”算法,保证运行期间不会产生碎片。
G1垃圾回收器会产生FGC吗?
会产生FGC,JDK10之前FGC都是串行回收。串行回收采用标记清除-算法,很慢,尽量不要产生。
G1什么时候引发Full GC?
- 1. 拷贝存活对象(Evacuation)时,没有足够的空间(to-space)来存放晋升的对象;
- 2. 并发处理过程完成之前空间耗尽。
如何避免G1产生FGC?
- 扩内存
- 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
- 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
为什么G1用SATB?(简单看看)
- 灰色到白色,引用消失时,如果没有黑色指向白色的引用会把push到堆栈中。下次扫描时拿这个引用,由于RSet的存在,不需要扫描整个堆去查找白色引用,效率比较高。
- SATB配合RSet,浑然天成。
15. 描述一下CMS和G1的异同?
- CMS只对老年代进行收集,采用“标记-清除”算法,会出现内存碎片;
- G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的。G1运作期间不会产生内存空间碎片,尤其是当Java 堆非常大的时候,G1 的优势更加明显。
- G1建立了可预测的停顿时间模型, 可以设定停顿时间的目标,减少每一次的垃圾收集时间,相比于CMS,G1 必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。