《码出高效-Java开发手册》走进JVM有感

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/qq_34160679/article/details/86142889

走进JVM

字节码

Java 所有的指令有200个左右,一个字节可以存储256种不同的指令信息,一个这样的字节称为字节码(中间码)。在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM 也可以将字节码编译执行,如果是热点代码,会通过JIT 动态地编译为机器码,提高执行效率

字节码主要指令如下:

  • 加载或存储指令

    在某个栈帧中,通过指令操作数据在虚拟机栈的局部变量表与操作栈之间来回传输,常用指令如下:

    • 将局部变量加载到操作栈中。
    • 从操作栈顶存储到局部变量表
    • 将常量加载到操作栈顶,这是极为高频使用的指令
  • 运算指令: 对两个操作栈帧上的值进行运算,并把结果写入操作栈顶IADD、IMUL

  • 类型转换指令 显示转换两种不同的数值类型

  • 对象创建于访问指令

    • 创建对象指令 NEW NEWARRAY
    • 访问属性指令 GETFIELD、PUTFIELD 等
    • 检查实例类型指令
  • 操作栈管理指令

    • 出栈指令
    • 赋值栈顶元素并压入栈
  • 方法调用与返回指令

  • 详细见 P125《码出高效: JAVA开发手册》

Java源文件 ----> 词法解析-- token流 -> 语法解析 -----> 语义分析 -----> 生成字节码 ----> 字节码

详细见P126

类加载过程

任何程序都需要加载到内存中才能与CPU进行交流。字节码 .class 文件同样需要家长到内存中,才可以实例化类。ClassLoader 就是提前加载 .class 文件到内存中

过程: 加载、链接、初始化

  • 加载: 读取类文件产生的二进制流,并转为特定的数据结构,初步校验cafe babe 魔法值,常量池、文件长度、是否有父类等,然后创建对应类的实例
  • 链接包括验证、准备、解析三个过程。验证是更详细的校验,比如final 是否合规,类型是否正确、静态变量是否合理等;准备结果是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局
  • 初始化结果,执行类构造器方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另一个类,在虚拟机栈中执行完毕后通过返回值进行赋值

内存布局

在这里插入图片描述

  • Heap (堆区)

    是OOM 故障的主要发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区各子线程共享使用。通常它的占用的空间是所有内存区域最大的,但是如果无节制的创建实例那么也将会消耗完内存导致OOM。可以在运行时动态的设置它的大小,-Xms256M - Xmx1024M 表示设定初始值和最大值。服务器在运行过程中,退空间不断地扩容和回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM 的Xms和Xmx 设置为一样大小,避免在GC 后调整堆大小时带来的额外压力。

    (下图:这里的放得下指的是当创建一个大对象时候,内存区域是否能够容纳得下)

在这里插入图片描述

  • Metaspace(元空间)

    元空间的前身是Perm区(被称为永久代),在JDK7 及之前的版本中才有Perm,现在的版本使用了Metaspace。因为Perm 在某些场景下,如果动态加载类过多,容易产生Perm 区的OOM(为了解决需要设定参数 -XX:MaxPermSize = 1280m),如果部署到新机器上,往往会因为JVM 参数没有修改导致故障再现,不熟悉此应用的人很难排查。所以,元空间就诞生了。元空间在本地内存中分配。

  • JVM Stack (虚拟机栈)

    栈的特性是先进后出的数据结构,JVM 是基于栈结构的院系环境。JVM 中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程。就是栈帧从入栈到出栈的过程。活动线程中只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法。

    虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。

    • 局部变量表: 存放方法参数和局部变量的区域
    • 操作栈 : 一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。
    • 动态连接:每一个栈帧包含一个常量池中对当前方法的引用,目的是支持方法调用过程的动态连接
    • 方法返回地址: 方法执行有两种退出情况: 1. 正常退出。2.异常退出。 无论何种退出情况都将返回到方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧。
  • 本地方法栈

    Native Method Stack 在JVM内存布局中,也是线程对象私有的。被称为Native 方法服务,线程开始调用本地方法时,会进入一个不再受JVM约束的世界。本地方法可以通过JNI来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。

  • 程序计数寄存器

    每一个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器,程序的执行或者恢复都要依赖程序计数器。程序计数器在哥线程之间互不影响,此区域不会发生内存溢出异常。

    从线程共享的角度来看,堆空间和元空间都所有线程共享的,而虚拟机栈和本地方法栈,程序计数器是线程内部私有的。

在这里插入图片描述

对象实例化

实例化对象过程

  • 确认类元信息是否存在。当JVM接受到new指令时,首先在metaspace 内检查需要创建的类元信息是否存在。若不存在,那么在双亲委派模式下,使用当前类加载器以ClassLoader +包名+类名为key 进行查找对应的.class 文件,如果没有找到文件,则抛出ClassNotFoundException 异常,如果找到,则进行类加载并生成对应的Class 类对象。
  • 分配对象内存。首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小,接着在堆中划分一块内存给新对象。在分配内存空间时,需要进行同步操作,比如采用CAS失败重试,区域加锁等方式保证分配操作的原子性。
  • 设定默认值。成员变量值都需要设定默认值,即各种不同形式的零值。
  • 设置对象头。设置新对象的哈希码,GC信息,锁信息,对象所属的类元信息等。这个过程的具体设置方式取决JVM实现。
  • 执行init 方法。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

垃圾回收

Java 会对内存进行自动分配与回收管理,使上层业务更加安全,方便地使用内存实现程序逻辑。GC 主要目的是清除不再使用的对象,自动释放内存。

  • 标记-清除

  • 复制

  • 标记-整理

  • 区分新老年代(分代收集)

猜你喜欢

转载自blog.csdn.net/qq_34160679/article/details/86142889