关于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):
程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
也叫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,2,3,4...这些就是指令地址,而他后面跟着的就是操作指令。程序计数器就相当于一个临时空间存储将要执行的指令地址,当该指令地址对应的操作指令被执行引擎解释并执行后存储下一个指令地址。
要想让计算机执行程序,需要让执行引擎中的解释器将字节码操作指令解释成CPU能够识别的机器指令。
而选取哪一条操作指令进行解释并执行,这个时候就需要依赖于程序计数器了。可以把它想象成一个临时空间,用于存储字节码操作指令的指令地址。
本图中,0 就是一个指令地址,通过指令地址就能够找到哪条指令,说明当前需要选取执行的操作指令是:iconst_1。
如果执行完0后,需要执行指向1的这条指令,那么将程序计数器(PC计数器)中存储的指令地址改成1就行了。
注意:当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
2).虚拟机栈
内存栈 FILO
描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。
内存栈 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计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本地方法栈
区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务
本地方法栈:
本地方法就是java调用非java代码的接口
存在意义:与java外环境交互、与OS交互、Sun`s java
本地方法栈
用于管理本地方法的调用,这是和JVM栈唯一的区别
当一个线程调用本地方法时,他就进入了一个全新的不再受虚拟机限制的世界,他和JVM有同样的权限。
本地方法可以访问JVM运行时数据区,可以直接使用寄存器,可以直接从堆中分配任意数量的内存。
Hotspot虚拟机中,直接把本地方法栈和虚拟机栈二合一
方法区(Method Area)
方法区用于储存已被虚拟机加载的类信息、常量、静态变量
可以看做堆的一个逻辑部分
一张图直观的了解栈,堆和方法区的关系
方法区可以看作是独立于Java堆的一部分,并且也是和堆一样是整个JVM实例共用一份。
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息:class 文件的类信息
方法区需要存储每个加载的类(类,接口,枚举,注解)的以下类型信息:
完整名称(包类.类名)
这个类的直接父类的完整名称(接口和java.long.object没有父类)
这个类型的修饰符(public,abstract,final的某个子集)
这个类型直接接口的一个有序列表
域(属性)信息:class文件中的Field信息
JVM需要保存类型的域信息和域的声名顺序
域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
方法信息
JVM需要保存所有方法的信息及其声明的顺序
方法的名称,返回类型,参数(数量类型,按顺序),修饰符
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
异常表(abstract和native方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
non-final的类变量(static)
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
补充说明:全局常量(static final)被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
运行时常量池和常量池
运行时常量池:在方法区中
常量池:字节码文件中的一部分 。
当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
运行时常量池
方法区的一部分
class文件中除了有关的版本、字段、方法、接口等描述信息外、还有一项信息是常量池,用于存放编辑期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
全局字符串池(string pool也有叫做string literal pool)
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
class文件常量池(class constant pool)
字节码文件中有个constant pool,就是常量池。
常量池中存储的符号引用,在程序运行的时候,会被转换为直接引用。
一个java源文件中的类、接口,编译后会产生一个字节码文件,而java中的字节码文件需要其他的数据支撑,通常这种数据很大,不能直接存放到字节码里面。所以把对这些数据的引用存放到常量池,在真正需要使用的时候,通过动态链接将符号引用转换为直接引用。
当字节码文件被加载到内存中之后,方法区中会存放字节码文件的constant pool相关信息,这时候就成为了运行时常量池。
常量池保存了各种字面量和对类型、域和方法的符号引用。
堆
内存中最大的一块
Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都在堆上分配。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续是连续的即可。若在堆中没有完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
HotSpot中方法区的演进
在JDK 8以前,许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代”(Permanent Generation),或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设 计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现,譬如BEA JRockit、IBM J9等来说,是不存在永久代的概念的。
在Oracle收购了JRockit后就决定将JRockit的优点移植到HotSpot上,但因为两者对方法区实现的差异而面临诸多困难。在JDK6时就有放弃永久代,该用本地内存实现方法区的计划了。
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.简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好 处)。
从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期,这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
1.强引用
强引用是最普遍的引用,一般把一个对象赋给一个引用变量,这个引用变量就是强引用。
// 强引用
MikeChen mikechen=new MikeChen();
在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(MikeChen)保存在Java堆中。
如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError异常。
如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:
//帮助垃圾收集器回收此对象
mikechen=null;
显式地设置mikechen对象为null,或让其超出对象的生命周期范围,则GC认为该对象不存在引用,这时就可以回收这个对象,具体什么时候收集这要取决于GC算法。
2.软引用
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference 类来实现。
String str=new String("abc"); // 强引用
SoftReference softRef=new SoftReference(str); // 软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
Object obj = new Object();
SoftReference softRef = new SoftReference
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用
弱引用的使用和软引用类似,只是关键字变成了 WeakReference:
Object obj = new Object();
WeakReference
弱引用的特点是不管内存是否足够,只要发生 GC,都会被回收。
ReferenceQueue queue = new ReferenceQueue<>();//引用队列
Object obj = new Object();
WeakReference weakRef = new WeakReference(obj);//弱引用
obj = null;//删除强引用
System.gc();//调用gc
System.out.println("gc之后的值:" + weakRef.get());// 对象依然存在
//申请较大内存使内存空间使用率达到阈值,强迫gc
byte[] bytes = new byte[5000 * 1024 * 1024];//如果obj被回收,则软引用会进入引用队列
if (queue != null){
System.out.println("对象已被回收: "+weakRef.get() ); // 对象为null
}
4.虚引用
虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。
虚引用需要java.lang.ref.PhantomReference 来实现:
A a = new A();
ReferenceQueue rq = new ReferenceQueue();
PhantomReference prA = new PhantomReference(a, rq);
虚引用主要用来跟踪对象被垃圾回收器回收的活动。它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
6.如何判断一个常量是废弃常量
引用计数器,可达性分析算法
7.如何判断一个类是无用的类1》所有实例都被回收;2》加载该类的classloader被回收;3》该类对象没有在任何地方引用
栈FILO 现金后出 fist in last out