1.内存分配和回收原则
- 大多数情况下,对象在新生代中Eden区分配,当Eden区空间不足时,虚拟机将进行一次MinorGC,将继续存活的对象存入Survivor区,并将存活的对象,年龄+1,对象在Survivor区每熬过一个Minor GC,年龄+1,年龄加到一定程度(默认值为15,可以通过
-XX: MaxTeruringThresold来设置阈值。),将会将对象转入老年代。 - 如果Eden区空间不足,虚机进行MinorGC后,将继续存活的对象存入Survivor区,但Survivor区的空间不足时,直接存入老年代。
- 大对象(大量连续空间:数组、字符串)直接存入老年代,为了减少新生代垃圾回收的频率和成本。
- G1垃圾回收器,通过
=XX:G1HeapRegionSize设置堆区域大小和-XX:G1MixedGCLiveThresholdPercent设置阈值,来决定那些对象直接入老年代 - Parallel Scavege垃圾回收期,默认情况下,没有一个固定的阈值(
XX:ThresholdTolerance是动态调整的)来决定何时直接在老年代分配大对象。而是由虚机根据当前堆内存情况和历史数据动态决定的。
- G1垃圾回收器,通过
2.死亡对象判断法
- 引用计数法:给每个对象增加一个引用计数器,每当有地方引用,计数器就加1,当引用失效,计数器就减1,任何时候计数器为0的对象就是不可能再被使用的。 有个严重问题:会产生循环引用问题,无法解决。
- 可达性分析法: 通过将一系列称为 “GC Roots”对象作为起点, 从这些结点向下搜索,结点所走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链时,就说这个对象无引用,需要被回收。
- 哪些对象可作为GC Roots: 虚拟机栈中引用对象、本地方法栈中引用对象、方法区中静态对象、方法区中常量、被同步锁指定的对象、JNI(java native interface)所引用的对象。
3.Java8内存结构
- Java 堆(heap), 存放对象实例、数组、字符串常量池、静态变量、线程分配缓冲区
- 虚拟机栈(Stack),执行java方法, 存放栈帧,其中包括局部变量表,操作数栈,动态连接,方法返回地址
- 程序计数器
- 本地方法栈:执行native方法
- 本地内存:方法区(元空间实现),存放类信息的版本、字段、方法、接口,常量池表(符号引用、字面量)类加载之后,进入运行时常量池。
- 直接内存:用来提高Io效率
4.垃圾收集算法
- 复制算法(copying): 年轻代使用的时Minor GC. 这种GC算法采用的是复制算法(Copying)
- MinorGC会将Eden中的所有或的对象都移动到 Survivor 区中,如果Survivor区中放不下,那么剩下的对象就被移动Old generation中, 即 一旦收集后,Eden区就变空了
- 当对象在Eden出生后,经过一个Minor GC后,如果Eden和from区的对象还存活,并能被to区所容纳,则使用复制算法 将还存活的对象复制到 to 区, 然后清理所使用的Eden区和from区,并将这些对象年龄设置为1,以后对象在Survivor区中,每存活一次,年龄+1/
- 优势:不会产生内存碎片,可以利用 bump-the-poniter 实现快速内存分配; 没有标记和清除的过程,效率高。
- 劣势: 复制算法想要使用,要保证对象的存活率非常低才行。浪费了to区内存。
- 标记清除算法:分为标记和清除两个阶段
- 首先要标记出所有需要回收的对象, 在标记完成后,统计回收所有被标记的对象;可以反过来标记存活的对象,统一回收未被标记的对象
- 优点: 解决了复制算法的痛点,to区不再被浪费。
- 缺点:
- 执行效率不稳定,标记和清除两个过程的执行效率会随着对象的数量增长而降低。
- 内存空间碎片化问题,标记清除之后会产生大量不连续的内存碎片
- 此算法要暂停整个应用STW(Stop the world)
- 标记压缩(Mark-compact): 老年代一般由标记清除或是标记清除与标记整理的混合实现
- 标记所有存活的对象,在整理压缩阶段,将标记的存活的对象都移向一端,然后直接清除边界以外的内存
- 优势: 弥补了清除算法中内存区域分散的缺点,也消除了复制算法浪费to区内存的问题
- 劣势:效率不高,不经要标记所有存活的对象,还要整理所有存活对象的引用地址。
- 标记-清理-压缩(Mark-Sweep-Compact)
- 标记清理和标记要锁的结合
- 一开始和标记清理一致,多次GC后才进行压缩。
- 优势:减少了移动对象的成本。
5. 垃圾收集器
- Serial收集器: 单线程的垃圾收集器,只有使用一个线程进行垃圾回收,会暂停其他所有工作线程(STW,Stop th e world), 直到它收集结束。 不适合服务器环境
- ParNew收集器: 就是Serial收集器的多线程部那边呢,除了使用多线程进行垃圾收集外,其余行为与Serial收集器完全一致
- Parallel Scavenge 收集器: 也是使用标记-复制算法的多线程收集器,主要关注点是吞吐量(高效率的利用CPU). CMS等垃圾收集器的关注点更多是用户线程的停顿时间(提高用户体验)。 吞吐量:CPU中运行用户代码的时间与CPU总消时间的比值。
- Serial Old 收集器: Seraial收集器的老年代版本, 单线程收集器,用途:一个是在JDK1.5及之前的版本中与 Parallel Scavenge 收集器搭配使用, 另一个用途作为CMS收集器的后备方案。
- Prallel Old 收集器: Parallel Scavenge收集器的老年代版本。
- CMS收集器:是一种以获取最短回收停顿时间为目标的收集器。实现了让垃圾收集线程与用户线程(基本上)同时工作。
- 过程:
- 初始标记:暂停所有其他线程,记录下与root相连的对象
- 并发标记:同时开启GC和用户线程,用一个闭包结构记录可达对象。因为用户线程会不断更新引用域,所以GC线程无法保证可达性分析的实时性。所以会跟踪记录这些发生引用更新的地方。
- 重新标记:修正并发标记期间用户线程导致标记变动的记录。这段时间停顿时间比初始标记时间稍长。
- 并发清除:开启用户线程,同时GC线程开始对未标记的区域清扫。
- 优点:并发收集、低停顿
- 缺点:对CPU资源敏感、无法处理浮动垃圾(并发清理期间用户线程产生的垃圾)、使用标记-清除算法会导致大量碎片空间产生。
- 过程:
- G1收集器:Garbage-First是针对配备多颗处理器和大容量内存的机器,以极高概率满足GC停顿时间要求,还具备高吞吐量性能特性。
Java对象创建的过程
① 类加载检查 ② 分配内存 ③ 初始化零值 ④ 设置对象头 ⑤ 执行init方法
堆空间的基本结构? 什么情况下对象会进入到老年代
堆空间的基本结构: * Eden:伊甸园区 * S0: 幸存者0区 * s1: 幸存者1区 * Old: 老年代 * 1.7以及之前是永久代 1.8 元空间 MetaSpace(使用直接内存).
- 进入老年代的对象:
-
比较大的对象会直接进入到老年代: 大对象需要连续的空间,而大对象进入老年代是由垃圾收集器和相关参数决定的。大对象进入老年代是一种优化策略,降低新生代GC频率和成本。
- G1会使用+xx: G1HeapRegionSize 和 -xx: G1MixedGCLiveThresoldPercent 设置阈值
-
长时间存活的对象进入老年代: 对象从Eden区分配空间后,经历一次MinorGC后,依然存活,就会进入Survivor区,年龄为1;每次S1-S0之间的MinorGC,还存活的对象年龄+1,当大于15时,此对象就会进入老年代。可以通过-XX: MaxTenuringThresold 拉设置进入老年代的年龄。
-
如何判断对象是否存活? 根可达的流程? 哪些对象可作为GC Roots?
- 引用计数法: 有一个对象引用,引用计数器就加1,当引用失效,引用计数器就减1,当引用计数器为0,表示没有对象引用。 但是存在问题,如果两个对象互相引用,就无法判断了。
- 根可达GC Roots: 从GC Roots的对象开始,向下搜索,会根据引用关系形成一条引用链,当一个对象没有在任何一条引用链中时,表示此对象不可用,需要被回收。
- 可以作为GCRoots的对象:
- 虚机栈中引用的对象
- 本地方法栈中应用的对象
- 方法区中静态变量引用的对象
- 方法去中常量引用的对象
- 所有被同步锁持有的对象
对象可以被回收,就一定会被回收吗?
- 不会,会进入缓刑阶段,经历两次标记之后才会回收。 首先是经过可达性分析,判断不可达时进行一次标记,之后会再经过一次标记,要是这个对象再没有与任何引用链有关联,就会被回收。
jdk中有几种引用类型?他们的特征分别是什么?
- 强引用:不会被回收,即使空间不足,宁愿抛出ooM也不回收。
- 软引用:当空间不足时,就会回收
- 弱引用:不管空间充不充足,只要发现了弱引用就会回收。
- 虚引用:任何时候只要发现就会回收
垃圾收集有哪些算法? 各自有什么特点?
- 常见的垃圾收集器:Serial\ ParNew \ paralle Scavenge\ CMS\ G1 \ ZGC
- 垃圾收集算法:
- 复制算法(copying): 年轻代使用的时Minor GC. 这种GC算法采用的是复制算法(Copying)
- MinorGC会将Eden中的所有或的对象都移动到 Survivor 区中,如果Survivor区中放不下,那么剩下的对象就被移动Old generation中, 即 一旦收集后,Eden区就变空了
- 当对象在Eden出生后,经过一个Minor GC后,如果Eden和from区的对象还存活,并能被to区所容纳,则使用复制算法 将还存活的对象复制到 to 区, 然后清理所使用的Eden区和from区,并将这些对象年龄设置为1,以后对象在Survivor区中,每存活一次,年龄+1/
- 优势:不会产生内存碎片,可以利用 bump-the-poniter 实现快速内存分配; 没有标记和清除的过程,效率高。
- 劣势: 复制算法想要使用,要保证对象的存活率非常低才行。浪费了to区内存。
- 标记清除算法:分为标记和清除两个阶段
- 首先要标记出所有需要回收的对象, 在标记完成后,统计回收所有被标记的对象;可以反过来标记存活的对象,统一回收未被标记的对象
- 优点: 解决了复制算法的痛点,to区不再被浪费。
- 缺点:
- 执行效率不稳定,标记和清除两个过程的执行效率会随着对象的数量增长而降低。
- 内存空间碎片化问题,标记清除之后会产生大量不连续的内存碎片
- 此算法要暂停整个应用STW(Stop the world)
- 标记压缩(Mark-compact): 老年代一般由标记清除或是标记清除与标记整理的混合实现
- 标记所有存活的对象,在整理压缩阶段,将标记的存活的对象都移向一端,然后直接清除边界以外的内存
- 优势: 弥补了清除算法中内存区域分散的缺点,也消除了复制算法浪费to区内存的问题
- 劣势:效率不高,不经要标记所有存活的对象,还要整理所有存活对象的引用地址。
- 标记-清理-压缩(Mark-Sweep-Compact)
- 标记清理和标记要锁的结合
- 一开始和标记清理一致,多次GC后才进行压缩。
- 优势:减少了移动对象的成本。
- 复制算法(copying): 年轻代使用的时Minor GC. 这种GC算法采用的是复制算法(Copying)
常见的GC? 谈谈Minor GC 和 Full GC的理解。Minor GC和Full GC在什么时候触发? MinorGc会发生STW现象吗?
- 常见的GC根据收集范围可以分为两种:
- Partial Gc:不是收集正个GC堆的模式
- Young GC/Minor GC: 只收集年轻代的GC, 一般在Eden区空间分配满后触发,将存活的对象进入幸存者区,部分对象进入老年代。
- Old GC: 只收集老年代的GC, 只有CMS 的并发清除有这个模式
- Mixed GC: 收集年轻代和老年代的GC, G1有这个模式
- Full GC: 或Major GC, 收集整个堆,年轻代、老年代、永久代或元空间。
- 触发时机:①当年轻代经过Young GC之后,存活的对象要进入老年代,但是老年代的空间不足时,会触发Full GC. ② 当永久代的分配空间不足时,会触发FullGC.③ System.gc() 也是fullGC
- Partial Gc:不是收集正个GC堆的模式
CMS垃圾收集器的四个步骤? CMS有什么缺点?
-
步骤:
- 初始标记:短暂暂停,标记与root直接相连的对象。
- 并发标记:同时开启GC和用户线程,用闭包结构来记录可达对象,由于在这个过程用户线程会更新引用,而GC线程无法保证可达性分析的实时性,所以此算法会跟踪记录发生变更的引用。
- 重新标记:修复并发标记阶段发生引用变化的标记记录。这个阶段停顿的时间会比初始标记阶段长一些。
- 并发清除:开启用户线程,同时GC对未标记的区域进行清理。
-
优点:并发收集,低停顿。
-
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 底层使用标记-清理算法,会产生大量的碎片空间。
-
并发标记:就是用户线程和GC线程并发。由于在分析可达性的时候,用户线程也会更新引用,这个时候就会导致出现 浮动垃圾(本来应该被删除的,但是因为更新引用,被标成了黑色), 对象消失问题(本来是黑色,更新引用后,变白色了)。 解决方案:增量更新和原始快照-> 根据记录,重新标记
三色标记法?
- 白色:表示当前对象还没有被垃圾收集器访问过。 如果分析结束阶段,还是白色,表示不可达
- 黑色: 表示当前对象已被访问过,并且此对象的所有引用都扫描过了。表示安全存活,可达。
- 灰色:表示当前对象被垃圾收集器访问过了,但是此对象部分引用未扫描。
G1垃圾收集器的步骤? 有什么缺点?
-
步骤:
- 初始标记:短暂停顿stw, 标记GC Roots直接引用的对象
- 并发标记:与应用线程并发,标记所有可达的对象,这个过程时间较长,取决于堆空间的大小和对象的多少
- 最终标记: 短暂停顿stw, 处理并发标记残留的少量引用变更
- 筛选回收:根据标记结果,选择回收价值高的区域,将标记存活的对象,复制到新区域,回收旧区域内存。这个过程会有一次或多次停顿,取决于回收的复杂度。
-
缺点:
- 使用标记-整理算法:效率不高。
ZGC?
- GC的停顿时间影响服务可用性
- CMS和G1的stw有局限性,有上限。
- ZGC:
- 初始标记: stw
- 并发标记/对象定位
- 再标记: stw
- 并发转移准备:
- 初始转移: stw
- 并发转移
ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。
JVM的安全点和安全区代表什么?
-
把栈上的引用类型的位置全部记录下来,这样到 GC 的时候就可以直接读取,而不用一个个扫描了。Hotspot 就是这么实现的,这个用于存储引用类型的数据结构叫
OopMap -
OopMap的更新,从直观上来说,需要在对象引用关系发生变化的时候修改。不过导致引用关系变化的指令非常多,如果对每条指令都记录OopMap的话 ,那将会需要大量的额外存储空间,空间成本就会变得无法忍受的高昂。选用一些特定的点来记录就能有效的缩小需要记录的数据量,这些特定的点就称为 安全点 (Safepoint) 。- 通常选择一些执行时间较长的指令作为安全点,如方法调用、循环跳转和异常跳转等。
-
处于
Sleep或者Blocked状态的线程无法跑到安全点,需要引入安全区域, 安全区域指的是,在某段代码中,引用关系不会发生变化,线程执行到这个区域是可以安全停下进行 GC 的。因此,我们也可以把 安全区域 看做是扩展的安全点。
什么是字节码? 类文件结构的组成?
- 字节码:在java中,JVM能够理解的代码, 可移植性高,Java无需重新编译便可在不同操作系统上运行。
- 类文件结构组成:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//字段数量
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
类的生命周期?类加载的过程? 加载这步发生了什么?初始化阶段哪些情况必须对类进行初始化?
- 生命周期:
- 加载
- 连接:
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
-
类的加载过程:
- 加载:
- 连接:验证、准备、解析
- 初始化
-
加载完成的事情:加载过程中,通过类加载器来完成,具体哪个类由哪个类加载器加载有双亲委派模型决定(也可打破双亲委派模型)
- 通过全类名获取此类的二进制字节流
- 将二进制字节流所代表的静态结构转换为方法区运行时数据结构
- 在内存中生一个代表该类的class对象,作为方法区数据访问的入口
-
必须初始化的情况:
- 遇到
new、getstatic、putstatic或invokestatic这 4 条字节码指令时:new: 创建一个类的实例对象。getstatic、putstatic: 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)。invokestatic: 调用类的静态方法。
- 使用
java.lang.reflect包的方法对类进行反射调用时如Class.forName("..."),newInstance()等等。如果类没初始化,需要触发其初始化。 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main方法的那个类),虚拟机会先初始化这个类。 MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用findStaticVarHandle来初始化要调用的类。- 「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
- 遇到
双亲委派模型了解? 如果打破双亲委派模型?
-
类加载器:
- BootstrapClassLoader: 启动类加载器,主要加载java的核心库
%JAVA_HOME%/lib目录下的rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被-Xbootclasspath参数指定的路径下的所有类 - ExtensionClassLoader:扩展类加载器,主要负责加载
%JRE_HOME%/lib/ext目录下的 jar 包和类以及被java.ext.dirs系统变量所指定的路径下的所有类。 - ApplicationClassLoader: 面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
- 自定义加载类,需要实现 ClassLoader父类,并重写
- BootstrapClassLoader: 启动类加载器,主要加载java的核心库
-
双亲委派模型:自底向上查找判断类是否被加载, 如果未被加载,从顶向下尝试加载
- 类加载时,首先会判断类是否被加载,如果被加载就直接返回,如果没有被加载,再交由父类判断
- 类加载器进行类加载时,首先不会加载类,而是交给父类进行加载,这样就会传到顶层 BootstrapClassLoader中
- 当父类反映无法加载此类,子加载类才会尝试加载
- 如果子加载类也无法加载,就会抛出 ClassNotFoundException的异常。
- 打破双亲委派的方法:
- 自定义加载器的话,需要继承
ClassLoader。如果我们不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写loadClass()方法。
- 自定义加载器的话,需要继承
双亲委派模型有什么好处?双亲委派模型是保证一个类在JVM中是唯一的?
- 避免类被重复加载 和 防止核心API被篡改
- Java区分不同类是通过 类名和加载此类的类加载器, 即使类名相同,加载的类加载器不同,也会视为不同的类。双亲委派模型确保核心类总是由 BootstrapClassLoader 加载,保证了核心类的唯一性。
- 即使通过重写ClassLoader也无法恶意篡改核心类。因为在ClassLoader的方法 preDefinedClass方法会判断当前类是否未 java. 开头的,如果是就会抛出 securityException ,防止核心类被恶意篡改或被伪造。
堆内存相关的参数?
-
-Xms2G -Xmx5G : 表示堆内存最小2G,最大5G
-
显式新生代内存:
- 方案一:
- -XX:NewSize=256m 新生代内存空间最小分配256m
- -XX:MaxNewSize=512m 新生代内存空间最大分配512m
- 方案二:
- -Xmn256m 新生代内存空间分配256m
- 方案一:
-
垃圾收集器:
- 串行垃圾收集器:-XX:+UseSerialGC
- 并行垃圾收集器:-XX:+UseParalleGC
- CMS垃圾收集器:-XX:+UseConcMarkSweepGC
- G1垃圾收集器:-XX:+UseG1GC
-
处理OOM
- -XX:+HeapDumpOnOutOfMemoryError :指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。
- -XX:HeapDumpPath=
./java_pid<pid>.hprof:表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个<pid>标记,则当前进程的进程 id 将附加到文件名中,并使用.hprof格式 - -XX:OnOutOfMemoryError="< cmd args >;< cmd args >" :用于发出紧急命令,以便在内存不足的情况下执行; 应该在
cmd args空间中使用适当的命令。例如,如果我们想在内存不足时重启服务器,我们可以设置参数:-XX:OnOutOfMemoryError="shutdown -r"。 - -XX:+UseGCOverheadLimit :是一种策略,它限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例
项目是否实际配置过?
项目中遇到过GC问题吗,是怎么分析和解决的?
Java中9种常见的CMS GC问题分析与解决 - 美团技术团队
项目中是如何JVM调优的?
JVM调优-JVM调优实践一携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详 - 掘金
如何降低Full GC的频率
- 可以减少进入老年代的对象数量,可以降低FullGC的频率。
GC性能指标了解吗? 调优原则?
- GC性能指标通常关注:吞吐量、停顿时间和垃圾税后频率
- 调优原则:降低FUll GC的执行频率 和 减少Full GC的执行时间。