大家好,这里是淇妙小屋,一个分享技术,分享生存的博主
以下是我的主页,各个主页同步更新优质博客,创作不易,还请大家点波关注
掘金主页
知乎主页
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),在各种类加载器环境中都可能保障是由某个特定的类加载器来加载的