一文吃透JVM,面试轻松拿offer

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

1. JVM内存结构

1.1 JDK6下的内存结构

一文吃透JVM,面试轻松拿offer_第1张图片

  • 运行时数据区

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

      • 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的内存结构如下图

    一文吃透JVM,面试轻松拿offer_第2张图片

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位标识位)

      一文吃透JVM,面试轻松拿offer_第3张图片

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

2.4 对象的访问定位——引用定位到对象的方式

  • 通过句柄访问对象

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

    reference存储的是句柄池的地址
    一文吃透JVM,面试轻松拿offer_第4张图片

  • 通过直接指针访问对象

    reference存储的是实例对象的地址
    实例对象的对象头中存储有指向Class对象的指针
    一文吃透JVM,面试轻松拿offer_第5张图片

  • JVM采用直接对象访问

2.5 对象创建时的内存分配策略

  • Java堆的内存模型
    一文吃透JVM,面试轻松拿offer_第6张图片
  • 对象优先分配在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 类加载流程

一文吃透JVM,面试轻松拿offer_第7张图片

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 双亲委托机制

一文吃透JVM,面试轻松拿offer_第8张图片

  • 工作过程

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

      找不到——传递给子类加载器……直到某个类加载器可以在它的搜索范围查找到所要加载的类(向下传递)

  • 双亲委托机制的好处

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

你可能感兴趣的:(java后端)