JVM面试(自用)

JVM

一、JVM的位置

JVM是用C编写的,并且是需要与OS进行信息的交互,所以JVM的位置是位于OS之上的(因为JRE中包含JVM所以,实际上是JRE的位置是位于OS之上的),而OS的运行位置是在硬件之上的;JVM于硬件之间没有直接的交互,但是可以调用底层的硬件。

1.1 Java程序的编译过程

JVM的作用:本质上是一个用C编写的程序,能够识别.class字节码文件(这个.class字节码文件是来自于JDK,JDK会将.java文件编译成为.class字节码文件),所以.class字节码文件中是存放对.java文件编译后产生的二进制代码,通过类加载器,将.class文件加载到JRE部分(JRE就是Java运行时的环境),接下来会运行到JRE部分,JRE是java的基础类库,是运行java文件的环境变量,通过类库中的资源,将.class字节码文件编译成为OS中的汇编语言,可以被OS读懂的二进制语言,进一步可以和计算机交互;JVM是java文件运行的虚拟机,它主要用来执行汇编语言,就是将JRE输入的语言进行执行,让OS执行指令。

二、JVM的体系结构

请添加图片描述

首先需要明确的是,会产生垃圾的只有方法区和堆,因为在栈中,使用过后的数据都会直接弹出清空,不会存在垃圾的情况

2.1 沙箱安全机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnnlQ4Xn-1660049647605)(E:\学习资料\笔记\JVM截图\沙箱安全.png)]

如图所示,上面的每一个域就代表一个沙箱,而不同的域,它们的访问的权限也不相同,可以类比在一个电脑上创建新用户来进行理解,其中的系统域主要就行用来负责域关键资源进行直接的交互;不同的域中,会存放不同的代码。

沙箱中的基本组件:

  • 字节码校验器:确保Java文件遵循给Java的语言规范;但是对一些核心类的东西是不会进行校验的
  • 类装载器的体系结构:类加载器的双亲委派机制
  • 存取控制器:存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定;类似于java中Robot类,可以直接通过它来操作电脑。
  • 安全管理器:是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高
  • 安全软件包:java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者、消息摘要、数字签名、加密、鉴别等。
2.2 类加载器

class Loader:是用来加载class文件,当new Student();,这个类的引用的声明是在Java栈中,new出来的实际的内容是在堆中。

new一个对象和Class对象的区别:

请添加图片描述

new 出来的对象在大部分情况下都是有着不同的hascode的,也就其实例化后的在Java栈中的引入类型也好,还是在堆中的真实的对象类型也好,他们都是会开辟新的空间;

而通过类加载器得到的Class对象,是唯一的,这个Class对象在被Class Loader加载之后,会一直存放在堆内存中,而new出来的实例,实际上就是对这个Class对象的一个实例化,因为在Class Loader加载之后,他会将这个类中的所有的数据都会存放在这个Class对象中。

Class Loader加载:

虚拟机自带的加载器;

根加载器(启动类加载器);(位于jre/lib/rt.jar,根加载器使用C++实现,所以通过getClassLoader方法调不到根加载器,很多C和C++写的方法都是native方法,位于本地方法库中;这个类加载器主要负责加载放在JAVA_HOME\lib目录下的包)

扩展类加载器;(位于jre/lib/ext/)

应用程序加载器;(位于java/lib/rt.jar/java/lang/ClassLoader的抽象类,实现了这个类就是类加载器)

类加载器的特点:

  • 全盘负责:当一个类加载器加载一个类的时候,该类所依赖的其他类也会被这个类加载器加载到内存中
  • 缓存机制:所有的Class对象都会被缓存,当程序需要使用时先到缓冲中查找,查找不到,再去class文件中通过类加载器将其转换为class对象
2.3 双亲委派机制

例子:

//我们声明一个和java.lang包下一样的String类,并且写一个toString方法
package java.lang;

public class String{
    public String toString(){
        return "Hello";
    }
    public static void main(String[] args){
        String s = new String();
        s.toString();
    }
}

运行之后整个程序会报错:

请添加图片描述

这就意味着,当前运行的这个方法,运行过程中,一定不是在这个main方法中运行的,从而引出了双亲委派机制。

这个机制的本质是:当前运行的这个类的时候,通过编译的到期.class字节码文件,这个字节码文件收到类加载器的请求,它会按照应当前用程序加载器—》扩展类加载器—》根加载器这样的顺序去找这个class对象,如果在根加载器找到了相应的class对象,那么它就不会运行在其它类加载器中的class对象,如果没有找到,就在扩展类加载器中寻找,以此类推;直到加载当前这个类。

双亲委派机制避免了类的重复加载;保护了程序的安全性,防止核心的API被修改

2.3.1 反向委托

当类加载器加载第三方所提供的jar时,他首先需要通过双亲委派机制找到这个jar所对应的加载器,之后,第三方的jar必须使用当前应用类加载器去加载,完成反向委托。

2.4 native方法区

native关键字,说明Java的作用范围达不到,直接调用c语言的库,而这里的库就是JVM中的本地方法库,再通过本地方法库来访问本地方法接口,所有的用native修饰的方法都会进入本地方法栈(Native Method Stack),凡是在本地方法栈中的方法,都是说明java的作用范围是达不到的,所以需要调用JNI接口(本地方法接口);JNI就是为了扩展Java使用,融合不同的编程语言为Java所用

public class Native {
    public static void main(String[] args) {
        new Thread(()->{

        },"my Thread").start();
    }
    //使用native修饰的本地方法,这个方法没有详细的方法体
    private native void start0();

}

本地方法的调用流程:

当出现使用native修饰的方法之后,会在本地方法栈中寻找,并且调用本地方法接口(JNI),因为本地方法没办法通过Java进行实现,此时可以将它的接口理解为就是抽象类和普通的接口,只定义了方法的名字,具体的方法体的实现,需要通过这个本地方法接口去调用本地方法库中的方法的具体实现,本地方法库中的实现,都是通过C,C++等语言进行实现的。

2.5 方法区

方法区是用来存放static变量、final常量、类信息(构造方法、接口定义)Class对象、运行时的常量池、方法。

Class对象:每个类的运行时的类信息就是Class对象,它包含了与类所有相关的信息,而new出来的实例对象,就需要通过Class对象来创建。每个类被编译的时候,会生成一个唯一的Class对象,它包含这个类中的所有信息,可以看作他就是类的抽象的集合。

2.5.1 栈和堆

​ Student stu = new Student();

在这个例子中,new Student()这个new出来的实例对象的具体相关信息,例如int age,String name等这些属性值,都是存放在堆内存中的,而存放在堆中的这个空间,会被赋值一个固定的地址,stu这个变量名,是存放在栈内存中的,stu在栈中的内存中存放的就是一个地址值,这个地址值指向堆内存中实例对象的内部的真实的值,这时这个stu变量,可以看成一个引用类型的变量,栈中对于所有的引用类型来说都是存放的地址值,对于所有的基础类型的数据存放的都是真实的值,同样占内存中会存放当前运行的方法,运行完后,该方法会出栈销毁。

Student这个类的声明,就是.class对象,存放在方法区中

2.5.2 方法区的特性

会随着JVM的开启而创建,关闭时会释放这一部分内存;

大小可以是固定的也是可以动态扩展的;

系统中有太多类时会出现OOM错误;

方法区对于每个线程来说都是可以访问的;

JDK9后,用原空间代替了方法区,并且,他是使用本地内存来存储,而不是在JVM设置的内存中;

2.5.3 方法区的存储内容和内部结构

方法区是用来存放static变量、final常量、类信息(构造方法、接口定义)Class对象、运行时的常量池、方法。

请添加图片描述

类信息:完整名称(包类.类名);这个类的直接父类的完整名称(接口和java.long.object没有父类);这个类型的修饰符(public,abstract,final的某个子集);这个类型直接接口的一个有序列表。

运行时的常量池:不同于常量池,常量池在.class字节码文件中,当当前的类加载到内存中后,JVM会将.class字节码文件中的常量池的内容存放到运行时常量池中,每个类都会有一个,它与常量池最大的区别时具备动态性

方法信息:JVM需要保存所有方法的信息及其声明的顺序,方法的所有相关信息,包括方法的字节码、操作数栈、局部变量表(除去abstract和native方法)

2.6 PC寄存器

每个线程都会有一个程序计数器,而这个计数器是线程私有的,其本质就是一个指针,它指向的是下一条指令的地址,就是即将执行的指令代码;同时它会存储当前运行的.class字节码文件中指令的地址

特性:

  • PC寄存器所占的内存极小,同时也是运行速度最快的存储区域;
  • 每个线程的都具有私有的PC寄存器;
  • 任何时间一个线程都只有一个方法在运行
  • PC寄存器(程序计数器)会存储当前线程正在执行的Java方法的JVM指令地址,若执行的是native方法,则是未指定值(undefined)

为什么会让PC寄存器是每个线程所私有的,因为CPU是并发执行的线程的,线程是CPU调度的基本单位,所以很有可能发生,在这一时刻还在运行a线程中的方法x,下一时刻就在运行b线程中的方法y,为了可以准确的记录各个线程当前正执行到的方法,最好的办法就是让PC寄存器是每个线程私有的。

为什么要存储当前运行的.class字节码文件中指令的地址,理由同上,再继续运行一个线程的时候,需要可以找到上次运行到的是那一条指令。

2.7 栈

先进后出,所以main方法最先执行,最后结束

栈溢出的错误,栈的无限入栈,所以会处栈溢出的错;线程结束,栈内存直接释放,不存在垃圾回收问题。

栈中存放的:8大基本类型+对象应用+实例方法

栈是一种快速的有效的分类存储的方式,访问速度仅次于PC寄存器。

**Java虚拟机,JVM规定允许Java栈的大小是动态的或者是固定不变的,固定情况下,如果线程过超过了最大容量,会抛出异常 StackOverflowError ;动态的话如果无法申请到足够内存,会抛出异常OutOfMemoryError **

2.7.1 栈帧

每个线程都有自己私有的栈,栈中的数据都是以栈帧的形式存在的;所以栈帧是存储栈的基本单位;在这个线程上执行的每个方法都会各自对应一个栈帧;栈帧是一个内存区块,里面存放着方法执行过程中所需要的各种数据信息。

2.7.2 栈帧的内部结构
  • 局部变量表
  • 操作数栈:在方法执行过程中,根据字节码指令,对栈进行入栈和出栈的操作
  • 动态链接:每个栈帧中都包含一个指向运行时常量池中该栈帧所属于的方法的引用,因为在Java文件被编译到.class字节码文件时,所有的变量和方法引用都会作为符号引用保存在class文件的常量池中,一旦这个class文件运行,那么这个常量池中会变成运行时常量池,其中的变量和方法引用也会变得可以被这个栈帧动态的链接。
  • 方法返回地址:存放该方法的PC寄存器的值(指针,指向下一个线程的指针,并存储了当前运行线程的信息)
  • 附加信息:
2.7.3 栈帧的运行原理

当前栈帧:当前正在运行的方法的栈帧是有效的;

当前方法:当前栈帧对应的方法;

当前类:当前栈帧对应的类;

  • 执行引擎所运行的字节码指令支队当前栈帧进行操作;
  • 如果当前栈调用了其他方法,对应的新栈会被创建,并且作为栈顶放在当前栈帧之上,成为新的当前栈帧;
  • 在一个栈帧中不能直接引用另一个栈帧的(类比于递归,在一个方法中调用另一个方法的行为是不被允许的);
  • 当前方法调用了其他方法,当前方法返回的时候,因为方法的返回地址是存放了该方法的PC寄存器,而这个PC寄存器中也会存放着下一个线程的指针,它会连同这个执行结果返回给前一个栈帧,与此同时JVM会丢弃掉当前栈帧,并将之前的栈帧重置未新的当前栈帧;
  • Java方法中,使用return指令是对方法的正常返回;另外一种是抛出异常,不管用哪种方式,栈帧都会被弹出;
2.8 JVM的三个版本

sum公司的HotSpot;

BEA的

IBM的

2.9 堆

一个JVM只有一个堆(所以会导致有垃圾的产生),不同于栈,每个线程都有自己私有的栈

堆内存的大小是可以调节的;同样栈也会又溢出异常,当存入的内容太多时,会发生栈的溢出。

堆区域在JVM启动的时候就会被创建,其空间大小也就确定了,是JVM管理的内存中最大的一个空间,堆是专门用来存放数组和对象的内存空间,同时,方法结束后,堆中的对象不会马上被移除,会在垃圾收集的时候被移除;

静态方法和静态变量,存放在堆的PermGen部分,因为它们是部分反射数据,它们会随着类对象同时加载,在整个类中也是唯一的,同时需要明白,父类中的只有虚方法(除了用private、static、final修饰的方法)可以被子类继承,虽然,父类中的私有和静态的变量都不会被子类继承,但是,子类可以调用这两种变量。

2.9.1 堆内存的细分

Java7及之前的Java堆内存在逻辑上分为:新生代、养老代、永久代

其中新生代又可以分为:伊甸园区、幸存者0区、幸存者1区

请添加图片描述

Java8之后主要分为:新生代、老年代、元空间

新生代还是分为:伊甸园区、幸存者0区、幸存者1区

请添加图片描述

2.9.2 年轻代和老年代

这里的年轻代和老年代,主要是针对存放在JVM中的Java对象来说的:

对于生命周期较短的顺势对象,这类对象的创建和小王都很快,所以这种对象存放在年轻代;

对于生命周期较长,甚至在某些情况下还能和JVM的生命周期中保持一致,这种对象就存放在老年代;

年轻代和老年代所占的空间比例大致为1:2;

年轻代中又会分为Eden空间和另外两个Survivor空间,它们的存储空间的大小比例大致为8:1:1;尤其需要注意的是,Survivor空间中的1号和2号所占的空间比例是1:1

2.9.3 对象的分配过程

这里先明确什么是内存垃圾,内存垃圾就是一个对象声明之后,就不再使用或者很长一段时间不再使用,并且他没有被销毁,这就是内存垃圾

接下来叙述对象分配的过程:

(1)new的最新的对象放在伊甸园区(Eden);

(2)当伊甸园区空间被填满时,并且此时有需要创建新的对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,对其中的内存垃圾进行销毁,并将新的对象放入伊甸园区;

(3)将没有被销毁的对象,放入到幸存者1区中;

(4)因为又有新的对象被new,伊甸园区依旧会执行上述操作,幸存者1区中也会发生垃圾回收,销毁部分对象;

(5)将幸存区1号中的剩下的对象放入到幸存者2号区域中;

(6)不断重复上述操作,直到执行了默认次数(15次后),就可以将幸存者2号区域里面剩余的对象,都放入到老年区中;

(7)老年区只会在它内存不足的时候,才会再次出发GC,对养老区内的内存进行清理;

(8)如果此时老年区执行了Major GC之后仍然没有足够的内存,就会产生OOM异常

对于幸存者1、2两个区域,谁有多余的空间,就可以将剩下的对象分配给他们

垃圾回收,只会频繁的繁盛在新生区,很少发生在老年区;几乎不会在永久区/元空间收集

请添加图片描述

垃圾收集算法的流程图:

请添加图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值