关于java:一文吃透JVM面试轻松拿offer

9次阅读

共计 4941 个字符,预计需要花费 13 分钟才能阅读完成。

大家好,这里是 淇妙小屋 ,一个分享技术,分享生存的博主
以下是我的主页,各个主页同步更新优质博客,创作不易,还请大家点波关注
掘金主页
知乎主页
Segmentfault 主页
开源中国主页
后续会公布更多 MySQL,Redis,并发,JVM,分布式等面试热点常识,以及 Java 学习路线,面试重点,职业规划,面经等相干博客
转载请表明出处!

1. JVM 内存构造

1.1 JDK6 下的内存构造

  • 运行时数据区

    • 堆(线程共享的区域):存储对象实例,由垃圾回收机制进行内存治理
    • 办法区(线程共享的区域):存储以下信息

      • Class 对象(该类的办法区中各种数据的拜访入口,各种数据蕴含了以下信息)
      • 运行时常量池 (内含字符串常量池)
        寄存各种字面常量,class 文件中的符号援用,以及符号援用解析失去的间接援用
      • 类型信息
        类型的全限定名,类型父类的全限定名,类型实现的接口的全限定名,类型是类还是接口,类型的拜访修饰符等
      • 字段信息
        类中申明的所有字段 (包含动态变量和实例变量,不包含局部变量) 的形容(名称,类型,修饰符等)
      • 办法信息
        办法的 名称,返回类型,参数表,字节码指令,修饰符,局部变量表和操作数栈的大小,异样表
      • 动态变量
      • 指向类加载器的援用
      • 指向 Class 类对象 (Class.forName() 的 Class)的援用
    • 虚拟机栈(每个线程一个,线程公有)

      每执行一个 Java 办法,会往 Java 虚拟机栈中 push 一个栈帧
      一个 Java 办法执行结束,其对应的栈帧从 Java 虚拟机栈中 pop

      编译 java 文件时,一个栈帧须要多大的局部变量表,多深的操作数栈曾经被剖析进去,并写入方发表的 code 属性中

      • 栈帧构造

        • 操作数栈
        • 局部变量表

          局部变量表存储了编译器可知的 Java 的根本数据类型,reference,returenAddress 类型

          这些数据在局部变量表中以 Slot模式存储,除了 double 和 long 占 2 个 slot,其余占 1 个 slot

          JVM 通过索引定位拜访局部变量表,索引从 0 开始

        • 锁记录
        • 动静连贯
          一个指向运行时常量池中该栈帧所属的办法的援用,该援用是为了反对办法调用过程中的动静连贯(常量池中的一部分符号援用会在每一次运行期间都转化为间接援用——动静连贯)
        • 办法返回地址
    • 本地办法栈(每个线程一个,线程公有):相似于 Java 虚拟机栈,不过执行的是 native 办法
    • 程序计数器(每个线程一个,线程公有)

      程序控制流的指示器

      如果执行的是 Java 办法,程序计数器存储的是下一条字节码指令的地址
      如果执行的是本地办法,程序计数器为 Undefined

  • 间接内存

    间接内存并不属于运行时数据区

    JDK1.4 引入 NIO 类,引入了一种基于 Channel 与 Buffer 的 I / O 形式,能够应用 Native 函数库间接调配堆外内存(在间接内存中调配空间),而后通过一个存储在堆中的 DirectByteBuffer 对象作为这块内存的援用进行操作

  • 执行引擎
  • 本地库接口
  • 本地办法库

1.2 JDK7,JDK8 内存构造的变动

  • JDK1.7

    ** 字符串常量池,动态变量 从办法区挪动到堆中
    办法区:移出 字符串常量池 动态变量
    堆:实例对象,字符串常量池 动态变量

  • JDK1.8
    移除了办法区,将 JDK1.7 的办法区中的剩下货色移到 元空间 (元空间属于本地内存)

    JDK1.8 的内存构造如下图

2. 对象

2.1 对象的创立

  • 应用 new——调用了构造方法
  • 应用 Class 对象的 newInstance()——调用了构造方法
  • 应用 Constructor 类的 newInstance 办法——调用了构造方法
  • 应用 clone 办法——没调用构造方法
  • 应用反序列化——没调用构造方法

2.2 通过 new 创建对象

  • ①遇到 new 指令时,首先查看这个指令的参数是否能在运行时常量池中定位到一个类的符号援用,并且查看这个符号援用代表的类是否曾经被加载、解析和初始化过。
    如果没有,执行相应的类加载,确保类曾经加载实现后执行②
  • ②为新对象分配内存(内存大小在类加载实现后便可确认),并保障线程平安

    • 分配内存的办法

      • 若堆内存规整——指针碰撞
        若堆内存规整,应用过的内存放在一边,未应用的放在另一边,两头放着一个指针作为指示器
        分配内存时,仅仅把指针向向未应用的一边挪动一段与对象大小相等的间隔即可
      • 若堆内存不规整——闲暇列表
        虚拟机维持一个列表,记录哪些内存块是可用的
        分配内存时,从列表中找到一块足够大的空间调配给对象实例,并更新列表记录
      • 分配内存采纳哪种办法——> 取决于堆内存是否规整——> 取决于应用的垃圾回收器
    • 保障分配内存时线程平安的办法

      并发状况下,可能呈现这种状况,正在给 A 对象分配内存,指针还未修改,对象 B 又同时应用了指针来分配内存,有以下 2 种解决方案

      • 对分配内存空间的动作进行同步解决——>JVM 采纳 CAS+ 失败重试 保障内存调配操作的原子性
      • 不同线程在不同的内存空间中进行内存调配
        Java 堆中,每个线程都调配一块小内存 (本地线程调配缓冲区 TLAB)
        哪个线程须要分配内存,就在本人的 TLAB 调配
  • ③将调配到的内存空间初始化为 0(不包含对象头),接下来就是填充对象头,把对象是哪个类的实例、如何能力找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
  • ④执行 init()办法初始化对象

2.3 对象的内存布局

一个实例对象占有的内存能够分为三块——对象头,实例数据,对齐填充

  • 对象头(一般对象 2 个字,数组 3 个字)

    • 第一个字——>Mark Word(32or64 位,内容取决于 最初 2 位标识位)

    • 第二个字——> 指针,指向这个实例对象所属的类的 Class 对象
    • 第三个字——> 数组长度
  • 实例数据
  • 对齐填充
    不是必然须要,次要是占位,保障对象大小是某个字节的整数倍

2.4 对象的拜访定位——援用定位到对象的形式

  • 通过句柄拜访对象

    如果应用句柄拜访,Java 堆中可能会划分出一块来作为 句柄池

    reference 存储的是句柄池的地址

  • 通过间接指针拜访对象

    reference 存储的是实例对象的地址
    实例对象的对象头中存储有指向 Class 对象的指针

  • JVM 采纳 间接对象拜访

2.5 对象创立时的内存调配策略

  • Java 堆的内存模型
  • 对象优先调配在 Eden 中,如果 Eden 没有足够的空间,那么进行一次 MinorGC
  • 大对象间接进入老年代(例如很长的字符串 or 元素数量很宏大的数组)
  • 长期存活的对象进入老年代
    JVM 给每个对象定义了一个年龄计数器 (Age),存储在对象头中
    对象通常在 Eden 中诞生,如果通过一次 MinorGC 后,对象仍存活
    如果对象不能被 Survivor 包容,那么间接进入老年代
    如果对象能被 Survivor 包容,那么进入 Survivor,Age 设为 1,对象在 Survivor 中每熬过一次 MinorGC,Age 达到肯定值(默认 15,能够设置), 进入老年代
  • 动静年龄断定
    Survivor 区中雷同年龄的对象,如果其大小之和占到了 To Survivor 区一半以上的空间,那么大于此年龄的对象会间接进入老年代
  • 空间调配担保
    产生 YGC 前,JVM 先查看老年代最大可用的间断空间 是否 > 新生代所有对象总空间

    • 如果大于,那么这次 YGC 平安
    • 如果不成立,阐明 YGC 不平安,JVM 查看 HandlePromotionFailure 参数,查看是否容许担保失败

      • 如果容许,查看老年代最大可用的间断空间 是否 > 历次降职到老年代对象的均匀大小

        • 如果大于,进行一个 YGC
        • 否则,进行一次 FullGC
      • 如果不容许,进行一次 FGC

3. 类加载机制

3.1 Java 程序如何启动

  • 首先进行编译,将.java 文件编译为.class 文件(二进制流文件)
  • 启动 Java 过程,在内存中创立运行时数据区
  • 在 main()所在的类加载到内存中,开始执行程序

    JVM 不会将全副的类加载进来,只有须要应用某个类时,才会把这个类加载进来,而且只会加载一次

3.2 类加载的机会

  • new 一个对象
  • 拜访对象的动态属性,静态方法
  • 反射
  • JVM 的启动类
  • 如果一个类进行加载,那么优先加载其父类
  • 如果一个接口定义了默认办法

3.3 类加载流程

3.3.1 Loading

该过程由 ClassLoader实现

就是将二进制流读入内存,并为之创立一个 java.lang.Class 对象

  • 通过类的 全限定名 获取类的.class 文件(能够从磁盘,网络等获取)
  • 将 class 文件中的动态存储构造转换为办法区中的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区中运行时数据结构的拜访入口,所有对类数据的拜访和应用都必须通过这个 Class 对象

3.3.2 验证 Verification

  • 文件格式验证

次要验证字节流是否合乎 Class 文件格式标准,并且能被以后的虚拟机加载解决。例如:主,次版本号是否在以后虚拟机解决的范畴之内。常量池中是否有不被反对的常量类型。指向常量的中的索引值是否存在不存在的常量或不合乎类型的常量。

  • 元数据验证

这个类是否有父类(Object 除外)

这个类是否继承了不容许继承的类(final 类)

如果这个类不是抽象类,这个类是否实现了父类和接口中要求实现的所有办法

类中的字段,办法是否与父类产生矛盾

  • 字节码验证

最重要的验证环节,剖析数据流和管制,确定语义是非法的,合乎逻辑的。次要的针对元数据验证后对办法体的验证。保障类办法在运行时不会有危害呈现。

  • 符号援用验证

次要是针对符号援用转换为间接援用的时候,是会延长到第三解析阶段,次要去确定拜访类型等波及到援用的状况,次要是要保障援用肯定会被拜访到,不会呈现类等无法访问的问题。

3.3.3 筹备 Preparation

为类变量分配内存并设置初始值

  • static 变量——设置为零值
  • final static 变量——设置为其申明的值

3.3.4 解析 Resolution

将.class 文件的常量池中的符号援用替换为间接援用

  • 符号援用:以一组符号来形容所援用的指标
  • 间接援用:能够指向指标的指针针、绝对偏移量或者是一个能间接定位到指标的句柄

3.3.5 初始化 Initialization

  • 初始化其实就是执行类的 clinit()办法的过程
  • 子类在初始化前必须先实现父类的初始化
  • JVM 保障一个类的 clinit()办法在多线程环境中只会被一个线程执行一次——保障一个类只会加载一次
  • 对于类

    • 查看父类是否曾经加载——若未加载,则先加载父类
    • 查看类的接口是否曾经加载——若未加载,则先加载接口
    • Javac 编译器会主动收集 static 属性赋值语句,static 代码块 生成 clinit()办法(收集程序依照代码中的程序)——而后执行类结构器 clinit()办法
  • 对于接口

    • 只有应用到父接口时,才会加载父接口,否则不加载父接口

3.4 ClassLoader

在 JVM 中,一个类由 加载它的类加载器 类自身 独特确定其在 JVM 中的唯一性

3.4.1 ClassLoader 类型

  • 启动类加载器(BootstrapClassLoader)

    C++ 实现,其余的都是用 Java 实现

    负责加载 JVM 根底外围类库,无奈被 Java 程序间接应用

    能加载的类有以下条件

    • 寄存在 ${JAVA_HOME}/lib 目录下,或者 寄存在被 -Xbootclassp 下
    • 被 -Xbootcalsspath 参数指定的门路下的类
  • 拓展类加载器(Extension ClassLoader)

    负责把一些类加载到 JVM 内存中,能够在 Java 程序中应用

    能加载的类有以下条件

    • 寄存于 ${JAVA_HOME}/lib/ext 目录下的类库
    • 被 java.ext.dirs 零碎变量所指定的门路中的所有类库
  • 应用程序类加载器(Application ClassLoader)

    是 ClassLoader.getSystemClassLoader()的返回值

    负责加载 用户类门路 (ClassPath) 上的的所有类库,能够在 Java 程序中应用

    个别状况下这个就是程序默认的类加载器

3.4.2 双亲委托机制

  • 工作过程

    1. 当一个类加载器收到类加载申请,它首先不会本人去加载这个类,而是把这个申请传递给它的父类加载器,父类加载器把这个申请传递给父类加载器的父类加载器……直到传递到启动类加载器(向上传递)
    2. 启动类加载器在它的搜寻范畴中查找所要加载的类——找到,就 loading 这个类

      找不到——传递给子类加载器……直到某个类加载器能够在它的搜寻范畴查找到所要加载的类(向下传递)

  • 双亲委托机制的益处

    使得 Java 根本类库中的类(像 Object),在各种类加载器环境中都可能保障是由某个特定的类加载器来加载的

正文完
 0