关于Java虚拟机,在面试的时候一般会问的大多就是①Java内存区域、②虚拟机垃圾算法、③虚拟机垃圾收集 器、④JVM内存管理、⑤JVM调优、⑥Java类加载机制这些问题了。推荐书籍《深入理解Java虚拟机:JVM高级特性 与最佳实践(第二版》、《实战Java虚拟机》。
用一张图展示关于jvm涉及的模块及他们的关联关系。
jvm处理的是被javac编译java后的class文件。即从class文件开始,被类加载器加载后在jvm内存中处理,jvm内存模型分两大类:一类是所有线程共享数据区(索引线程共享的数据区)包括:方法区和堆;另一类是每个线程独有的(线程隔离的数据区)包括:虚拟机栈、本地方法栈、程序计数器,共享的数据被执行引擎进行处理,线程独有的被本地库接口处理最终执行索引被本地库接口执行再最终被本地方法接口处理。还有GC收集器的相关内容。
那按照上图关于jvm虚拟机涉及到的知识:
1.Java代码的编译和执行:java文件的编译从java文件--》class文件;类加载及类加载器和著名的双亲委派机制;类执行机制(jvm执行引擎);
2.虚拟机参数
3.jvm内存模型(JMM)
4.对象的创建到对象的内存分配再到对象的访问
5.CG垃圾收集器,判断对象什么时候可以被回?对象收集方法有哪些?常见的垃圾收集器有哪些?
面试常见问题:
1.介绍下 Java 内存区域(运行时数据区)
java内存模型JMM(java manary model):JDK1.8之前,Java内存区域包括堆、方法区、虚拟机栈、本地方法栈、程序计数器,1.8之后使用元数据区替代了方法区。
Java内存区域是指 JVM运行时将数据分区域存储 ,简单的说就是不同的数据放在不同的地方。通常又叫 运行时数据区域。
Java内存模型(JMM)定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
其中程序计数器、虚拟机栈、本地方法栈是线程独有的内存区,而堆和方法区是线程共享区;
1) 程序计数器(Program Counter Register):也叫PC寄存器,程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
看文字比较抽象直接上代码
public class PCRegister {
public static void main(String[] args) {
int x = 1;
int y = 2;
System.out.println(x+y);
}
}
用Java 类解析器 javap对class文件进行反编译,查看分解的class文件。有两种方式
第一种:javac XXX.java 先将java类编译成class文件,再javap -v XXX.class就可以查看到了
第二种:插件jclasslib进行
第一种方式:
第一步:右键字节码文件打开控制台终端
第二步:在控制台输入命令:javap -v 字节码文件名称
第三步:控制台打印的内容即是反编译后的,前面标数字的就是程序计数器的行号指示器
如果没有class文件可以直接javac XXX.java 文件进行编译先生成class文件
第二种方式idea安装插件
安装 jclasslib Bytecode Viewer 插件
选中PCRegister.java类 在工具栏view下拉找到 点击 Show Bytecode with Jclasslib
然后是英文的,做一下本地语言设置就可以
选中方法Methods -> 选中main -> 选中 Code, 即可查看字节码反编译后的内容
如上图,字节码文件反编译后可以看到有一系列 指令地址和 操作指令。比如前面的数字0,1,...这些就是指令地址,而他后面跟着的就是操作指令。程序计数器就相当于一个临时空间存储将要执行的指令地址当该指令地址对应的操作指令被执行引擎解释并执行后存储下一个指令地址。
要想让计算机执行程序,需要让执行引擎中的解释器将字节码操作指令解释成CPU能够识别的机器指令。
而选取哪一条操作指令进行解释并执行,这个时候就需要依赖于程序计数器了。可以把它想象成一个临时空间,用于存储字节码操作指令的指令地址。
本图中,0 就是一个指令地址,通过指令地址就能够找到哪条指令,说明当前需要选取执行的操作指令是:iconst_1。
如果执行完0后,需要执行指向1的这条指令,那么将程序计数器(PC计数器)中存储的指令地址改成1就行了。
注意:当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
2).虚拟机栈
内存栈 FILO(fist in last out)
描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。生命周期和线程的生命周期是一致的。作用:主管Java程序的运行,保存方法的局部变量,部分结果,并参与方法的调用和返回。
栈的特点:
栈是一种快速有效的分配储存方式,访问速度仅次于程序计数器。
JVM虚拟机栈的操作只有两个。
每个方法执行,伴随着进栈
方法执行结束后,伴随着出栈。
对于栈来说并不存在垃圾回收的问题,但是存在溢出的问题。
使用参数 -Xss选项来设置每个线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
每个线程都会创建一个独有的虚拟机栈,虚拟机栈由一个个栈帧组成,一个栈对应一个方法,栈帧包括四块内容:操作数栈、局部变量、动态连接、方法返回地址。
虚拟机栈有几个概念:栈顶和栈低,入栈和出栈。
如图是入栈从方法1--》调用方法2入栈---》调用方法3入栈--》调用方法4入栈,当方法4执行后从栈顶弹出然后方法3--》方法2--》方法1执行完毕弹出虚拟机栈,线程执行方法结束。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
执行引擎运行的所有的字节码指令都是针对当前栈帧进行操作的。
局部变量表:
JVM字节码之整型iconst、istore、iload指令 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。
从istore_1开始
istore_0 = this
操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
iconst从0开始
当int取值 -128~127 时,JVM采用 bipush 指令将常量压入栈中
iload:加载局部变量;非静态方法从iload_1开始的,默认第iload_0是this,静态方法iload_0
动态连接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址:
种情景:一是正常执行完成后退出,二是出现未处理的以长,非正常退出。无论哪种退出方式,方法退出后都会返回该方法的调用位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
2.Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)
分别是: 加载--验证--准备--解析--初始化
1>加载:
加载有两种情况,①当遇到new关键字,或者static关键字的时候就会发生(他们对应着对应的指令)如果在常量池中找不到对应符号引用时,就会发生加载 ,②动态加载,当用反射方法(如class.forName(“类名”)),如果发现没有初始化,则要进行初始化。(注:加载的时候发现父类没有被加载,则要先加载父类)
2> 验证:
这一阶段的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全(虽然编译器会严格的检查java代码并生成class文件,但是class文件不一定都是通过编译器编译,然后加载进来的,因为虚拟机获取class文件字节流的方式有可能是从网络上来的,者难免不会存在有人恶意修改而造成系统崩溃的问题,class文件其实也可以手写16进制,因此这是必要的)
3>准备:
该阶段就是为对象分派内存空间,然后初始化类中的属性变量,但是该初始化只是按照系统的意愿进行初始化,也就是初始化时都为0或者为null。因此该阶段的初始化和我们常说初始化阶段的初始化时不一样的
4>解析:
解析就是虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用其实就是class文件常量池中的各种引用,他们按照一定规律指向了对应的类名,或者字段,但是并没有在内存中分配空间,因此符号就理解为一个标示,而用直接指向内存中的地址
5>初始化:
简单讲就是执行对象的构造函数,给类的静态字段按照程序的意愿进行初始化,注意初始化的顺序。(此处的初始化由两个函数完成,一个是,初始化所有的类变量(静态变量),该函数不会初始化父类变量,还有一个是实例初始化函数,对类中实例对象进行初始化,此时要如果有需要。
3.对象的访问定位的两种方式(句柄和直接指针两种方式)
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:
1>句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
4.如何判断对象是否死亡(两种方法)。
有以下两种算法判断对象实例是否死亡:
1、引用计数算法:给每个对象添加一个引用计数器,当有对象引用时加1,当引用失效时减1,任何引用计数器为0的对象实例就是不可能再被使用的——对象实例死亡。但它无法解决对象相互引用的情况。
2、可达性分析算法:通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则说明此对象不能再被使用——对象实例已死亡。可作为GC Roots的对象包括:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区常量引用的对象,本地方法栈中本地方法引用的对象。
5.简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好 处)。
1.强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfM moryError错误,使程序异常终止,也不会靠随意回收具有强引用 对象来解决内存不足的问题。
2.软引用
软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
3.弱引用
弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
4.虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null,弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
6.如何判断一个常量是废弃常量
引用计数器,可达性分析算法
7.如何判断一个类是无用的类
1》所有实例都被回收;2》加载该类的classloader被回收;3》该类对象没有在任何地方引用
8.垃圾收集有哪些算法,各自的特点?待续
9.HotSpot为什么要分为新生代和老年代?
10.常见的垃圾回收器有那些?
11.介绍一下CMS,G1收集器。
12.Minor Gc和Full GC 有什么不同呢?
拓展问题:
1.String类和常量池
2.8种基本类型的包装类和常量池
栈FILO 现金后出 fist in last out