1、JVM虚拟机一

1、JVM虚拟机一

为什么java是跨平台的呢?

一般的c或者c++编写的程序在windows和在linux是不一样的。

在不同位数的cpu中数据宽度不一样、指针长度不一样,比如32位操作系统中的指针长度最大是32位4个字节,在64位操作系统中就是64位8字节。

不同的操作系统程序入口、函数库、汇编风格都不一样,而java是通过虚拟机中的条件编译判断的。

Jdk = JRE + 开发环境

JRE = JVM + core lib

JVM的组成部分

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)
java文件执行和JVM的联系

.java 文件通过编译器编译 .class 文件, 类加载器把 .class 文件加载到运行时数据区(方法区中),执行引擎把 .class文件翻译位地产系统指令交给cpu执行,这个过程需要调用其他语言的本地库接口来实现整个程序的功能。

对象创建方式

  • 使用Clone方法

  • 使用(反)序列化机制创建,第一种和这个是没有额外调用构造器的,下面的是通过构造器创建的

  • 使用 new 关键字创建

  • 反射

    • 使用 Constructor类 的 newInstance方法

    • 使用 Class类 的 newInstance方法

      • Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法
clinit和init

clinit 是类构造器方法,在类加载中的初始化调用。

clinit方法会执行静态变量赋值代码和静态代码块,而且静态变量赋值会执行在静态代码块前。

jvm会保证在执行子类clinit方法前完成父类的clinit方法调用

init 是对象构造器方法,在创建对象调用构造方法时调用。

init方法会执行非静态变量赋值、非静态代码块和构造器方法。有几个构造器就会有几个init方法

执行顺序是:父类变量初始化—>父类代码块—>父类构造器—>子类变量初始化—>子类代码块—>子类构造器

所以通过clinit和init方法可以很好知道对象在实例化过程中的顺序是:

父类静态变量赋值—>1.父类静态代码块—>

子类静态变量赋值—>2.子类静态代码块—>

父类普通变量初始化—>3.父类普通代码块执行—>4.父类构造器调用—>

子类普通变量初始化—>5.子类普通代码块—>6.子类构造器调用

对象实例化过程

  1. 先去看常量池中能不能定位到类的符号引用,找不到先进行类加载

  2. 在方法区进行类加载,执行 clinit方法,clinit是类构造器方法,如果有父类要先加载父类

  3. 内存分配,对象需要的内存大小在类加载完之后就可以确定。(指针碰撞或者空闲列表两种分配方式)

  4. 变量初始值为0,这样可以使java中对象的实例变量可以在不赋值的情况引用

  5. 设置对象头,通过设置对象头来得到类的元数据、哈希值balabala的

  6. 入栈执行init指令,init是对象构造器方法,此时将对象进行初始化。若是子类,这个步骤之后还要把父类的构造方法进栈,完毕后出栈然后才是子类构造方法进栈。

    1. 在栈内存中申请内存,引用变量在这里。给对象的属性进行默认初始化比如属性age=0,然后类成员变量显示初始化比如属性age=1
  7. 初始化完成后,将堆内存中的地址赋给引用变量,构造方法出栈(代码块与非静态成员变量显示初始化无先后顺序,与代码顺序相关,如代码块在上,则先加载代码块)

  8. 在整个构造函数执行完并弹栈后,把空间分配的地址赋值给一个引用对象(对象的访问定位有句柄和直接指针两种方式)

类加载器执行过程

当虚拟机遇到一个new指令时,会先去检测这个指令的参数可不可以定位到这个类的符号引用,如果检测不到进行类加载。

类加载机制:虚拟机将Class文件中描述类的数据加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

类初始化情况

在下面第3个步骤(3.初始化)中Java虚拟机由明确规定有且只有以下5种对类的主动引用情况才会立刻对类进行初始化。

1.加载—>2.连接【2-1.验证—>2-2.准备—>2-3.解析】—>3.初始化—>4.使用—>5.卸载

  1. 遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,如果类没有进行过初始化那么进行初始化;这4个指令在java中对应的场景就是new一个对象、对静态字段进行读取写入操作、调用静态方法。
  2. 使用 java.lang.reflect 包的方法进行反射调用的时候,如果类没有进行过初始化那么进行初始化。
  3. 初始化一个类,但是发现它的父类还没有进行初始化,那么触发父类的初始化。
  4. 虚拟机启动时,用户指定一个要执行的主类(包含main方法的那个类),虚拟机要先初始化该类。
  5. 当使用JDK1.7时,如果一个 java.lang.invoke.MethodHandle 实例最后解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,这个句柄对应的类没有初始化那么进行那个类的初始化。

以上5种是主动引用,下面看一下开发中常见的几种被动引用。注意:被动引用是不会对类进行初始化的。

  1. 通过子类引用父类的静态字段,会导致父类的初始化,不会导致子类的初始化

  2. 通过数组定义的引用类,不会触发引用类的初始化

    public class SuperClass{  static{    sout("SuperClass init");  }    public static void main(String[] args){    // 运行这个main函数不会触发SuperClass的初始化    SuperClass[] supClass = new SuperClass[10];  }}
  3. 常量在编译阶段会存入调用类的常量池,本质上没有直接引用定义常量的类,所以不会触发初始化

类加载的过程
加载

.class文件 从磁盘读到内存中

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。全限定名称没有限制,可以从网络中读取,可以运行时计算……
  2. 将这个字节流所代表的静态存储结构转为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,做为方法区这个类的访问入口,这个对象存在方法区中
连接
  1. 验证:验证字节码文件的正确性

    1. 文件格式验证:是否以0xCAFEBABE开头、版本号是否准确、常量池的常量是否被支持……
    2. 元数据验证:这个类是否有父类、父类是否是final、这个类是否为抽象类、这个类中的字段,方法名是否重复……
    3. 字节码验证:数据流在运行时不会做出危害虚拟机安全的事件。在这个阶段是最耗时最复杂的工作
  2. 准备:给类的静态变量分配内存,并赋予默认初始化值,类的实例变量不是在这里赋值的,它是在对象实例化时随着对象一起分配在Java堆中

    //比如public static int a = 10;// 此时,a的值是0而不是10,将a赋值为10的putstatic指令在程序被编译后存放在类构造器<clinit>()方法中,把a赋值位10的动作在初始化阶段才执行​//如果是这种情况,那么此时a=10public static final int a = 10;
  3. 解析:虚拟机将常量池中的符号引用替换成直接引用的过程,符号引用就理解为一个标示,而在直接引用直接指向内存中的地址(指针、相对偏移量、句柄)。解析包括:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点7类解析。

初始化

这是类加载过程的最后一步,在这里会真正开始执行Java代码。

主要是执行类构造器()方法。这里父类的clinit方法肯定先于子类的clinit方法执行。clinit主要作用:

  • 为类的静态变量赋予正确的初始值,为变量分配的真正初始值

  • 执行静态代码块

    public class SubClass{  static int a=1;  static{    a=2;  }}//那么这个类加载时初始化这里a=2
使用
卸载

如果类的所有实例对象都被GC,或者类的ClassLoader实例被回收、类的class实例没有其他地方引用就会被卸载,也就是被GC回收

Class文件结构

  • 16进制的特殊标志,魔数:0xCAFEBABE(16进制,咖啡宝贝)
  • 魔数后面就是跟随的Class文件的版本号
  • 常量池:静态常量池、运行常量池、字符串常量池
  • 访问标志:用于识别一些类或者接口层次的访问信息
  • 类索引、父类索引和接口索引集合
  • 当前类信息:是否为public、是否为final、是否为接口
  • 字段表集合:字段表用于描述接口或类中声明的变量,但是不包括局部变量
  • 方法表集合:和字段表一样包括了访问标识、名称索引、描述符索引、属性表集合
  • 属性表集合…………balabala的
常量池

常量池是占用Class文件空间最大的数据项目之一,也是Class文件中第一个出现表类型的数据项目。

常量池中的每一项常量都是一个表,jdk1.7之前由11中不同结构的表结构数据,后额外添加了3种:CONSTANT_MethodHandle_info(表示方法句柄)、CONSTANT_MethodType_info(表示 方法类型)、CONSTANT_InvokeDynamic_info(表示一个动态方法调用点)

常量池分为两类:

  • 字面量

  • 符号引用

    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

类和类加载器

比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提才有意义,否则,如果两个类来源同一个Class,同一个虚拟机相同,但是加载他们的类加载器不同,那么两个类就不同

双亲委派模型

如果一个类加载器收到了一个类加载的请求,它首先不会自己加载这个类,而且去找父加载器,把这个类委托交给父类加载器去加载,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父类无法加载时,子类才会自己加载

从Java虚拟机的角度来看,只有两种类加载器:

  1. 启动类加载器Bootstrap ClassLoader,由C++语言实现,是虚拟机自身的一部分
  2. 其他类加载器,由Java语言实现,独立于虚拟机,继承抽象类ClassLoader

以上类加载器细分又可以分为:

  • 启动类加载器 bootstrap ClassLoader:负责加载JRE的核心类库,比如jre下面的 rt.jar
  • 扩展类加载器 extension ClassLoader:负责加载jre扩展目录ext中的jar包
  • 系统类加载器 application ClassLoader:负责加载classPath路径下的类包,一般情况没有自定义类加载器默认就是这个加载器
  • 用户自定义加载器:负责加载用户自定义路径下的类包

双亲委派的好处 :层级关系,保证相同类在不同目录加载出来的类都是唯一的,比如一个Object类,在不同类加载器中加载都是唯一的

怎么跳过双亲委派模型:
  1. 绕过系统类加载器,直接将扩展类加载器作为使用类的父加载器
  1. 自定义加载器指定父加载器为null,也是绕过ApplicationClassLoader
  2. 线程上下文类加载器:有了它,启动类加载器需要委托子类加载自定义的加载器

对象内存布局

在JVM中,Java对象保存在堆时,由3部分组成。对象头、实例数据、对其填充

对象头Mark Word

用于存储对象自身的运行时数据,比如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳。64位虚拟机占64位

CMS promoted object(cms提升后的对象,是指从新生代提升到老年代的对象?)

还有以上在偏向锁状态时如果有其他线程来抢锁,通过cas操作如果成功会替换线程id,如果失败就会升级位轻量级锁

锁标志位(lock)

区分锁状态;01—>无锁或者偏向锁,00—>轻量级锁,10—>重量级锁,11—>待GC回收标记

是否偏向锁(biased_lock)

用于区分是否为偏向锁,在锁标志等于01时,是无锁或者偏向锁,然后通过该标志位来区分是否为偏向锁:0表示无锁,1表示偏向锁

分代年龄(age)

表示对象被GC的次数,当该值达到15时(默认),该对象就会转移到Monitor老年代中。最大值为15

对象的hashCode:

如果没有重写hashCode方法,通过System.identityHashCode()来计算,把结果赋值到这里。

偏向锁的线程ID

如果是偏向锁模式,如果当前对象被线程持有,这里会记录持有线程的ID

epoch

当偏向锁模式进行CAS操作时,这里会记录对象更偏向哪个锁

ptr_to_lock_record

轻量级锁状态下,表示指向栈中锁记录的指针。当抢锁是无竞争时,JVM使用原子操作而不是OS(操作系统互斥),这种技术称为轻量级锁定

ptr_to_heavyweight_monitor

重量级锁状态下,指向对象监视器Monitor的指针。

类型指针klass pointer

klass是c++层面的。类型指针,是对象指向它的类型元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。64位虚拟机占64位

数组长度length field

当对象是数组时有值,用来记录这个数组的长度

实例数据

主要存放类的数据信息,父类的信息,对象字段属性信息;比如:int占4个字节32位,boolean类型占一个字节8位

一个Java对象在内存中的布局可以分为两个部分:instanceOop和实例数据

上图中的reference占有4/8个字节

栈用来存储指针,堆存储对象的实例数据、方法区存储类信息、常量、静态变量

对齐填充

为了字节对其填充的数据,所以对象大小是8的倍数

为什么要有对齐数据?

原因1:HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。

原因2:让字段只出现在同一cpu的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段,这样这个字段的存储会同时污染多行缓存行,不利于程序的执行效率,不方便计算机高效寻址。

对象大小

32位系统,class指针是4字节,Mark Word是4字节,所以对象头是8字节

64位系统,class指针是8字节,Mark Word是8字节,所以对象头是16字节。在开启指针压缩的情况下,class指针是4字节,MarkWord是8字节,对象头是12字节。

压缩指针

为了尽量减少对象的内存使用量,64位Java虚拟机引入压缩指针的概念(-XX:+UseCompressedOops,默认开启),将堆中原本64位Java对象指针压缩成32位的。这样对象头中的类型指针也是64位的,对象头的大小从16字节降到12字节。

内存分配方式

内存如何分配与Java堆是否规整有关,Java堆是否规整与采用的垃圾收集器是否带有压缩整理功能决定。

指针碰撞

假如Java堆内存是规整的,那么分配内存时将用过的内存放一旁,空闲内存放一旁,中间放着指针做为分界点的指示器,这样分配内存的时候只需要这个指示器忘空闲内存移动与对象大小相等的距离。

Serial、ParNew垃圾收集器使用的是带有内存整理的算法(复制算法、标记整理算法)可以使用指针碰撞方式

空闲列表

如果Java堆内存不是规整的,那么无法通过指针碰撞来分配,只能在虚拟机中维护一个列表,用来记录哪些内存时可用的,在分配的时候要更新列表。(idle空闲的,在from或者to区)、(used已用的,eden一部分)、(available可用的,eden一部分)

CMS垃圾收集器使用标记清除算法,所以内存不是规整的,只能通过空闲列表来维护内存了

TLAB线程私有堆

由于在上面的内存分配空间时还要考虑到并发的情况,比如在多线程场景下,对象a分配了内存,但是指针还没有修改,对象b也使用了这个指针来分配对象。为了解决这个问题,有两种方式

  • 对分配内存空间的动作进行CAS+失败重试操作
  • TLAB:把内存分配的动作按照线程划分在不同空间指针,也就是说每个线程在Java堆中预先分配一小块内存,这个称为本地线程分配缓冲TLAB,默认是开启的。哪个线程需要分配内存就去自己的TLAB上分配,当TLAB用完并分配新的TLAB时,才需要同步锁定。

另外:在java中对象一定是分配在堆中的吗?

不一定。

栈上分配:在java中,有很多对象的作用于不会逃逸出方法外,比如a对象,它做为一个方法b的局部变量,没有在除b方法之外有任何引用,这样它的生命周期就是和方法调用一样,这种对象,jvm为了优化考虑,不需要将它分配到堆中(对象多了需要gc),方法调用完毕后,它可以随着方法的结束而结束,这种对象可以分配在方法栈中,但是这种方式的弊端就是栈空间没有堆空间大。

TLAB分配:就是在上面的那种线程私有堆,这个TLAB所在位置处于新生代,在新生代中开辟的线程私有区域。特别小的对象去TLAB中

对象内存访问定位

建立对象是为了使用对象,Java测下需要通过栈上的 reference来操作堆的具体对象。在Java中,有两种对象访问方式

句柄

使用句柄需要用到句柄池,句柄池在Java堆中,栈中reference存储的就是对象的句柄地址,句柄包含了对象实例数据和对象类型数据

句柄优点
  • 在对象被移动是,只要改变句柄中的实例数据指针,而reference不需要修改
直接指针

使用直接指针的话栈中的reference存储的是对象地址

直接指针优点

访问速度快。

JVM的内存模型

JVM的内存结构也称为运行时数据区

程序计数器

线程私有,一个线程对应一个计数器;

存放这当前线程接下来要执行的字节码指令、分支、循环、跳转,最终有操作系统通过控制总线向CPU发送机器指令。

唯一一个没有规定OOM的地方

本地方法栈

线程私有,Java虚拟机有时调用native类型的方法或者类,这些信息就存在这里。

虚拟机栈

线程私有,每一个线程创建的时候或者说每个方法执行都会创建一个独立的栈帧;

每一个方法从调用到执行完成对应的栈帧在虚拟机栈中的入栈到出栈的过程。当线程请求的栈深度大于虚拟机允许的深度会出现这一块对应的异常:StackOverflowError ,此时如果无法申请足够的内存就会出现OOM

用来存放 局部变量表、操作栈、动态链接、方法出口 等信息;

局部变量表中存了8大基本类型的数据、对象引用和方法返回地址

方法区(元空间)

线程共享,属于堆内存的逻辑分区,站在垃圾回收器的角度划分属于永久代(jdk1.8之后被称为元空间),在HotSpot JVM中,方法区分为持久代和代码缓冲区;

存储已经被虚拟机加载的类信息(类信息是类加载器加载反射的地方)、运行常量池(保存Class文件中描述的符号引用)、静态变量、编译器编译之后的代码数据;

元空间存储
  • 引导类加载器Bootstrap ClassLoader
  • 扩展类加载器Extension ClassLoader
  • 应用类加载器Application ClassLoader
为什么用元空间取代永久代

永久代的缺点

  • 占用堆的大小
  • 引发GC:字符串、动态字节码技术等引发
  • 存储的类信息有限
  • 存在碎片化问题
堆内存

线程共享,JVM中最大的内存区域,堆内存也被称为GC堆,细分可分为:新生代(1/3) 和老年代(2/3) ;

新生代:eden区、from survivor、to survivor区

存放的是对象(new、反射、克隆等)和数组等;

推荐阅读