JVM内存布局
JVM在内存布局上可以分为哪些区域?
- 堆(线程共享):GC的主要回收地,包含几乎所有的实例对象、字符串常量池;
- 元空间(线程共享):在本地内存分配,包含类元信息、字段、静态属性、方法、常量等;
- 虚拟机栈(线程私有):是描述Java方法执行的内存区域;
- 本地方法栈(线程私有):本地方法栈为Native方法服务,线程调用本地方法时,会进入一个不再受JVM约束的世界;
- 程序计数器(线程私有):程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。
垃圾回收
什么样的对象会被回收?
- 通过一系列称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,该对象将被回收,这个过程又称为可达性分析算法。
- 简单来说就是,没有被GC Roots引用的对象会被回收。
什么是GC Roots?
- 类静态属性或常量中引用的对象;
- 虚拟机栈中引用的对象;
- 本地方法栈中引用的对象。
什么是Stop The World?
- 可达性分析的执行需要一个确保一致性的快照下进行,即对象引用关系达到稳定,因此必须停顿所有java线程,这种操作称为Stop The World,简称 STW。
什么是安全点和安全区域?
- Stop The World 需要java线程到达安全点或安全区域后执行;
- 安全点(Safe Point)可以是方法调用、循环跳转、异常跳转等;
- 安全区域指一段代码片段中,引用关系不会发生变化。
被GC Roots引用的对象就一定不会回收吗?
常见的GC回收算法有哪些?
- 复制算法:将内存分为大小相等的两块,每次只使用其中一块,回收完把存活的复制到另一块,当前区域清空,这样回收的效率会很高,不会产生空间碎片,缺点是内存的使用上限变低了,而且不适合存放长期生存的对象,适合新生代;
- 标记-清除:先根据GC Roots标记可达对象,再清除不可达的对象,缺点是回收的效率比复制算法低,回收完会产生内存空间碎片;
- 标记-整理:在标记清除基础上增加了对象整理过程,避免空间碎片,适合老年代使用。
一个对象从出生到被回收的过程是什么?
- 绝大部分对象在Eden区生成,当Eden区满了的时候,会触发YGC。没有被引用的对象直接回收,依然存活的对象被移送到Survivor区。
- Survivor区分为S0和S1两块内存空间,每次YGC时,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。另外每次GC后,存活的对象年龄会+1,当达到一个阈值的时候直接移至老年代,这个阈值默认是15。
- 如果YGC要移送的对象大于Survivor区容量的上限,则直接移至老年代。如果老年代也无法放下,则会触发FGC。如果依然无法放下,则抛出OOM。
CMS的工作原理是什么?
cms的特点
针对老年代;
采用标记清除算法(为了节省时间,不进行整理,会产生内存碎片,可配置成周期性整理);
并发收集,低停顿;
以获得最短停顿时间为目标。
回收过程
初始标记:仅标记一下GC Roots能直接关联到的对象,不用向下追溯,速度很快,但需要STW;
并发标记:进行GC Roots Tracing的过程, 这个过程相对耗时,但却可以和用户线程并行;
重新标记:将并发标记期间发生变化的对象进行重新标记,需要STW;
并发清除:回收所有的垃圾对象,整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作。
G1的工作原理是什么?
g1的特点
并行与并发:STW时间短。
分代收集,收集范围包括新生代和老年代 ,而不需要与其他收集器搭配。将整个堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离,它们都是一部分Region的集合。
结合多种垃圾收集算法,空间整合,不产生碎片。从整体看,是基于标记-整理算法,从局部(两个Region间)看,是基于复制算法。
可局部收集,可预测停顿:每个Region都一个Remembered Set,记录引用其它区域对象的指针,这样便可以局部回收避免全局扫描。因此可以做到可预测停顿的时间。
垃圾最多的Region,会被优先收集,这也是 G1 名字的由来。
回收过程
初始标记:仅标记一下GC Roots能直接关联到的对象,用户程序能在正确可用的Region中创建新对象。需要STW,但速度很快。
并发标记:进行GC Roots Tracing的过程, 这个过程相对耗时,但却可以和用户线程并行;
最终标记:将并发标记期间发生变化的对象进行重新标记,需要STW;
筛选回收:首先排序各个Region的回收价值和成本,然后根据用户期望的GC停顿时间来制定回收计划,最后按计划回收一些价值高的Region中垃圾对象。 回收时采用复制算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存。可以并发进行,降低停顿时间,并增加吞吐量。
什么时候才应该考虑使用G1?
- 堆空间在6G以上;
- 存活数据占50%以上堆内存;
- 大对象比较多或晋升比较快的情况;
- GC 停顿在0.5~1s之间。
是否听说过ZGC?
ZGC是JDK11新加入的低延迟收集器,它立下了三个令人期待的flag:
停顿时间不会超过 10ms;
停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下);
可支持几百 M,甚至几 T 的堆大小(最大支持 4T)。
常用命令与参数
如何查看jvm进程参数?
jinfo -flags 1280
如何查看gc回收情况?
##1280是pid,每1000ms 打印一次
jstat -gcutil 1280 1000
如何导出dump文件?
jmap -dump:format=b,file=/usr/temp/heap.hprof 1280
CPU飙升如何定位问题?
## 找到占用cpu最多的进程,记录pid
top
## -Hp参数查看进程中占用cpu最多的线程信息,记录tid
top -Hp 7083
## 将十进制的tid转换成十六进制
printf %x 32468
## jstack将进程的线程栈信息打印,最后再根据十六进制tid找到问题线程的上下文
jstack 7083 >stack.log
free命令的作用是什么?
- 用于查看当前内存使用情况,主要包括的字段有total、used、free、shared、buff/cache、available;
- available(可用内存)= free + buffer/cache - 不可被回收内存。
netstat命令的作用是什么?
# 查看8081端口的使用情况
netstat -tunpl | grep 8081
iostat命令的作用是什么?
常用的JVM参数与推荐值
https://hujinyang.blog.csdn.net/article/details/103655911
类加载原理
一个类被加载的全流程是怎样的?
- 基于双亲委派模型,确定class的classloader;
- 执行具体的加载、验证、准备、解析、初始化等步骤。
什么是双亲委派模型?工作流程是什么?有什么优点?
- 当Application ClassLoader 收到一个未知类的加载请求时,会执行loadClass方法,先判断这个类是否已经被加载过,如果没有,则交给它的parent(Extension ClassLoader)进行加载;
- Extension ClassLoader收到类加载请求后,同样执行loadClass的逻辑,如果这个类没有被加载过,它会继续向它的上级Bootstrap ClassLoader询问这个类是否被加载;
- Bootstrap ClassLoader收到类加载请求后,如果发现该类没有被加载过,此时它会尝试加载这个类;
- 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),会抛出异常,下级Extension ClassLoader捕获异常后,自己尝试加载;
- 如果Extension ClassLoader也加载失败,与上面过程同理,由Application ClassLoader加载;
- 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载;
- 如果均加载失败,就会抛出ClassNotFoundException异常。
- 优点:Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种环境下的加载顺序。 保证了运行的安全性,防止不可信类扮演可信任的角色。
如何自定义类加载器?
- 正常方式:继承ClassLoader类并实现findClass方法;
- 破坏双亲委派:继承ClassLoader类重写loadClass方法。
自定义类加载器有哪些使用场景?
- 隔离加载类。例如在某些框架内进行中间件与应用的模块隔离,把类加载到不同环境。
- 修改类加载方式。类的加载模型并非强制,除了Bootstrap ClassLoader外,其他的加载并非一定要引入。可以按需动态加载。
- 扩展加载源。例如从数据库、网络,甚至电视机机顶盒进行加载。
- 防止源码泄漏。Java代码容易被编译和篡改,可以通过自定义类加载器进行编译加密。
有哪些开源实现破坏了双亲委派?
SPI
典型的例子就是JDBC加载数据库驱动;
JDK提供了统一的JDBC驱动接口Driver,各种数据库厂商(MySQL、Oracle等)会根据SPI规范,以jar包形式提供自己的实现。对于实现类的查找与加载本就属于JDK的职责范畴,但是这些实现类都存在于classpath路径下,顶层的双亲类加载器是无法找到的,需要借助子加载器AppClassLoader才能加载,但这又违背双亲委派原则。
为了解决这个问题,Java设计师引入了一种线程上下文类加载器(Thread Context ClassLoader),可以通过Thread类的setContextClassLoader()方法进行设置,如果没有设置,默认就是AppClassLoader。这样便可以可以利用线程上下文加载器去加载SPI的实现类,实现一种父类加载器请求子类加载器完成类加载的行为。虽然这样违背了双亲委派原则,但也解决了一些特殊需求。
能否自定义一个java.lang.Object类?
https://hujinyang.blog.csdn.net/article/details/104113847
- 正常情况下类加载过程会遵循双亲委派机制,依次向上级类加载器委托加载,上级都加载不了,才会自行加载。
- 如果想绕过双亲委派机制,需要覆写ClassLoader类的loadClass方法,一般不推荐这么做。
- 由于final方法defineClass的限制,正常情况下我们无法加载以“java.”开头的系统类。
- 一般自定义类加载器只需实现ClassLoader的findClass方法来加载自定义路径下的类,而不是覆写loadClass破坏双亲委派,避免带来系统安全隐患。
编译原理
什么是解释执行?什么是编译执行?
- 解释执行:解释器对程序逐条翻译成机器语言,然后再由计算机执行,优点就是可以立刻执行,无需预热,不会产生新代码;
- 编译执行:编译器将程序一次性翻译成机器语言,后续可以直接执行,集成更多优化,有效提升执行效率,缺点是需要预热等待生成机器码。
JIT即时编译器的作用是什么?
- 虚拟机将热点代码编译成与本地平台相关的机器码,从而提高热点代码的执行效率;
- 同时JIT还能做一些代码级的优化,如方法内联、栈上分配、锁消除、锁粗化等。
什么是方法内联?
- 方法内联指的是,把一些短小的方法体,直接纳入目标方法的作用范围之内,就像是直接在代码块中追加代码。这样,就少了一次方法调用,执行速度就能够得到提升。
除了基本数据类型,一定是在堆上分配的吗?
- 不一定,通过逃逸分析,JVM 能够分析出一个新的对象的使用范围,从而决定是否要将这个对象分配到堆上。
什么是逃逸分析?
- 对象被赋值给成员变量或者静态变量,或者作为返回结果返回,可能被外部使用,变量就发生了逃逸。
什么是栈上分配?
- 如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力;
- JIT可以将某些没有逃逸出方法的对象优化成栈上分配。