JVM面试题这一篇就够了

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

Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。
一旦Java代码被编译为Java字节码,便可以在不同平台上的Java虚拟机上运行。
不同平台用不同的JVM,因此JDK和JRE也不同
JDK和JRE区别

Java代码是怎么运行的?

Java代码被编译为Java字节码,在Java虚拟机上运行。

Java虚拟机将运行时内存区域划分为五个部分,分别为方法区PC寄存器Java方法栈本地方法栈。Java程序编译而成的 class文件,需要先加载至方法区中,方能在Java虚拟机中运行。

为了提高运行效率,标准JDK中的HotSpot虚拟机采用的是一种混合执行的策略
解释执行 :在执行时才翻译成机器指令,无需保存不占内存。
即时编译 (just-in-time compilation,JIT):将其中反复执行的热点代码,以方法为单位进行即时编译,翻译成机器码后直接运行在底层硬件之上。HotSpot 装载了多个不同的即时编译器。

  • Client Complier(C1):更高的编译速度
  • Server Complier(C2):更好的编译质量

但即时编译类似预编译,编译之后的指令需要保存在内存中,这种方式吃内存,按照二八原则这种混合模式最恰当。java混合执行策略

类加载

类的加载,由类加载器完成:

  1. 通过类的全限定名来获得二进制字节流
  2. 将这个字节流所代表的静态存储结构转换成方法区运行时的数据结构
  3. 并在堆内存区创建一个Java.lang.Class 对象,作为对方法区中这个数据结构的访问入口

类加载器分类:

  • Bootstrap ClassLoader : C++实现,加载java核心API, jre/lib/rt.jar
  • ExtClassLoader: 加载扩展API,jre/lib/ext 目录中 ,如 javax.*
  • APPClassLoader或SystemClassLoader:加载ClassPath下定义的class
  • CustomClassLoader:应用程序根据自身需要自定义的ClassLoader

类加载过程采用父类委托机制,更好的保证了java平台的安全性(更优先加载jre的class文件),类的加载首先请求父类的加载器加载,父类加载器无法加载该类时才尝试从自己的类路径中加载该类。(父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码)

类的生命周期

当Java程序需要使用某个类时,JVM要保证这个类被加载、连接(校验、准备、解析)和初始化。

  1. 类的加载:
    1. 通过类的全限定名来获得二进制字节流
    2. 将这个字节流所代表的静态存储结构转换成方法区运行时的数据结构
    3. 并在堆内存区创建一个Java.lang.Class 对象,作为对方法区中这个数据结构的访问入口
  2. 连接校验:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。(文件格式验证,元数据验证,字节码验证,符号引用验证)
  3. 准备:为静态成员变量分配内存并设置默认的初始值(不是等于之后的真实值),这些变量所使用的内存都将在方法区中进行分配。

    假设定义类变量:public static int value=3; 在准备阶段后value为0,将value=3的指令是存放于构造器方法之中的,在初始化阶段才会执行。如果同时被 final 修饰,则在准备阶段就会value赋值为3

  4. 解析:将常量池内的符号引用替换为直接引用(也可能发生在初始化之后)
  5. 初始化:先初始化父类,对 static修饰的静态变量和静态代码进行初始化,并初始化程序员设置的变量值。
  6. 生命周期结束:
    • 执行了System.exit() 方法。
    • 程序正常执行结束。
    • 程序在执行过程中遇到了异常或错误而异常终止。
    • 由于操作系统出现错误而导致java虚拟机进程终止。

Java 对象的创建过程

当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的 实例变量(非静态变量) 及其 从父类继承过来的实例变量 (即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。

在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。
在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化实例代码块初始化 以及 构造函数初始化

在继承中代码的执行顺序为:

  1. 父类静态对象,父类静态代码块
  2. 子类静态对象,子类静态代码块
  1. 父类非静态对象,父类非静态代码块
  2. 父类构造函数
  3. 子类非静态对象,子类非静态代码块
  4. 子类构造函数

1-2为类的初始化完成,3-6为类的实例化,即一个对象的初始化时完成。

获取对象的方式

A a = (A)Class.forName(“pacage.A”).newInstance();
A a = new A()

是一样的效果。

newInstance( )是一个方法,而new是一个关键字。创建对象的方式不一样,前者是使用类加载机制,后者是创建一个新类。Class下的newInstance()的使用有局限,因为它生成对象只能调用无参的构造函数,而使用 new关键字生成对象没有这个限制。
newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。

Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);
第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行。

ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);
第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,可以不进行链接意味着不进行包括初始化等一系列步骤,那么静态块和静态对象就不会得到执行。

为什么在加载数据库驱动包的时候用的是Class.forName( ),却没有调用newInstance( )?
newInstance()方法,会保证这个类加载、连接和(初始化)。
JDBC Driver源码如下,因此使用Class.forName(classname)才能在反射回去类的时候执行static块。

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

所以在静态初始化器的中已经进行了注册,所以我们在使用JDBC时只需Class.forName(XXX.XXX);就可以了。

JVM内存结构

JVM内存结构
堆(heap)
java虚拟机管理的内存中最大的一块,线程共享,存放对象实例。

方法区(Method Area)
线程共享,存储已被虚拟机加载的类信息常量(static final)静态变量即时编译器编译的代码等数据。

A、类的基本信息:

  1. 每个类的全限定名
  2. 每个类的直接超类的全限定名(可约束类型转换)
  3. 该类是类还是接口
  4. 该类型的访问修饰符
  5. 直接超接口的全限定名的有序列表

B、已装载类的详细信息

  1. 运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。它们以数组形式通过索引被访问,是外部调用与类联系及类型对象化的桥梁。(存的可能是个普通的字符串,然后经过常量池解析,则变成指向某个类的引用)
  2. 字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。字段名称指的是类或接口的实例变量或类变量,字段的描述符是一个指示字段的类型的字符串,如 private A a = null; 则 a 为字段名,A为描述符,private 为修饰符。
  3. 方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区)
  4. 静态变量:就是类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态快。
  5. 到类classLoader的引用:到该类的类装载器的引用。
  6. 到类class的引用:jvm为每个加载的类型(译者:包括类的接口)都创建一个Java.lang.Class 的实例。而jvm必须为某种方式把class的这个实例和存储在方法区中的类型数据联系起来。

程序计数器(program counter register)
是当前线程所执行的字节码的行号指示器。唯一一个没有任何 OutOfMemoryError 的区域。

JVM栈(JVM Stacks)
线程私有,生命周期与线程相同。描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个桟帧(stack frame)用于存储局部变量表、操作桟、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个桟帧在虚拟机桟中从入栈到出栈的过程。会抛出两种异常:当线程请求的桟深度大于虚拟机允许的深度,会抛出stackoverflow;如果虚拟机桟允许动态扩容,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError。

本地方法栈(native method stacks)
与虚拟机桟作用相似,区别在于他为虚拟机使用到的native方法服务。有的虚拟机将本地方法桟与虚拟机桟合二为一。也会抛出StackOverFlow或OutOfMemory。

stack: 桟是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和桟的最大容量是系统预先选定好的。由系统自动分配,速度较快,但程序员是无法控制的。
heap: 堆是向高地址扩展的数据结构,是不连续的内存区域。由new 分配的内存,一般速度较慢,容易产生内存碎片,但用起来方便。

GC垃圾回收算法

垃圾收集器

JVM控制参数

-Xms 堆默认空间
-Xmx 堆最大空间,最大空间默认是物理内存的 1/4
所以服务器的 Xmx 和 Xms 设置一般应该设置相同避免每次 GC 后都要调整虚拟机堆的大小
-XX:NewSize 新生代最小空间大小
-XX:MaxNewSize 新生代最大空间大小
-Xmn 用来设置堆内新生代的大小(相当于设置了相同的-XX:NewSize和-XX:MaxNewSize)
-XX:SurvivorRatio 用于设置Eden和其中一个Survivor的比值
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代大小来间接控制。

方法区或元空间:存放class文件信息和常量池
JDK1.8 -XXPermSize=8m -XXMaxPermSize=8m失效,改为 -XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=8m

-Xss设置每个线程可使用的栈空间大小、每个线程都有他自己的 Stack

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页