java虚拟机面试总结.docx
- 文档编号:8060594
- 上传时间:2023-01-28
- 格式:DOCX
- 页数:26
- 大小:37.47KB
java虚拟机面试总结.docx
《java虚拟机面试总结.docx》由会员分享,可在线阅读,更多相关《java虚拟机面试总结.docx(26页珍藏版)》请在冰豆网上搜索。
java虚拟机面试总结
第一章JVM内存模型
Java虚拟机(JavaVirtualMachine=JVM)的内存空间分为五个部分,分别是:
1,程序计数器
2,Java虚拟机栈
3,本地方法栈
4,堆
5,方法区。
下面对这五个区域展开深入的介绍。
1.1程序计数器
1.1.1什么是程序计数器?
程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。
也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。
注:
但是,如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。
1.1.2程序计数器的作用
程序计数器有两个作用:
1,字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:
顺序执行、选择、循环、异常处理。
2,在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
1.1.3程序计数器的特点
1,是一块较小的存储空间
2,线程私有。
每条线程都有一个程序计数器。
3,是唯一一个不会出现OutOfMemoryError的内存区域。
4,生命周期随着线程的创建而创建,随着线程的结束而死亡。
1.2Java虚拟机栈(JVMStack)
1.2.1什么是Java虚拟机栈?
Java虚拟机栈是描述Java方法运行过程的内存模型。
Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:
1.局部变量表存放基本数据类型变量、引用类型的变量、returnAddress类型的变量。
1,操作数栈
2,动态链接
3,方法出口信息
当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈帧”,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。
当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。
当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。
注意:
人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!
这里的“堆”可以这么理解,但这里的“栈”只代表了Java虚拟机栈中的局部变量表部分。
真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:
局部变量表、操作数栈、动态链接、方法出口信息。
1.2.2Java虚拟机栈的特点
(1)局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。
而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。
此外,在方法运行的过程中局部变量表的大小是不会发生改变的。
(2)Java虚拟机栈会出现两种异常:
StackOverFlowError和OutOfMemoryError。
∙a)StackOverFlowError:
若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
∙b)OutOfMemoryError:
若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
(3)Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
注:
StackOverFlowError和OutOfMemoryError的异同?
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。
而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。
1.3本地方法栈
1.3.1什么是本地方法栈?
本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模型。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间。
也会抛出StackOverFlowError和OutOfMemoryError异常。
1.4堆
1.4.1什么是堆?
堆是用来存放对象的内存空间。
几乎所有的对象都存储在堆中。
1.4.2堆的特点
(1)线程共享
整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。
而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个的。
(2)在虚拟机启动时创建。
(3)垃圾回收的主要场所。
(4)可以进一步细分为:
新生代、老年代。
新生代又可被分为:
Eden、FromSurvior、ToSurvior。
不同的区域存放具有不同生命周期的对象。
这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。
(5)堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError。
1.5方法区
1.5.1什么是方法区?
Java虚拟机规范中定义方法区是堆的一个逻辑部分。
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
1.5.2方法区的特点
1.线程共享方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。
整个虚拟机中只有一个方法区。
2.永久代方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为老年代。
3.内存回收效率低方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。
对方法区的内存回收的主要目标是:
对常量池的回收和对类型的卸载。
4.Java虚拟机规范对方法区的要求比较宽松。
和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。
1.5.3什么是运行时常量池?
方法区中存放三种数据:
类信息、常量、静态变量、即时编译器编译后的代码。
其中常量存储在运行时常量池中。
我们一般在一个类中通过publicstaticfinal来声明一个常量。
这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。
当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。
而且在运行期间,可以向常量池中添加新的常量。
如:
String类的intern()方法就能在运行期间向常量池中添加字符串常量。
当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。
1.6直接内存
直接内存是除Java虚拟机之外的内存,但也有可能被Java使用。
在NIO中引入了一种基于通道和缓冲的IO方式。
它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。
直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OOM异常。
1.7综上所述
1.Java虚拟机的内存模型中一共有两个“栈”,分别是:
Java虚拟机栈和本地方法栈。
两个“栈”的功能类似,都是方法运行过程的内存模型。
并且两个“栈”内部构造相同,都是线程私有。
只不过Java虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈是描述Java本地方法运行过程的内存模型。
2.Java虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。
方法区本质上是属于堆的一个逻辑部分。
堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译的代码。
3.堆是Java虚拟机中最大的一块内存区域,也是垃圾收集器主要的工作区域。
4.程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法栈。
并且他们的生命周期和所属的线程一样。
而堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。
并在JVM启动的时候就创建,JVM停止才销毁。
第二章揭开Java对象创建的奥秘
2.1对象的创建过程
当虚拟机遇到一条含有new的指令时,会进行一系列对象创建的操作:
(1)检查常量池中是否有即将要创建的这个对象所属的类的符号引用;
∙若常量池中没有这个类的符号引用,说明这个类还没有被定义!
抛出ClassNotFoundException;
∙若常量池中有这个类的符号引用,则进行下一步工作;
(2)进而检查这个符号引用所代表的类是否已经被JVM加载;
∙若该类还没有被加载,就找该类的class文件,并加载进方法区;
∙若该类已经被JVM加载,则准备为对象分配内存;
(3)根据方法区中该类的信息确定该类所需的内存大小;
一个对象所需的内存大小是在这个对象所属类被定义完就能确定的!
且一个类所生产的所有对象的内存大小是一样的!
JVM在一个类被加载进方法区的时候就知道该类生产的每一个对象所需要的内存大小。
(4)从堆中划分一块对应大小的内存空间给新的对象;分配堆中内存有两种方式:
∙指针碰撞如果JVM的垃圾收集器采用复制算法或标记-整理算法,那么堆中空闲内存是完整的区域,并且空闲内存和已使用内存之间由一个指针标记。
那么当为一个对象分配内存时,只需移动指针即可。
因此,这种在完整空闲区域上通过移动指针来分配内存的方式就叫做“指针碰撞”。
∙空闲列表如果JVM的垃圾收集器采用标记-清除算法,那么堆中空闲区域和已使用区域交错,因此需要用一张“空闲列表”来记录堆中哪些区域是空闲区域,从而在创建对象的时候根据这张“空闲列表”找到空闲区域,并分配内存。
综上所述:
JVM究竟采用哪种内存分配方法,取决于它使用了何种垃圾收集器。
(5)为对象中的成员变量赋上初始值(默认初始化);
(6)设置对象头中的信息;
(7)调用对象的构造函数进行初始化;
此时,整个对象的创建过程就完成了。
2.2对象的内存模型
一个对象从逻辑角度看,它由成员变量和成员函数构成,从物理角度来看,对象是存储在堆中的一串二进制数,这串二进制数的组织结构如下。
对象在内存中分为三个部分:
1.对象头
2.实例数据
3.对齐补充
2.2.1对象头
对象头中记录了对象在运行过程中所需要使用的一些数据:
哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
此外,对象头中可能还包含类型指针。
通过该指针能确定这个对象所属哪个类。
此外,如果对象是一个数组,那么对象头中还要包含数组长度。
2.2.2实例数据
实力数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。
2.2.3对齐补充
用于确保对象的总长度为8字节的整数倍。
HotSpot要求对象的总长度必须是8字节的整数倍。
由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的,因此需要对齐补充字段确保整个对象的总长度为8的整数倍。
2.3访问对象的过程
我们知道,引用类型的变量中存放的是一个地址,那么根据地址类型的不同,对象有不同的访问方式:
1.句柄访问方式堆中需要有一块叫做“句柄池”的内存空间,用于存放所有对象的地址和所有对象所属类的类信息。
引用类型的变量存放的是该对象在句柄池中的地址。
访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址再访问对象。
2.直接指针访问方式引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。
但对象所在的内存空间中需要额外的策略存储对象所属的类信息的地址。
比较
HotSpot采用直接指针方式访问对象,因为它只需一次寻址操作,从而性能比句柄访问方式快一倍。
但它需要额外的策略存储对象在方法区中类信息的地址。
第三章揭开Java对象内存分配的秘密
Java所承诺的自动内存管理主要是针对对象内存的回收和对象内存的分配。
在Java虚拟机的五块内存空间中,程序计数器、Java虚拟机栈、本地方法栈内存的分配和回收都具有确定性,一半都在编译阶段就能确定下来需要分配的内存大小,并且由于都是线程私有,因此它们的内存空间都随着线程的创建而创建,线程的结束而回收。
也就是这三个区域的内存分配和回收都具有确定性。
而Java虚拟机中的方法区因为是用来存储类信息、常量静态变量,这些数据的变动性较小,因此不是Java内存管理重点需要关注的区域。
而对于堆,所有线程共享,所有的对象都需要在堆中创建和回收。
虽然每个对象的大小在类加载的时候就能确定,但对象的数量只有在程序运行期间才能确定,因此堆中内存的分配具有较大的不确定性。
此外,对象的生命周期长短不一,因此需要针对不同生命周期的对象采用不同的内存回收算法,增加了内存回收的复杂性。
综上所述:
Java自动内存管理最核心的功能是堆内存中对象的分配与回收。
3.1对象优先在Eden区中分配
目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代。
在新生代中为了防止内存碎片问题,因此垃圾收集器一般都选用“复制”算法。
因此,堆内存的新生代被进一步分为:
Eden区+Survior1区+Survior2区。
每次创建对象时,首先会在Eden区中分配。
若Eden区已满,则在Survior1区中分配。
若Eden区+Survior1区剩余内存太少,导致对象无法放入该区域时,就会启用“分配担保”,将当前Eden区+Survior1区中的对象转移到老年代中,然后再将新对象存入Eden区。
3.2大对象直接进入老年代
所谓“大对象”就是指一个占用大量连续存储空间的对象,如数组。
当发现一个大对象在Eden区+Survior1区中存不下的时候就需要分配担保机制把当前Eden区+Survior1区的所有对象都复制到老年代中去。
我们知道,一个大对象能够存入Eden区+Survior1区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及到大量的复制,就会造成效率低下。
因此,对于大对象我们直接把他放到老年代中去,从而就能避免大量的复制操作。
那么,什么样的对象才是“大对象”呢?
通过-XX:
PretrnureSizeThreshold参数设置大对象
该参数用于设置大小超过该参数的对象被认为是“大对象”,直接进入老年代。
注意:
该参数只对Serial和ParNew收集器有效。
3.3生命周期较长的对象进入老年代
老年代用于存储生命周期较长的对象,那么我们如何判断一个对象的年龄呢?
新生代中的每个对象都有一个年龄计数器,当新生代发生一次MinorGC后,存活下来的对象的年龄就加一,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。
使用-XXMaxTenuringThreshold设置新生代的最大年龄
设置该参数后,只要超过该参数的新生代对象都会被转移到老年代中去。
3.4相同年龄的对象内存超过Survior内存一半的对象进入老年代
如果当前新生代的Survior中,年龄相同的对象的内存空间总和超过了Survior内存空间的一半,那么所有年龄相同的对象和超过该年龄的对象都被转移到老年代中去。
无需等到对象的年龄超过MaxTenuringThreshold才被转移到老年代中去。
3.5“分配担保”策略详解
当垃圾收集器准备要在新生代发起一次MinorGC时,首先会检查“老年代中最大的连续空闲区域的大小是否大于新生代中所有对象的大小?
”,也就是老年代中目前能够将新生代中所有对象全部装下?
若老年代能够装下新生代中所有的对象,那么此时进行MinorGC没有任何风险,然后就进行MinorGC。
若老年代无法装下新生代中所有的对象,那么此时进行MinorGC是有风险的,垃圾收集器会进行一次预测:
根据以往MinorGC过后存活对象的平均数来预测这次MinorGC后存活对象的平均数。
如果以往存活对象的平均数小于当前老年代最大的连续空闲空间,那么就进行MinorGC,虽然此次MinorGC是有风险的。
如果以往存活对象的平均数大于当前老年代最大的连续空闲空间,那么就对老年代进行一次FullGC,通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。
这个过程就是分配担保。
注意:
1.分配担保是老年代为新生代作担保;
2.新生代中使用“复制”算法实现垃圾回收,老年代中使用“标记-清除”或“标记-整理”算法实现垃圾回收,只有使用“复制”算法的区域才需要分配担保,因此新生代需要分配担保,而老年代不需要分配担保。
第四章了解Java虚拟机的垃圾回收算法
Java虚拟机的内存模型分为五个部分,分别是:
程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。
这五个区域既然是存储空间,那么为了避免Java虚拟机在运行期间内存存满的情况,就必须得有一个垃圾收集者的角色,不定期地回收一些无效内存,以保障Java虚拟机能够健康地持续运行。
这个垃圾收集者就是平常我们所说的“垃圾收集器”,那么垃圾收集器在何时清扫内存?
清扫哪些数据?
这就是接下来我们要解决的问题。
程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁。
那么,垃圾收集器在何时清扫这三块区域的问题就解决了。
此外,Java虚拟机栈、本地方法栈中的栈帧会随着方法的开始而入栈,方法的结束而出栈,并且每个栈帧中的本地变量表都是在类被加载的时候就确定的。
因此以上三个区域的垃圾收集工作具有确定性,垃圾收集器能够清楚地知道何时清扫这三块区域中的哪些数据。
然而,堆和方法区中的内存清理工作就没那么容易了。
堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时。
因此它们没办法根据线程的创建而创建、线程的结束而释放。
堆中存放JVM运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定。
方法区中存放类信息、静态成员变量、常量。
类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类。
因此,JVM究竟要加载多少个类也需要在程序运行期间确定。
因此,堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一些心思。
4.1堆内存的回收
4.1.1如何判定哪些对象需要回收?
在对堆进行对象回收之前,首先要判断哪些是无效对象。
我们知道,一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收。
一般有两种判别方式:
∙引用计数法每个对象都有一个计数器,当这个对象被一个变量或另一个对象引用一次,该计数器加一;若该引用失效则计数器减一。
当计数器为0时,就认为该对象是无效对象。
∙可达性分析法所有和GCRoots直接或间接关联的对象都是有效对象,和GCRoots没有关联的对象就是无效对象。
GCRoots是指:
1.Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象)
2.方法区中静态属性引用的对象
3.方法区中常量所引用的对象
4.本地方法栈所引用的对象
两者对比:
引用计数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题。
因此,目前主流语言均使用可达性分析方法来判断对象是否有效。
4.1.2回收无效对象的过程
当JVM筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会,具体过程如下:
(1)判断该对象是否覆盖了finalize()方法
∙若已覆盖该方法,并该对象的finalize()方法还没有被执行过,那么就会将finalize()扔到F-Queue队列中;
∙若未覆盖该方法,则直接释放对象内存。
(2)执行F-Queue队列中的finalize()方法虚拟机会以较低的优先级执行这些finalize()方法们,也不会确保所有的finalize()方法都会执行结束。
如果finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除。
(3)对象重生或死亡如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。
如果没有,那么就会被垃圾收集器清除。
注意:
强烈不建议使用finalize()函数进行任何操作!
如果需要释放资源,请使用try-finally。
因为finalize()不确定性大,开销大,无法保证顺利执行。
4.2方法区的内存回收
我们知道,如果使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代,新生代中的对象“朝生夕死”,每次垃圾回收都会清除掉大量的对象;而老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除掉。
由于方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。
方法区中主要清除两种垃圾:
1.废弃常量
2.废弃的类
4.2.1如何判定废弃常量?
清除废弃的常量和清除对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。
4.2.2如何废弃废弃的类?
清除废弃类的条件较为苛刻:
1.该类的所有对象都已被清除
2.该类的java.lang.Class对象没有被任何对象或变量引用只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:
java.lang.Class。
这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。
3.加载该类的ClassLoader已经被回收
4.3垃圾收集算法
现在我们知道了判定一个对象是无效对象、判定一个类是废弃类、判定一个常量是废弃常量的方法,也就是知道了垃圾收集器会清除哪些数据,那么接下来介绍如何清除这些数据。
4.3.1标记-清除算法
首先利用刚才介绍的方法判断需要清除哪些数据,并给它们做上标记;然后清除被标记的数据。
分析:
这种算法标记和清除过程效率都很低,而且清除完后存在大量碎片空间,导致无法存储大对象,降低了空间利用率。
4.3.2复制算法
将内存分成两份,只将数据存储在其中一块上。
当需要回收垃圾时,也是首先标记出废弃的数据,然后将有用的数据复制到另一块内存上,最后将第一块内存全部清除。
分析:
这种算法避免了碎片空间,但内存被缩小了一半。
而且每次都需要将有用的数据全部复制到另一片内存上去,效率不高。
解决空间利用率问题:
在新生代中,由于大量的对象都是“朝生夕死”,也就是一次垃圾收集后只有少量对象存活,因此我们可以将内存划分成三块:
Eden、Survior1、Survior2,内存大小分别是8:
1:
1。
分配内存时,只使用Eden和一块Survior1。
当发现Eden+Survior1的内存即将满时,JVM会发起一次MinorGC,清除掉废弃的对象,并将所有存活下来的对象复制到另一块Survior2中。
那么,接下来就使用Survior2+Eden进行内存分配。
通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题。
但是,当一个对象要申请内存空间时,发现Eden+Survior中剩下的空间无法放置该对象,此时需要进行MinorGC,如果MinorGC过后空闲出来的内存空间仍然无法放置该对象,那么此时就需要将对象转移到老年代中,这种方式叫做“分配担保”。
什么是分配担保?
当JV
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- java 虚拟机 面试 总结