文章目录
- JVM回顾
- JVM、JRE、JDK之间关系?
- Java程序执行过程?
- 面试官:解释执行和JIT(及时编译)两种执行方式有什么区别?
- java虚拟机内存管理
- jvm整体架构
- JVM只是定义内存划分规范等,具体实现依赖不同虚拟机实现,如HotSpot虚拟机
- jvm运行时内存
- 程序计数器(PC寄存器)
- 面试官:程序计数器是什么?
- 面试官:java多线程如何实现的?多个线程同时执行的?
- 虚拟机栈
- 面试官:什么是java虚拟机栈?
- 本地方法栈
- 堆
- 元空间
- 面试官:为什么要废弃永久代,引入元空间?
- 方法区
- 面试官:元空间和方法区有什么区别?
- 运行时常量池
- 面试官:常量池和运行时常量池有什么区别?
- 直接内存
- 面试官:你知道如何读取大文件不保证内存溢出?
- 面试官:给我讲讲什么是NIO以及实现原理?
- OOM异常
- JVM类加载机制
- 面试官:你知道一个java类从编译到运行的全过程?
- java源码编译阶段
- 面试官:给我讲讲类整个执行过程?
- 类加载系统
- 类加载系统的执行过程
- 面试官:给我讲讲类加载执行顺序以及各个阶段作用?
- 加载
- 验证
- 准备
- 解析
- 面试官:“解析”一定要在“初始化”之前执行?什么是“静态绑定”和“动态绑定”?
- 初始化
- 面试官:给我讲讲类初始化阶段 静态变量、静态代码块以及包含子类父类等初始化顺序?
- 类加载器
- 面试官:Class对象和实例对象以及类之间的区别?
- 类加载器分类
- 双亲委派模型
- 什么是双亲委派模型?
- 如何自定义类加载器?
- 自定义类加载器
- ClassLoader源码剖析
- 垃圾回收机制及算法
- 如何判断对象已经死亡?
- 面试:讲讲常见的垃圾收集算法?
- 面试:垃圾回收器了解?讲讲几种垃圾回收器?
- 我理解的垃圾回收器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- 【面试必问】G1收集器
- 用过jvm调优工具?
- jvm常用指令
- jvm常用工具
- Jconsole监控管理工具
- VisualVM可视化工具
- 线上问题如何对GC日志分析?
- 理解GC日志参数
- GC日志分析方法
- GC日志分析工具
- 对生产环境jvm调优过?
- tomcat
- jmeter
- 有过jvm参数优化经验?你们生产环境jvm启动参数如何配置对:
JVM回顾
JVM、JRE、JDK之间关系?
- JVM(java virtual machine):java虚拟机,它是操作系统和我们java程序的桥梁,它封装了操作系统和java程序底层交互信息(底层还是解释成操作系统识别的指令),使java一次编译处处运行。它定义了java内存区域、虚拟机类加载过程、垃圾回收等一整套流程,使得我们java程序能够顺利在操作系统基础上运行。
- JRE(java runtime environment):java运行环境,单靠JVM是无法完成一次编译处处运行的,还需要很多基础类库,例如:怎么操作文件、怎么连接网络等,因此 JRE=JVM+基础类库。
- JDK(java developer kit):java开发工具,现在JRE给我们提供了运行环境,但是想让开发者能够顺利编写代码,使它们不用去关心底层实现,因此JDK面向开发者提供了更多的类库、小工具,例如:java、javac、jar工具、集合框架、并发框架等基础类库等等,因此 JDK=JRE+面向开发者的类库、工具。
Java程序执行过程?
面试官:解释执行和JIT(及时编译)两种执行方式有什么区别?
我们一直在说 Java 字节码是沟通 JVM 与 Java 程序的桥梁,下面使用 javap 来稍微看一下字节码到底长什么样子。
package net.dreamzuora.jvm;public class HelloWorld {public static void main(String[] args) {System.out.println("Hello World");}
}
查看字节码指令步骤:
1.javac Helloword.java
2.javap -c HelloWorld
警告: 二进制文件HelloWorld包含net.dreamzuora.jvm.HelloWorld
Compiled from "HelloWorld.java"
public class net.dreamzuora.jvm.HelloWorld {public net.dreamzuora.jvm.HelloWorld();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 获取静态字段的值3: ldc #3 // String Hello World 常量池中的常量值入栈5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V invokevirtual 运行时方法绑定调用方法8: return
}
然后 JVM 会翻译这些字节码,它有两种执行方式。常见的就是解释执行,将 字节码指令 + 操作数翻译成机器代码;
另外一种执行方式就是 JIT,也就是我们常说的即时编译,它会在一定条件下将字节码编译成机器码之后再执行。
编译器是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,翻译与执行是分开的,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;C,C++都是靠编译实现的。
解释器则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,翻译与执行一次性完成,所以运行速度是不如编译后的程序运行的快的,但是就启动效率而言,解释执行的速度更快,因为它不需要进行编译过程
Java程序也需要编译,但是没有直接编译称为机器语言,而是编译成为字节码,然后在JVM上用解释方式执行字节码。Python 的也采用了类似Java的编译模式
Java通过解释器解释执行字节码,这样的执行方式相对较慢,尤其是遇到一些运行频繁的代码块或者方法时。于是后来JVM引入了JIT即时编译器(just in time),当JVM发现某些代码运行频繁时就会认定为热点代码“hot spot code”,为了提高运行效率,就会把这些代码编译成为平台相关的机器码然后进行优化,JIT就是用来完成这项工作的。二者共同造就了java的优势——当程序需要迅速启动时,解释器首先发挥作用,省去编译时间,当程序运行时,编译器会逐渐将更多的代码编译成本地机器码从而获得更高的效率。
引用:《解释执行与编译执行以及JIT的区别》
java虚拟机内存管理
jvm整体架构
名称 | 作用 | 配置参数 | 异常 |
---|---|---|---|
程序计数器(线程私有) | 字节码运行的行号指令器 | 无 | 无 |
虚拟机栈(线程私有) | 存储局部变量表、操作栈、动态链接、方法出口等信息 | -Xss | StackOverflowError/ OutOfMemoryError |
堆 | 保存对象实例(包括数组) | -Xmn -Xms -Xsx | OutOfMemoryError |
方法区 | 类信息、常量、静态变量、即时编译器编译后的代码等数据 | -XX:PermSize:16M -XX:MaxPermSize:64MB | OutOfMemoryError |
本地方法栈 | 为虚拟机使用到的Native 方法服务 | 无 | StackOverflowError/OutOfMemoryError |
JVM只是定义内存划分规范等,具体实现依赖不同虚拟机实现,如HotSpot虚拟机
jvm运行时内存
程序计数器(PC寄存器)
面试官:程序计数器是什么?
- 是一块较小的内存单元。
- 看作->当前线程所执行的字节码行号指令器。
- 字节码解释器工作时通过改变计数器的值来选取将要执行的字节码指令、分支、循环、跳转、异常处理等功能。
特点:
1.计算机上的PC寄存器是用来存放“伪指令”或地址,jvm中的pc寄存器(程序计算器)是一块内存用来存放将要执行的指令地址
2.程序计数器是线程私有的,生命周期同线程生命周期,每个线程都有一个
3.该区域不回出现OOM
面试官:java多线程如何实现的?多个线程同时执行的?
jvm通过给线程分配处理器执行时间,使线程能够执行,但是一个处理器一次只能执行一条指令,因此java多线程是通过线程切换的方式使得在同一时间段多个线程同时执行,但是同一时刻一个处理器只能执行一个线程。
因此为了多线程能够顺利切换,需要利用程序计数器来记录每个线程运行的字节码指令,并且属于线程私有内存,各个线程互不共享。
虚拟机栈
参考文章:
操作数栈详解
动态链接、栈帧解释
面试官:什么是java虚拟机栈?
切记:java虚拟机栈是线程私有的
- 存储局部变量表:方法参数、方法内定义的局部变量,存储的是八种基本数据类型以及returnAddress类型(指向一条指向字节码指令的地址)
- 方法返回地址:return或者抛出异常返回的地址
- 操作数栈:用来程序计算的存储单元
- 动态链接:将字节码中对应的符号引用变成直接引用地址
本地方法栈
和java虚拟机栈类似只不过执行的是native方法
堆
特点:
- 存储对象实例、数组
- 堆是多线程共享空间,除了TLAB( Thread Local Allocation Buffer)堆中也包含私有的线程缓冲区
- 垃圾回收的主要区域
堆划分:
jdk7:年轻代、老年代、永久代
jdk8:年轻代、老年代
GC:
分为部分GC和整个FULL GC
部分收集器: 不是完整收集java堆的的收集器,它又分为:
新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集
老年代收集 (Major GC / Old GC): 只是老年代的垃圾收集 (CMS GC 单独回收老年代)
混合收集(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收, region区域回收)
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集器
元空间
从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
面试官:为什么要废弃永久代,引入元空间?
- 原来的永久代中存储类的元数据、静态变量、常量等,永久代中等空间大小不好确定,如果内存太小会内存溢出
- 永久代回收概率低,会对GC回收造成干扰
这样做的好处:
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。
- 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。
- 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。
方法区
与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载 的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据。
类型信息
对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:
- ① 这个类型的完整有效名称(全名 = 包名.类名)
- ② 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)
- ③ 这个类型的修饰符( public, abstract,final的某个子集)
- ④ 这个类型直接接口的一个有序列表
域信息
域信息,即为类的属性,成员变量
JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。
域的相关信息包括:
域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的
某个子集)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称方法的返回类型(或void) 2. 方法参数的数量和类型(按顺序)
- 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
- 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏
移地址、被捕获的异常类的常量池索引
面试官:元空间和方法区有什么区别?
运行时常量池
面试官:常量池和运行时常量池有什么区别?
字节码文件中,内部包含了常量池
方法区中,内部包含了运行时常量池
常量池:存放编译期间生成的各种字面量与符号引用*
运行时常量池:常量池表在运行时的表现形式
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加
载到内存中,存储在了方法区的运行时常量池中。
让我们来看看字节码编译后,查看常量池相关信息
package net.dreamzuora.jvm;public class HelloWorld {public HelloWorld() {}public static void main(String[] var0) {System.out.println("Hello World");}
}
反编译:javap -verbose HelloWorld.class
字节码文件除了包含了类的版本信息、方法、字段、接口等描述信息外,还包括常量池表:包括字面量、域、类型和方法的符号引用。
虚拟机指令根据常量池表找到要执行的类名、方法名、参数类型、字面量等类型。
指令解释:ldc 把常量池中的项压入栈、getstatic 从类中获取静态字段、调度对象的实现方法:invokevirtual
Javap与JVM指令解释
为什么需要常量池?
public class Solution { public void method() { System.out.println("dreamzuora");
} }
这段代码很简单,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会
更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,采取加载。
直接内存
直接存储大小不受JVM里内存,它是直接利用操作系统内存的堆外内存。
面试官:你知道如何读取大文件不保证内存溢出?
利用MappedByteBuffer类来读取,读取方式
面试官:给我讲讲什么是NIO以及实现原理?
NIO:New Input/Output
NIO基于通道(Channel)和缓冲区(Buffer)的IO方式,通过Native方法直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象引用这个内存地址进行操作,NIO之所以快这是通过这种方式直接访问堆外内存避免了java堆和native堆来回copy。
ByteBuffer:读取虚拟机分配的堆内存,受堆大小影响,会有OOM。
DirectByteBuffer:继承ByteBuffer,但是直接访问虚拟机物理内存的类,
在访问普通的ByteBuwer时,系统总是会使用一个“内核缓冲区”进行操作。
而DirectBuwer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuwer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuwer更快。(???这块没搞懂后期补充)
《Linux 内核详解以及内核缓冲区技术》
OOM异常
JVM类加载机制
面试官:你知道一个java类从编译到运行的全过程?
java源码编译阶段->类加载阶段->执行阶段
java源码编译阶段
执行:javac HelloWord.java
将java->变成class文件
编译阶段做的三件事(这块想深入理解可以去看《编译原理》日后有时间会简单做个总结)
- 词法分析和输入到符号表
- 注解处理
- 语义分析与字节码class文件生成
详细过程:
源代码文件.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> JVM字节码文件.class
查看字节码信息:javap -c HelloWord.class
引用:
《编译做了哪些事》
《java 编译和加载和执行类的全过程》
类加载阶段以下内容会讲到
面试官:给我讲讲类整个执行过程?
类加载系统
- 类加载子系统负责从文件、网络中加载.class文件
- 将类信息存放到方法区,还会存储常量池信息(字符串字面量、数字常量)
- classLoader只负责加载,运行由执行引擎(Execution engine)决定
- 调用构造方法将对象实例存储到堆中
类加载系统的执行过程
面试官:给我讲讲类加载执行顺序以及各个阶段作用?
加载、验证、准备、解析、初始化、使用、卸载
简要说明:
- 加载:把Java字节码byte[]转换成JVM中的java.lang.Class类的对象;
- 验证:Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。
- 初始化:主要是执行静态代码块和初始化静态域;
加载
- 预加载:加载JAVA_HOME/lib/下的rt.jar下的.class文件
- 运行时加载:虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类
加载阶段的三件事:
1.获取class文件二进制流(文件读取操作rt.jar中)
2.将类信息、静态变量、常量、字节码放入方法区
3.在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般
这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
虚拟机规范对这三点的要求并不具体,因此虚拟机实现与具体应用的灵活度都是相当大的。例如第一条,根本没有
指明二进制字节流要从哪里来、怎么来,因此单单就这一条,就能变出许多花样来:
- 从zip包中获取,这就是以后jar、ear、war格式的基础
- 从网络中获取,典型应用就是Applet
- 运行时计算生成,典型应用就是动态代理技术
- 由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件
从数据库中读取,这种场景比较少见
验证
.class文件不光是通过java编译而来,可以通过任何手段生成class文件,例如利用十六进制编辑器生成等,因此为了验证class文件是否合法,需要对class信息进行验证。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
为类的变量分配空间并初始赋值在方法区中,类的实例变量在初始化的时候赋值并分配在堆中。
这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
code-snippet 1 将会输出 0,而 code-snippet 2将无法通过编译。
切记
这是因为局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。
因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。符号引用和虚拟机的内存布
局是没有关系的,引用的目标未必已经加载到内存中了。
解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做了哪些工作呢?
- 类或接口的解析
- 类方法解析
- 接口方法解析
- 字段解析
面试官:“解析”一定要在“初始化”之前执行?什么是“静态绑定”和“动态绑定”?
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地
开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。
当子类和父类(接口和实现类)存在同一个方法时,子类重写父类(接口)方法时,程序在运行时调用的方法时,是调用父类(接口)的方法呢?还是调用子类的方法呢?我们将确定这种调用何种方法的操作称之为绑定。
静态绑定是在程序执行前就已经被绑定了(也就是在程序编译过程中就已经知道这个方法是哪个类中的方法)。
动态绑定:编译器在每次调用方法时都要进行搜索,时间开销相当大。因此虚拟机会预先为每个类创建一个方发表(method table),其中列出了所有方法的签名和实际调用的方法。
引用:《动态绑定和静态绑定》
初始化
类初始化阶段做的事情:
- 类初始化,调用类构造器cinit()方法对类初始化
- 对象初始化,构建实例对象,调用类的构造方法init()初始化对象
类的初始化和对象初始化?
cinit()方法:类构造器对类的静态变量、静态代码块初始化过程
init()方法:调用类的构造方法对实例对象进行初始化
首先让我们类看看这段代码:
public class InitDemo {static {i = 0;System.out.println(i);}static int i = 1;public static void main(String[] args) {}
}
第一个问题:这段代码执行结果是什么?
答案:会报错。
那么第二个问题:这段代码为什么会报错,会抛出什么异常?
那么第三问题:为什么会抛出这个异常?
编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问
面试官:给我讲讲类初始化阶段 静态变量、静态代码块以及包含子类父类等初始化顺序?
执行步骤:
- 父类静态变量和静态代码块(先声明的先执行);
- 子类静态变量和静态代码块(先声明的先执行);
- 父类的变量和代码块(先声明的先执行);
- 父类的构造函数;
- 子类的变量和代码块(先声明的先执行);
- 子类的构造函数。
面试官:Java语言里,new表达式总体负责两个动作?
- 分配对象空间并对其做默认初始化。默认初始化会将对象的所有成员字段设到其类型对应的默认值(零值)。
- 初始化对象会按照程序猿设定的值对类变量、局部变量等值赋值
引用文章:
《静态变量、代码块、子类父类等顺序》
《类初始化阶段做的事》
类加载器
类加载器负责将class字节码文件二进制流加载到内存中,放入方法区,并生成java.lang.Class对象的过程,用来封装类在方法区内的数据结构。
注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
面试官:Class对象和实例对象以及类之间的区别?
《javaClass对象详解》
类加载器分类
BootstrapClassLoader引导类加载器、自定义类加载器(ExtensionClassLoader拓展类加载器,SystemClassLoader系统类加载器、用户自定义类加载器)
作用:不同的类加载器负责加载不同路径下的Class
《通俗易懂 启动类加载器、扩展类加载器、应用类加载器》
双亲委派模型
什么是双亲委派模型?
有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。
或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
如何自定义类加载器?
自定义类加载器
ClassLoader源码剖析
垃圾回收机制及算法
如何判断对象已经死亡?
面试:讲讲常见的垃圾收集算法?
面试:垃圾回收器了解?讲讲几种垃圾回收器?
我理解的垃圾回收器
Serial收集器
serial:Serial(串行)[ˈsɪəriəl]收集器
ParNew收集器
Parallel Scavenge收集器
Parallel scavenge:Parallel(并行)[ˈpærəlel] Scavenge(清除)[ˈskævɪndʒ]收集器