2021面试-JVM
JVM内存模型
程序计数器(线程私有)
- Java方法指向字节码文件的行号指示器
- Native方法为空
- 这是虚拟机唯一一个没有OOM的区域
虚拟机栈(线程私有)
- 是Java方法的执行的内存模型
- 方法在执行的时候会创建一个栈帧:包含局部变量表,操作数栈,动态链接,方法返回地址。
- 栈帧随着方法调用被创建,随着方法结束而销毁。
本地方法栈(线程私有)
- 和虚拟机栈作用类似,区别是:虚拟机栈为Java方法服务,而本地方法栈是为Native方法服务
- HotSpot VM将本地方法区和虚拟机栈合二为一。
堆 (线程共享)
- 运行时数据区
- 创建的对象和数组都是保存在Java堆内存中。
- 堆也是垃圾收集器进行垃圾回收的最重要区域。
- 从GC的角度分为:新生代(Eden,From Survivor,To Survivor)和老年代
方法区/永久代(线程共享)
- 用于存储JVM加载的类信息。常量和静态变量,即时编译后的代码数据。
- 包含运行时常量池,用于存放各种字面量和符号引用。
MinorGC的过程(复制-清空-互换)
- MinorGC采用复制算法
- eden,from复制到to区 , 年龄+1
- 清空eden,from
- to和from互换
MajorGC的过程:
- 首先扫描一次老年代,标记出存活的对象,回收没有标记的对象。
- 耗时比较长,会产生内存碎片
产生OOM:
- 永久带,GC不会在主程序运行期间对永久带进行清理,所以随着加载class的增多而胀满,最终抛出OOM
- 老年代,当有大的对象老年代装不下的是就会抛出OOM
老年代:
- 存放应用周期长的内存对象
垃圾回收
如何确定对象已死?
- 引用计数法:会产生循环引用的问题
- 可达性分析法:
- 通过GCroots作为起点,向下搜索,当对象没有任何的引用链相连,说明已经死亡。
- VM栈中的引用 — 方法区的静态引用 ---- JNI中的引用
垃圾收集算法:
- 标记清楚法 : 效率低,内存碎片多
- 标记:标记处需要回收的对象
- 清除:清除被标记的对象所占用的空间
- 复制算法:
- 将内存分为均等的两块,每次只使用一块,
- 当一块满了复制存活的对象到另一块
- 把已使用的清除掉
- 标记整理算法:
- 结合了上面两种算法
- 标记阶段和标记清除差不多,标记后不清理对象,而是将存活的对象移到另一端
- 然后清除端边界外的对象。
- 分代收集法:
- 新生代采用复制算法
- 每次只有少量的存活对象,只需付出少量的复制成本就可以完美收集
- 每次只使用eden和from区,将存活的复制到to区
- 老年代采用标记整理算法
- 当对象在Survivor区躲过一次GC后,其年龄+1, 默认下年龄到达15的对象移到老年代
- 新生代采用复制算法
垃圾收集器:
- Serial
- 最基本的垃圾收集器,单线程,复制算法
- Client模式下新生代的垃圾回收器
- ParNew
- Serial收集器的多线程版本, 复制算法
- -XX:ParallelGCThreads参数限定线程数
- Server模式下新生代默认的垃圾回收器
- Parallel Scavenge
- 多线程复制算法、高效
- 利用高吞吐量高效的利用CPU时间
- Serial Old
- 单线程,标记整理算法
- Client默认老年代垃圾收集器
- Server模式下:
- 与新生代的Parallel Scavenge搭配使用
- 作为老年代CMS的后备垃圾收集方案
- Parallel Old
- 多线程, 标记整理算法
- CMS - Concurrent Mark Sweep
- 多线程,标记清除算法
- 垃圾收集的四个阶段
- 初始标记
- 并发标记
- 重复标记
- 并发清除
- 美团技术相关文章
- G1: Garbage first
- 标记整理算法,不产生内存碎片
- 非常精准的停顿时间,不牺牲吞吐量的前提下, 实现低停顿垃圾回收
多路复用IO模型
- 会有一个线程不断的轮询多个socket的状态,只有当socket真正有读写事件的时候,
- 才真正调用实际的IO读写操作。
相关参数:
堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
- -XX:MaxPermSize=n:设置持久代大小
收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
并行收集器设置
- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
Linux上排查JVM问题
- top : cpu,内存,磁盘,负载等
- jstat :gc和类加载情况
- jmap : 查看jvm内存占用
- jstack :对账信息,线程信息
类加载-反射
JVM类加载机制
- 加载-验证-准备-解析-初始化
- 加载: 在内存中生成一个代表这个类的对象,作为方法区这个类的数据入口。
- 验证: 确保class文件的字节流中包含的信息是否符合当前虚拟机的要求
- 准备: 在方法区中分配变量所使用的内存空间。初始化阶段为默认值
- 解析: 虚拟机将常量池中的符合引用替换为直接引用的过程
- 初始化: 真正执行类中定义的Java程序代码
- 初始化是执行类构造器的过程
- 启动类加载器: Bootstrap ClassLoader
- 拓展类加载起: Extension ClassLoader
- 应用程序类加载器: Application ClassLoader
- 用户自定义加载器: User ClassLoader
- 初始化是执行类构造器的过程
双亲委派:
- 当一个类收到类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,
- 每一层类加载器都是如此。
- 只有当父类反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载。
- 目的是:保证不同的类加载最终得到的是同一个object对象。
双亲委派的几个好处:
- 可以避免类的重复加载
- 保证了安全性
双亲委派 “父子加载器”之间是继承关系吗?
双亲委派模型中,类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合的关系来复用父加载器的代码
⭐️ 双亲委派是如何实现的 ?
- 先检查类是否已经被加载过
- 若没有加载则调用父类加载器的loadClass()进行加载
- 若父类加载器为空,则默认使用启动类加载器作为父加载器
- 若父类加载失败,抛出ClassNotFoundException异常后,在调用自己的findClass()进行加载
如何主动破坏双亲委派机制:
- 自定义一个类加载器
- 重写其中的loadClass()方法,使其不进行向上委派即可
双亲委派被破坏的列子
-
- 双亲委派出现之前:JDK1.2之后才引入的,在这之前是没有遵守的
-
- JNDI,JDBC等需要加载SPI接口实现类的情况。
-
- 为了实现热插拔热部署的工具。
-
- Tomcat等容器的出现
-
- OSGI,Jigsaw等模块化技术的应用
Person p = new Person()在内存中发生了什么?
- 将Person.class文件加载进内存中。
- 在栈中为p开辟一个变量空间
- 在堆中为对象分配空间
- 对对象中的成员变量进行初始化
- 调用静态代码块,非静态代码块对象进行初始化(没有就不执行)
- 调用构造方法对对象进行初始化,对象初始化完毕
- 将对象的内存地址赋值给p变量,让p变量指向该对象。
什么情况下创建的对象不在堆中?
- jit编译器下的逃逸分析算法会分析类使用情况