JVM:JVM常见面试题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/apache_z/article/details/102321423

问题一:JDK和JVM的关系

说到JVM,我们不得不提到JDK,这是Java的基础。JDK(Java Developer’s Kit)也就是java开发工具包。下面是window下的目录结构。
在这里插入图片描述
可以知道JDK中包含JRE,JRE中包含JVM。下面的图更加清晰。
在这里插入图片描述

问题二:什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

  1. JVM其实就是通过软件虚拟成的计算机,但是它跟底层的操作系统和硬件有很深的联系,可以理解为他在在不同操作系统就会产生对应的JVM虚拟机。
  2. JVM的作用就是将我们的写好的Java代码(.java)编译的字节码(.class)文件进行转化为底层硬件可以理解的机器语言。所以,不管我们的程序运行在什么平台上,我们都不需要操心Java代码能不能兼容,因为JVM可以根据平台的特性,进行翻译为特定的机器码。

问题三:JVM的启动流程

在这里插入图片描述

  1. 当我们执行java命令,后面加上类名,或者jar包时开始启动JVM。(类的加载)
  2. 然后JVM装载配置文件。
  3. 根据配置文件找到对应的JVM.dll文件
  4. 根据dll文件初始化JVM,并且通过JNIENV(Java Native Interface Environment)拿到本地接口
  5. 从类的Main方法开始运行。

问题四:类的加载

  1. 首先明白类的生命周期
加载
验证
准备
解析
初始化
卸载

其中验证、准备、解析属于连接阶段。

  1. 类的加载时间(5种)
    (1) 被 new 时加载
    (2) 子类被调用(会先加父类)
    (3) 访问类的静态变量(调用或者赋值)
    (4) 调用类的静态方法
    (5) 反射调用
  2. 类的加载器分为三种
    (1) 根加载器(bootstrap class loader)使用C++写的,用来加载lib/rt.jar下面的核心类
    (2) 扩展加载器(extensions class loader),主要加载Java的扩展Class
    (3)系统类加载器(system class loader),如果没用用户自定义,我们平时写的Class就是这个加载器加载的
  3. 加载器加载机制三种
    (1) 全盘加载:这个加载器加载的类所依赖或者引入的所有类都需要由这个加载器进行加载
    (2) 双亲加载:当加载一个类时,这个加载器会先让它的父加载器加载,不行,自己才会去加载
    (3) 缓存加载:当加载一个类时,先判断这个类是否在缓存区,在的话,不进行二次加载
    注意:类被不同的加载器加载会生成不同的类对象(类对象的名字是由全类名+加载器名字生产),加载器将类的类型信息放在方法区,将类对象放在Java堆中。
  4. 验证 分为四种验证 :
    (1) 文件格式验证:主要校验类的文件格式
    (2) 元数据验证: 主要对字节码文件进行语义分析,是否符合Java语法规范
    (3) 字节码验证:主要是对元数据验证之后的方法体进行验证
    (4) 符号引用验证: 主要是确保符号引用是否存在
  5. 准备:为类的静态变量分配内存
  6. 解析:将符号引用改为直接引用
  7. 初始化: 为类的静态变量赋值
  8. 卸载:执行System.exit()正常结束或者出现异常时调用

问题五:JVM的内存结构

在这里插入图片描述

  1. 方法区:
    有时候也称为永久代(Permanent Generation),在方法区中,存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,在这里进行的GC主要是方法区里的常量池和类型的卸载。当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
    在方法区中有一个非常重要的部分就是运行时常量池,用于存放静态编译产生的字面量和符号引用。运行时生成的常量也会存在这个常量池中,比如String的intern方法。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。
  2. 堆:
    Java中的堆是用来存储对象实例以及数组(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。在JVM中只有一个堆。堆是Java垃圾收集器管理的主要区域,Java的垃圾回收机制会自动进行处理。
    Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
    堆空间分为老年代和年轻代。刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。年轻代中又被分为Eden区和两个Survivor区(From Space和To Space)。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。 当一个对象大于eden区而小于old区(老年代)的时候会直接扔到old区。 而当对象大于old区时,会直接抛出OutOfMemoryError(OOM)。
  3. Java栈:
    Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈。JVM栈是线程私有的,每个线程创建的同时都会创建自己的JVM栈,互不干扰。
    Java栈是Java方法执行的内存模型。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。
    局部变量表:用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译期就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
    操作数栈:栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
    指向运行时常量池的引用:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
    方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
  4. 程序计数器:
    程序计数器(Program Counter Register),也有称作为PC寄存器。
    由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
    在JVM规范中规定,如果线程执行的是非native(本地)方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
    由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
  5. 本地方法栈:
    JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
  6. 执行引擎:
    执行引擎负责具体的代码调用及执行过程。
    执行过程:
    (1)输入字节码文件
    (2)解析字节码文件
    (3)执行字节码文件
    类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area)(输入字节码),然后执行引擎执行会执行这些字节码。通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。不过Java字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被JVM执行的语言。
    字节码可以通过以下两种方式转换成合适的语言。
    解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。
    字节码这种“语言”基本来说是解释执行的。
    即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
    注意
    方法区和堆是线程共享的,栈和本地方法栈和程序计数器都是非线程共享。

问题六:JVM中的对象结构

Java对象由三个部分组成:对象头、实例数据、对齐填充。
(1) 对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
(2)实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
(3)对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)

问题七:JVM的垃圾回收机制(太过复杂,慢慢总结)

(发现一篇文章,关于GC(Garbage Collection) 写的很易懂,分享一下)博客
这篇文章大致思路:
什么东西会被回收(或者已死、是否能联系到)
方法区的什么东西被回收
如何回收(四种垃圾回收算法):标记-清除算法、复制算法(新生代算法),老年代算法(标记整理算法)、分代收集算法。

猜你喜欢

转载自blog.csdn.net/apache_z/article/details/102321423