IT服务|面试阿里被问到JVM,不逼逼赖赖,直接盘给面试官看!!!
文章图片
面试阿里被问到JVM,不逼逼赖赖,直接盘给面试官看!!!
-
- 概述
- JVM体系结构
- 类加载机制
-
-
- 类加载器
- 类加载过程
- 双亲委派机制
- 全盘负责委托机制
- 打破双亲委派机制
- 自定义类加载器实现
-
- JVM运行时数据区
-
-
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区(元空间)
- 运行时常量池
- 直接内存
-
- 垃圾回收机制
-
-
- GC对象判定方法
- 垃圾收集算法
- 垃圾收集器
- JVM调优参数
-
概述
JVM是Java Virtual Machine
(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机(JVM从软件层面帮我们屏蔽不同操作系统在底层硬件与指令上的区别),字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
JVM体系结构
文章图片
Java虚拟机包含类装载器子系统、执行引擎、运行时数据区、本地方法接口和垃圾收集模块。其中垃圾收集模块在Java虚拟机规范中并没有要求Java虚拟机垃圾收集,但是在没有发明无限的内存之前,大多数JVM实现都是有垃圾收集的。
- 类装载器子系统:根据给定的全限定类名(如:java.lang.Object)来装载class文件到运行时数据区域的方法区中。
- 执行引擎:执行字节码或执行本地方法。
- 运行时数据区:我们常说的JVM的内存,堆,方法区,虚拟机栈,本地方法栈,程序计数器。
- 本地方法接口:与本地方法库交互,作用就是为了融合不同编程语言为Java所用,它的初衷是融合C/C++程序。
类加载机制 Java类加载机制就是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类加载器 类加载器分为启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器。各种类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们直接没有从属关系。除了启动类加载器(
Bootstrap ClassLoader
)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader
。- 启动类加载器(
BootstrapClassLoader
):负责加载$JAVA_HOME/jre/lib
目录下的核心类库,比如rt.jar,charsets.jar。 - 扩展类加载器(
ExtClassLoader
):负责加载支撑J$JAVA_HOME/jre/lib/ext
目录下的JAR类包。父加载器是启动类加载器。 - 应用类加载器(
AppClassLoader
):负责加载ClassPath
路径下的类包,主要就是加载我们自己写的那些类。父加载器是扩展类加载器。 - 自定义类加载器(
CustomClassLoader
):负责加载用户自定义目录下的类包。父加载器是应用类加载器。
- 加载:将字节码从不同的数据源(可能是 class 文件,也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;并在堆中生成一个代表该类的
java.lang.Class
对象,作为对方法区这个类的各种数据的访问入口。
- 验证:验证的目的是为了确保加载进来的class文件符合JVM的规范,一般是进行文件格式的验证、元数据的验证、字节码验证和符号引用验证。
- 文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
- 元数据的验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
- 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
- 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
- 准备:给类的静态变量分配空间,并赋予默认值。
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块。
【IT服务|面试阿里被问到JVM,不逼逼赖赖,直接盘给面试官看!!!】双亲委派机制的优点:
- 沙箱安全机制:避免核心API被篡改。自己写的
java.lang.String.class
类不会被加载。 - 避免重复加载:如果父加载器已经加载过该类,子类加载器就没有必要再去加载。
ClassLoader
类的loadClass()
方法:protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// 首先会检查该类是否已经被本类加载器加载,如果已经被加载则直接返回
Class> c = findLoadedClass(name);
if (c == null) {// 如果没有被加载,则委托父加载器去加载
//加入Java开发交流君样:593142328一起吹水聊天
long t0 = System.nanoTime();
try {if (parent != null) {// 让父加载器对象去调用loadClass方法
c = parent.loadClass(name, false);
} else {// parent==null,说明父加载器是启动类加载器。启动类加载器是C++编写的,这里去调用本地方法区尝试加载该类。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果父加载器没有加载到该类,则自己去加载。这里会调用URLClassLoader类的findClass()方法
//加入Java开发交流君样:593142328一起吹水聊天
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {resolveClass(c);
}
return c;
}
}
全盘负责委托机制 全盘负责委托机制就是当一个
Classloader
加载一个Class
的时候,这个Class
所依赖的和引用的其它Class通常也由这个Classloader负责加载。打破双亲委派机制 打破双亲委派机制就是我们希望自定义类加载器去直接加载指定类,而不是先委托父加载器去加载或者是自定义类加载器加载不到才让父加载器去进行加载。
自定义类加载器实现 了解双亲委派机制以及打破双亲委派机制之后,我们可以自己写一个自定义类加载器。自定义类加载器实现思路:
如果使用双亲委派机制就是重写
findClass()
方法(类加载器具体去加载类的方法),如果要打破双亲委派机制,在重写
findClass()
方法基础上,还需要重新loadClass()
方法,这里我们可以改写逻辑,先让该类加载器去加载类,加载不到再让父加载器去进行加载JVM运行时数据区 程序计数器
- 程序计数器线程私有的,它的生命周期与线程相同,它是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程当前正在执行的方法是本地方法,这个计数器值则应为空。
- 字节码解释器的工作就是通过这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。
- 这个区域是唯一不会抛出OutOfMemoryError异常的区域。
- 虚拟机栈
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(
boolean
、byte
、char
、short
、int
、float
、long
、double
),对象引用和returnAddress
类型。以下异常条件与Java虚拟机栈相关:
如果线程中请求的栈深度大于虚拟机所允许的深度,Java虚拟机将会抛出
StackOverflowError
异常。如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,Java虚拟机将会抛出
OutOfMemoryError
异常。- 本地方法栈
以下异常条件与本地方法栈相关联(与虚拟机栈一样):
如果线程中请求的栈深度大于虚拟机所允许的深度,Java虚拟机将会抛出
StackOverflowError
异常。如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,Java虚拟机将会抛出
OutOfMemoryError
异常。- 堆
以下异常情况与堆相关联:
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
堆内存分为年轻代(
Young Generation
)和老年代(Old Generation
)。- 年轻代(
YoungGen
):年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。 - 老年代(
OldGen
)。
- 方法区(元空间)
以下异常条件与方法区域相关联:
如果方法区无法满足新的内存分配需求时,Java虚拟机将会抛出
OutOfMemoryError
异常。- 运行时常量池
以下异常条件与类或接口的运行时常量池的构造相关联:
如果运行时常量池无法再申请到内存时,则Java虚拟机将抛出
OutOfMemoryError异常
。- 直接内存
OutOfMemoryError
异常出现,所以这里简单提一下。直接内存的分配不会受到Java 堆大小的限制,既然是内存,肯定还是会受到本机总内存的大小及处理器寻址空间的限制。
服务器管理员配置虚拟机参数时,一般会根据实际内存设置
-Xmx
等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制从而导致动态扩展时出现OutOfMemoryError
异常。垃圾回收机制 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
GC对象判定方法
- 引用计数法:为每个对象创建一个引用计数器,有对象引用时计数器+1,引用被释放时计数器-1,当计数器为0时就可以被回收。它有一个缺点就是不能解决循环引用的问题。
- 可达性算法(引用链法):从
GC Roots
开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots
没有任何引用链相连时,则证明此对象是可以被回收的。【白嫖资料】
- 分代收集理论
- 标记—清除算法
文章图片
- 标记—复制算法
文章图片
- 标记—整理算法
文章图片
垃圾收集器
- Serial 收集器(标记—复制算法):新生代单线程收集器,标记和清理都是单线程,[优点是简单高效]
- ParNew 收集器(标记—复制算法):新生代并行收集器,实际上是
Serial
收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
- Parallel Scavenge 收集器(标记—复制算法):新生代并行收集器,追求高吞吐量,高效利用
CPU
。吞吐量=用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效的利用CPU
时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景。
- Serial Old 收集器(标记—整理算法):老年代单线程收集器,Serial收集器的老年代版本。
- Parallel Old 收集器(标记—整理算法):老年代并行收集器,吞吐量优先,
Parallel Scavenge
收集器的老年代版本。
- Concurrent Mark Sweep(
CMS
)收集器(标记—清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- Garbage First(G1)收集器(标记—整理算法):Java堆并行收集器,G1收集器是
JDK1.7
提供的一个新收集器,G1
收集器基于“标记—整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java
堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或者老年代。
-Xms4g
:初始化堆大小为4g-Xmx4g
:堆最大内存为4g-XX:NewRatio=4
:设置年轻代和老年代的内存比例为1:4-XX:SurvivorRatio=8
:设置新生代Eden和Survivor
比例为8:2(8:1:1)-XX:+UseParNewGC
:指定使用ParNew + Serial Old
垃圾回收器组合-XX:+UseParallelOldGC
:指定使用ParNew + ParNew Old
垃圾回收器组合-XX:+UseConcMarkSweepGC
:指定使用CMS + Serial Old
垃圾回收器组合-XX:+PrintGC
:开启打印gc信息-XX:+PrintGCDetails
:打印gc详细信息
推荐阅读
- 社保代缴公司服务费包含哪些
- 私有化轻量级持续集成部署方案--03-部署web服务(下)
- 探索免费开源服务器tomcat的魅力
- 2018国考外交部面试演讲不再难——只需把握好三点
- iOS面试题--基础
- [源码解析]|[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)
- Dubbo引用服务
- java|java 常用知识点链接
- 第六章|第六章 Sleuth--链路追踪
- 云原生微服务技术趋势解读