「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
前言
- 关于作者:励志不秃头的一个CURD的Java农民工
- 关于文章:以下内容单纯为作者觉得面试八股文中比较经常遇到的总结,同时会穿插一些作者面试遇到的问题作为记录,打*号的都是作者主观认为比较重要的
JVM虚拟机
JVM ,Java虚拟机,屏蔽了与具体平台相关的信息,使得Java语言在不同平台运行时不需要重新编译
虚拟机架构
- 最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域。
- 中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)。
- 最下层:解释器、JIT(just in time)编译器和 GC(Garbage Collection,垃圾回收器)
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的退出
- 某线程调用Runtime类或System类的exit方法,或 Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止 ,由于操作系统出现错误而导致Java虚拟机进程终止
类加载机制
加载过程
加载、验证、准备、解析、初始化、使用、卸载
面试题:热点代码
- JVM会对热点代码做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块的运行特别频繁的时候,就有可能把这部分代码认定为热点代码
- JVM使用热点探测来检测是否为热点代码。热点探测一般有两种方式,计数器和抽样。HotSpot使用的是计数器的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器
- 两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言
双亲委派模型*
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
实现
- 首先检查类是否被加载;
- 若未加载,则调用父类加载器的**loadClass**方法;
- 若该方法抛出ClassNotFoundException异常,则表示父类加载器无法加载,则当前类加载器调用findClass加载类;
- 若父类加载器可以加载,则直接返回Class对象;
优势
- 避免类的重复加载,确保一个类的全局唯一性
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 保护程序安全,防止核心API被随意篡改
劣势
- 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
面试题:如何破坏双亲委派机制? **
- 只要加载类的时候,不是从APPClassLoader->Ext ClassLoader->BootStrap ClassLoader 这个顺序找,就是破坏了双亲委派机制
- 我们自己可以自定义一个类继承ClassLoader,重写loadClass方法,不依照往上开始寻找类加载器,那就算是打破双亲委派机制了
面试题:为什么要从父类开始加载? **
如果不从父类开始加载,那么相同的类可以就会加载多次。造成内存浪费的同时也会带来安全隐患
如:A写了一个String类,然后自己加载;B又写了一个String类,然后自己又加载,加上JVM原来的String,这时候系统就出现了多个String类,那么类之间的比较结果及类的唯一性将无法保证,同时造成了内存的浪费。
面试题:Tomcat打破双亲委派机制了吗?为什么?(为什么Tomcat不能使用默认的双亲委派机制?)*
Tomcat打破了双亲委派机制
-
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
- 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
- 做法:Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找
-
并不是所有类都是需要隔离的,有一些类在版本相同时,是可以共享的,如:redis
Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。
tomcat的几个主要类加载器:
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;
内存结构
介绍
堆、元空间(方法区)、虚拟机栈、本地方法栈、程序计算器
线程共享
- 堆:用于存放对象的实例,与内存回收(垃圾回收)有关
- 新生代
- Eden区
- Survivor(from)区
- Survivor(to)区
- 设置Survivor是为了减少送到老年代的对象
- 设置两个Survivor区是为了解决碎片化问题
- 老年代
- 新生代
- 方法区/元空间--在JDK1.8之后,方法区被永久移除了,取而代之的是元空间
- 静态常量池
- 运行时常量池
线程私有
生命周期与线程相同
- 虚拟机栈
- 栈帧
- 动态链接
- 符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。
- 前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。
- 动态链接
- 操作数栈
- 保存着Java 虚拟机执行过程中的数据
- 局部变量表
- 方法返回地址
- 异常
- 线程请求的栈深度大于虚拟机所允许的深度
- StatckOverflowError
- JVM动态扩展时无法申请到足够的内存时
- OutOfMemoryError
- 线程请求的栈深度大于虚拟机所允许的深度
- 栈帧
- 本地方法栈
- 本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的「本地方法」指的是「非Java方法」,一般本地方法是使用C语言实现的。
- 程序计数器
面试题
什么情况下栈会发生内存溢出*
因为栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表、方法出口等信息,如果线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出StackOverflowError,栈内存溢出异常;一般在方法递归调用时会造成栈内存溢出,但不全是
对象都是在堆上分配吗?*
不一定;在特定条件时,可以在虚拟栈上分配内存
- JVM通过逃逸分析,分析出新对象的使用范围,就可能将对象在栈上进行分配
- 栈分配可以快速在栈帧上创建和销毁对象,不用再将对象分配到堆空间,减少GC的压力
逃逸分析
- 逃逸分析,是Java虚拟机可以分析新创建对象的使用范围,并决定是否在Java堆上分配内存的一项技术,从jdk 1.7开始已经默认开启逃逸分析
- 如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
//sb逃逸
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
//sb没有逃逸
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
JVM有那几块内存区域,Java 8之后对内存分代做了什么改进?
Java8之后移除了永久代,增加了元空间
- 永久代使用的是虚拟机内存,容易造成内存OOM异常;
- 元空间直接使用本地内存,默认情况下元空间是可以是可以无限使用本地内存的,只要本地内存足够就不会出现OOM异常
- 设置元空间最大大小:-XX:MaxMetaspaceSize
- 默认值:unlimited,意味着只受系统内存的限制
- -XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
结语
JVM是每一次一面基本都绕不过去的槛,,虽然平时工作中,我们很少会注意到JVM,但是依旧需要了解,因为不知道什么时候就遇到了OOM的情况。只有平时做好准备,机会来临时才能把握住。