jvm面试问题,jvm面试题总结及答案
JVM简介JVM是Java虚拟机的缩写,意为Java虚拟机。
虚拟机是指由软件模拟的,具有完整硬件功能,在完全隔离的环境下运行的完整计算机系统。
常见的虚拟机:JVM、VMwave、虚拟盒子。
JVM和其他两个虚拟机的区别:
1.VMwave和VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多寄存器;
2.JVM是通过软件模拟Java字节码的指令集。JVM中只保留PC寄存器,其他寄存器被切掉。
JVM是现实中不存在的定制计算机。
日常开发中,Java程序员一般不会用到JVM内部的东西。如果想有更深入的了解,可以看看这本书,里面有很多干货。
1.JVM存储区分区
JVM的内存是从操作系统申请的,内存分为不同的区域,不同的区域完成不同的功能。
什么是线程私有?
由于JVM的多线程是通过线程轮流切换和分配处理器执行时间的方式实现的,所以在任何给定的时刻,一个处理器(多核处理器指的是一个内核)只会执行一个线程中的指令。所以为了在切换线程后恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,每个线程的计数器互不影响,独立存储。我们称这样的区域为“线程私有”内存。
1.1程序计数器(thread private)程序计数器函数:用于记录当前线程执行的行号。
它是内存中最小的区域,是下一条要执行的指令的地址…
它是指令字节码。如果一个程序想要运行,JVM必须加载字节码并把它放在内存中。程序会把指令一条一条从内存中取出,放到CPU上执行,也就是要记住当前执行的是哪一条。
CPU是一个并发执行进程。它不仅为一个进程提供服务,而且为所有进程服务。只是因为操作系统是按线程调度执行的,所以每个线程都要记录它的执行位置,也就是程序计数器,每个线程都有一个。
1.2 Java虚拟机堆栈(线程私有)描述局部变量和方法调用信息。当一个方法被调用时,每调用一个新方法,就涉及到‘推入’操作,每执行一个方法,就涉及到‘推出’操作。
堆栈空间比较小,在JVM中可以配置堆栈空间的大小,但是通常只有几米或者几十米,所以堆栈很可能是满的(通常我们写代码的时候害怕递归,一旦不设置递归条件就会出现堆栈溢出:StackOverflowException)
Java虚拟机栈的作用:Java虚拟机栈的生命周期与线程的生命周期相同,Java虚拟机栈描述了Java方法的执行。
内存模型:每个方法在执行时都会创建一个堆栈框架来存储局部变量表、操作数堆栈、动态链接、方法出口等信息。在堆内存和栈内存中,栈内存指的是虚拟机栈。
Java虚拟机堆栈包含以下四个部分:
局部变量表:存储编译器已知的各种基本数据类型(八种基本数据类型)和对象引用。局部变量表所需的内存空间是在编译过程中分配的。在进入一个方法时,这个方法需要在框架中分配多少局部变量空间是完全确定的,在执行过程中局部变量表的大小不会改变。简单来说就是存储方法参数和局部变量。操作:每个方法生成一个先进后出的操作堆栈。动态链接:对运行时常量池的方法引用。方法返回地址:PC寄存器的地址1.3本地方法栈(线程私有)本地方法栈与虚拟机栈类似,只是Java虚拟机栈由JVM使用,而本地方法栈由本地方法使用。
1.4堆(线程共享)堆的作用:程序中创建的所有对象都保存在堆中。
一个进程只有一个副本,多个线程共享一个堆,堆也是内存中空间最大的区域。新创建的对象在堆中,它的成员变量自然也在堆中。
注意:说内置类型的变量在栈上,引用类型的变量在堆上是不对的。
应该是栈上的局部变量,堆上的成员变量和新对象。
1.5方法区(线程共享)方法区的功能:用于存储虚拟机加载的即时编译器编译的类信息、常量、静态变量、代码等数据。
在方法区域,有“类对象”,即所谓的“类对象”:代码如。我们写的java会变成。类(二进制字节码),而。类会被加载到内存中,内存是JVM构造的类对象(加载过程称为‘类加载’)。“类对象”描述了这个类是什么样子,这个类的名字是什么,里面有什么。每个成员的名称和类型是什么(公共/私有…),每个方法的名称和类型是什么(公共/私有…),以及方法中包含的指令…
‘类对象’里还有一个很重要的东西,静态成员。
由static修饰的成员成为‘类属性’,而普通成员称为‘实例属性’。
2.JVM类加载机制类加载实际上是设计运行时环境的一个重要核心功能。类加载是做什么的?他装上了。类文件放入内存并构建类对象。
2.1类加载流程类加载生命周期:
前五步是有固定顺序的,也是类加载的过程,中间三步属于链接,所以对于类加载,有三步:加载,加载,链接,初始化(回答别人的时候尽量用英语)。
2.1.1加载“加载”阶段是整个“类加载”过程中的一个阶段,它不同于类加载,一个是加载,一个是类加载,所以不要混淆两者。
在加载阶段,Java虚拟机需要完成以下三件事:
1)获取二进制字节流,该字节流通过类的完全限定名来定义类。
2)将这个字节流表示的静态存储结构转换成方法区的运行时数据结构。
3)在内存中生成一个表示这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。
总结就是找到对应的。类文件,然后打开并阅读。类文件(字节流)并最初生成一个类对象。
装货的关键环节是什么?像这样的类文件?
详见官方文档:https://docs . Oracle . com/javase/specs/JVMs/se8/html/JVMs-4 . html。
按照上图的格式,读取并解析的信息会初步填充到类对象中。
2.1.2链接连接一般是建立多个实体之间的连接。
答:验证(Verification)
主要目的是验证读取的内容是否与规范中规定的格式完全匹配。如果发现这里读取的数据格式不符合规范,那么类加载将会失败并抛出异常。
二:准备(Preparation)
准备阶段是为类中定义的变量(即静态变量、由static修饰的变量)正式分配内存并设置类变量初始值的阶段。
例如:
公共静态int值=123;
他用int值0而不是123来初始化这个值。
第三:决心
解析阶段是Java虚拟机将常量池中的符号引用替换为直接引用的过程,也就是初始化常量的过程。
这意味着在。类文件中,常量放在中央,每个常量都有一个编号。在的结构中。类文件,最初只记录了编号,所以需要根据编号找到相应的内容,填入类对象中。
2.1.3初始化(Initializing)在初始化阶段,Java虚拟机真正开始执行类中编写的Java程序代码,把主导权交给应用程序。初始化阶段是执行类构造函数方法的过程,是类对象的真正初始化,尤其是对于静态成员。
典型面试问题:什么时候触发一个类的加载(代码示例)?
他的印刷顺序是什么?
结果:
只要使用这个类,就必须先加载(比如实例化,调用方法,调用静态方法,被继承…都用)
大原则:
1.静态代码块将在类加载阶段执行。如果要创建实例,必须先加载类;
2.静态代码块在类加载阶段只执行一次。
3.构造方法和构造代码块,每次实例化都会执行,构造代码块在构造方法之前。
4:父类先执行,子类最后执行。
5:我们的程序是从main执行的,这是Test的方法,所以要执行main,需要先加载TestDemo。
我们的程序是从main方法执行的。这里主要是这个类的方法TestDemo。所以要先执行main,需要先加载TestDemo,TestDemo继承B,要加载TestDemo,必须先加载B,B继承A,先加载A。
2.2父母委托模式在我们的工作中不是很有用,但在面试中经常被问到…
这个东西是类加载中的一个环节,在加载阶段(前期)。父委托模型实际上是JVM中的类装入器,以及如何找到。根据完全限定名(java.lang.String)创建类文件。
类加载器:JVM中提供了一个叫做类加载器的特殊对象,负责类的加载。当然,查找文件的过程也是由类加载器处理的…。类文件可以放在很多地方,有的在JDK目录下,有的在项目目录下,有的在其他特定的位置等等。因此,JVM中提供了多个类装入器,每个类装入器负责一个领域…
有三种默认的类装入器:
1: BootstrapClassLoader
负责加载标准库中的类(string,ArrayList,random,scanner …)
2:扩展类加载器
负责加载JDK扩展的类(现在很少使用)
3:应用程序类加载器
负责加载当前项目目录中的类。
此外,程序员还可以自定义类加载器来加载其他目录中的类。例如,Tomcat定制了类加载器来专门加载。webapps中的类…
我们的母代理模型描述了这个目录搜索过程,也就是上面的类加载器是如何协作的…
这套搜索规则被称为‘父母委托模型’(这是音译,父母可以是父亲也可以是母亲。按规矩,叫他‘单亲委托模式’也不是不可以。当然不是由我们来命名)
JVM为什么要这样设计?
原因是一旦程序员编写的类和标准库中的类之间完全限定类名重复,就可以成功加载到标准库中的类中!
java.lang.String这样的类是我们自己定义的。如果程序加载了,还是标准库中的类,这样就不会有冲突,安全性也有保证。
如果自定义类装入器,是不是也要遵循父母委托模型?
可以遵守,也可以不遵守,看需求。
就像Tomcat在webapps中加载类一样,不遵从(因为遵从没有意义)。
3.JVM垃圾收集机制(GC)3.1什么是垃圾收集(GC)?我们写代码的时候,经常会申请内存,创建变量,新对象,加载类……这些都是在申请内存,都是来自操作系统。既然申请了内存,那么在不使用的时候也必须归还。
一般来说,申请内存的时间是明确的(如果需要保存一些数据,需要申请内存),但是释放内存的时间就没那么明确了。我们不知道我们是否需要这段记忆。
举个例子,假设你下午回家就把衣服扔了,你就当没看见。你妈妈发现后,会把你的衣服整理好,放在衣柜里。第二天,如果你不穿它们,但你仍然要穿这件衣服出去,你可以在你原来的位置上找它们,嗯?没了,是不是很尴尬……(这是记忆的提前释放)
以后发布可以吗?也不是很好。就像你在图书馆占了一个座位,一大早就占了,结果一整天都没去。那不是也很尴尬吗?如果你占了座位,你就不能用,别人也不能用(所以内存释放的比较晚)
我们想要的是既不太早也不太晚。
3.2为什么会有类似C语言的垃圾收集机制:我不管内存释放,你们程序员自己做就行了,反正你们也不会扣我的钱……所以,在C语言中,你会遇到一个常见的头疼事=内存泄漏(申请后忘记释放)=可用内存越来越少,最后没有可用内存!所以‘内存泄漏’是C/C程序员很头疼的问题。有的漏得快,有的漏得慢,暴露时间不确定。如果出现,就很难查了。C后来提出了智能指针(可能只是简单的依靠C中的RAII机制,但一点也不智能.)这可以在一定程度上降低“内存泄漏”的风险.但是
所以像Java,GO,PHP…现在市面上大多数主流编程语言都采用了一种方案,就是垃圾收集机制!
大概有一个运行时环境(比如JVM、Python解释器、Go runtime……)通过更复杂的策略来判断内存是否可以回收,并执行回收动作……垃圾回收,本质上是依赖于运行时环境,做了很多额外的工作来完成自动释放内存的操作,大大减轻了程序员的精神负担。
然而,垃圾收集也有缺点:
1.消耗额外开销(消耗更多资源)
2.它可能会影响程序的顺利运行(垃圾收集经常会引入STW(停止世界,就像时间静止不动)问题)
回收这么香,C为什么不引入GC?
其实也有大佬提出过这个方案,只是没有实现,因为C语言有两条高压线,这是他的核心原理:
1.兼容C语言,也可以最大化兼容各种硬件和操作系统。
2:极致的性能…
如人工智能、游戏引擎、高性能服务器、操作系统内核.对于兼容性/性能要求极高的场景,C/C还是必要的。
3.3垃圾回收要回收的是内存,但内存包括:程序计数器、堆栈、堆和方法,有些回收,有些不回收:
程序:固定大小,不涉及释放,所以不需要GC。
堆栈:函数执行后,对应的堆栈框架会自动释放,所以不需要GC。
堆:需要GC,代码中的大量内存都在堆上。
方法:类对象,类被加载,‘类卸载’需要释放内存。卸载其实是一个很低频的操作(很少涉及垃圾收集)。
这里我们将讨论堆上的垃圾收集。
首先来看看这张图:
上图可以理解为三派:正面派,负面派,中间摇摆派,
活动pie:正在使用的内存不会被释放。
消极派:不用的内存必须释放。
中间摇摆派:红蓝之间的代表,一部分在用,一部分不用。鉴于这种情况,它们在用完和停止使用之前不会被释放。
GC中不会有‘半个对象’,主要是为了让垃圾收集更加方便简单。记住:垃圾回收的基本单位是‘对象’,不是字节。
3.4具体来说,如何实现垃圾收集可以分为两大阶段,第一阶段:发现垃圾/判断垃圾.第二阶段:释放垃圾。
这就像打扫房间,先把垃圾全部清理进垃圾桶,再把垃圾扔出房间…
3.4.1如何发现垃圾/确定垃圾?我们目前的主流想法有两种方案:
1.基于引用计数(不是Java采用的方案,这是另一种语言,像Python采用的方案)
2.基于可达性分析(这是Java采用的方案)
注意别人问你:
1.谈谈垃圾收集机制中如何确定是否是垃圾。
2.说说Java垃圾收集机制中如何判断是不是垃圾?
这两个问题都有漏洞。这个是基于可达性分析的Java,但是别人问Java的是基于引用计数的。
对于每个基于引用计数的对象,都会引入一小块额外的内存来保存这个对象有多少次引用指向它。
例如:Test t=new Test();t是对这个对象的引用,所以Test对象有一个引用计数为1的引用。
如果你再写一个:Test t2=t,说明T和t2都是对这个对象的引用,那么我们的引用计数就变成了2。
当引用计数为0时,它不再被使用,被认为是垃圾,内存被释放。
参考的缺点:
1.空间利用率比较低!每个新对象都必须与一个计数器匹配(计数器假定为4个字节)。如果对象本身很大(几百个字节),多出来的4个字节不算什么,但是如果对象本身很小(只有4个字节),多出来的4个字节就相当于浪费了一半的空间。
2.会有循环引用的问题。
基于可访问性的分析是通过附加线程定期扫描整个内存空间的对象。有一些起始位置(称为GCRoots),会像深度优先遍历一样标记所有可访问的对象。(有标记的对象是可达对象),没有标记的是不可达的,也就是垃圾…
GCRoots:指堆栈上的局部变量,常量池中引用指向的对象,方法区中静态成员指向的对象…
例如:
优点:克服了引文计数的两个缺点,即空间利用率低和循环引用。
缺点:系统开销大。如果内存中有很多对象,遍历一次可能会比较慢,会消耗时间和系统资源。
简而言之,找垃圾的核心是确认这个对象未来是否会被使用,那么什么是不被使用的呢?如果没有参考点,就不用。
3.4.2垃圾收集算法标记-清除算法标记是可达性分析的过程,清除是内存的直接释放。
如果这个时候直接释放内存,虽然内存是返回给系统的,但是我们发现释放的内存是离散的,不连续的,这就给我们带来了内存碎片的问题
有很多空闲内存。假设总内存是1 G,如果我们申请500M内存,他可能申请不成功(因为申请的500M是连续内存),每次申请,内存都得是连续空间,这里的1G空闲内存可能只是内存碎片,加起来只有1G。
复制算法为了解决‘内存碎片’问题,引入了复制算法。一般来说,就是‘用一半,赔一半’
把不是垃圾的东西直接复制到另一半,释放整个原空间!
优点:解决了“内存碎片”问题
缺点:1。内存空间利用率低;2.如果要保留的对象很多,而要释放的对象很少,那么复制开销会非常高。
标记排序算法
优点:空间利用率高
缺点:仍然没有解决复制/运输元素成本高的问题。
虽然上面提到的都是有缺陷的,但是在JVM中的实现会结合各种方案。
逐代回收算法旨在对对象进行分类(根据其“年龄”)。如果一个对象通过了一轮GC扫描,它就被称为“老了一岁”。针对不同年龄的对象采用不同的方案…
注:网上可能会有这样一种说法:98%的新物体无法存活一轮GC,2%的新物体会进入存活区。这个数字其实不靠谱。如果别人问,最好不要说这个,就说大部分对象都撑不过一轮GC。
版权归作者所有:原创作品来自博主、程序员,转载请联系作者获得授权,否则将追究法律责任。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。