jvm虚拟机浅谈(一)

一、jvm内存运行时区域

1.1 程序计数器

程序计数器作为当前线程所执行的字节码的行号指示器,保存当前线程的执行位置。当字节码解释器工作时,就是通过改变这个计算器的值来选取下一条要执行的字节码指令。每条线程都有一个独立的程序计数器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的字节码指令地址。如果线程正在执行的是Native方法,这个计数器值为空。

1.2 本地方法栈

本地方法栈就是执行本地native方法的栈,native方法由虚拟机实现

1.3 java虚拟机栈

java虚拟机栈描述的是该线程执行java方法时的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。栈帧中的局部变量表存储了方法中的基本数据类型变量、对象引用变量。栈内存默认最大1M。

1.4 堆

Java堆是被所有线程共享的一块内存区域。此内存区域用于存放类实例和数组。

1.5 方法区

方法区用于存储JVM加载的类信息、final常量、static静态变量等数据,还包含了运行时常量池,主要存放编译期生成的字面量和符号引用(在类加载后存入运行时常量池)。

二、java字节码指令

2.1 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。

load和store

●load 命令:用于将局部变量表的指定位置的相应类型变量加载到栈顶

●store命令:用于将栈顶的相应类型数据保入局部变量表的指定位置

指令名称

描述

iload_0

将局部变量表的第1个int型变量入栈

istore_0

将栈顶int数值存入局部变量表的第一个位置

astore_0

栈顶ref对象存入第1局部变量

aload_0

第1个ref型变量进栈

const、push和ldc

●const、push:将相应类型的常量放入栈顶

●ldc:则是从常量池中将常量放入栈顶

指令名称

描述

iconst_1

int型常量1进栈

bipush

byte型常量进栈

ldc

int、float或String型常量从常量池推送至栈顶

2.2 对象创建以及访问指令

字段调用

指令名称

描述

getstatic

获取类的静态字段,将其值压入栈顶

putstatic

给类的静态字段赋值

getfield

获取对象的字段,将其值压入栈顶

putfield

给对象的字段赋值

方法调用

指令名称

描述

invokestatic

用于调用静态方法

invokespecial

用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的default方法

invokevirtual

用于调用非私有实例方法

invokeinterface

用于调用接口方法

invokedynamic

用于调用动态方法

2.3 流程控制指令

指令名称

描述

ifeq

当栈顶int类型数值等于0时跳转

ifne

当栈顶int类型数值不等于0时跳转

if_icmpeq

比较栈顶两int类型数值大小,当前者等于后者时跳转

if_icmpne

比较栈顶两int类型数值大小,当前者不等于后者时跳转

2.4 字节码举例分析

public class ByteCode {

    private int a = 1;

    public int add() {
        int b = 2;
        int c = a+b;
        System.out.println(c);
        return c;
    }
}

public ByteCode();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 1: 0
        line 3: 4

public int add();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1         //stack:最大栈深度,locals:局部变量数,args_size:方法参数长度
         0: iconst_2
         1: istore_1
         2: aload_0
         3: getfield      #2                  // Field a:I
         6: iload_1
         7: iadd
         8: istore_2
         9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: iload_2
        13: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        16: iload_2
        17: ireturn
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 9
        line 9: 16
}

操作数栈以及本地变量表变化分析

 

三、类加载过程

3.1 java基本类型

 在 Java 语言规范中,boolean 类型的值只有两种可能,它们分别用符号“true”和“false”来表示。

在 Java 虚拟机规范中,boolean 类型则被映射成 int 类型。具体来说,“true”被映射为整数 1,而“false”被映射为整数 0。这个编码规则约束了 Java 字节码的具体实现。

修改字节码中boolean的值

public class Foo {
 public static void main(String[] args) {
  boolean flag = true;
  if (flag) System.out.println("Hello, Java!");
  if (flag == true) System.out.println("Hello, JVM!");
 }
}

  public Foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: ifeq          14
         6: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #3                  // String Hello, Java!
        11: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: iload_1
        15: iconst_1
        16: if_icmpne     27
        19: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        22: ldc           #5                  // String Hello, JVM!
        24: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 14
        line 7: 27
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 14
          locals = [ int ]
        frame_type = 12 /* same */
}

boolean字节码修改

java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
 awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm
 java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2                         //将iconst_1替换为iconst_2
         1: istore_1
         2: iload_1
         3: ifeq          14
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #1                  // String Hello, Java!
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: iload_1
        15: iconst_1
        16: if_icmpne     27
        19: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        22: ldc           #2                  // String Hello, JVM!
        24: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 14
          locals = [ int ]
        frame_type = 12 /* same */
}

3.2 类加载

加载,是指查找字节流,并且据此创建类的过程。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。

类加载器双亲委派机制

 

反向委派加载机制

 

三方sqlDriver通过线程上下文类加载器进行加载(Mysql Driver、Oracle Driver等)

 

 

 

3.3 链接

3.3.1 验证阶段

确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全

3.3.2 准备阶段

为类中的静态字段分配内存,并设置默认的初始值,同时还会构造与该类相关的方法表,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译期就将其结果放入了调用它的类的常量池中。

3.3.3 解析阶段

符号引用

在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。

解析阶段的目的,是将常量池内的符号引用转换为直接引用的过程。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

public class ByteCode
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #5                          // ByteCode
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#18         // ByteCode.a:I
   #3 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(I)V
   #5 = Class              #23            // ByteCode
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               a
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               add
  #14 = Utf8               (I)I
  #15 = Utf8               SourceFile
  #16 = Utf8               ByteCode.java
  #17 = NameAndType        #9:#10         // "<init>":()V
  #18 = NameAndType        #7:#8          // a:I
  #19 = Class              #25            // java/lang/System
  #20 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #21 = Class              #28            // java/io/PrintStream
  #22 = NameAndType        #29:#30        // println:(I)V
  #23 = Utf8               ByteCode
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (I)V

3.3.4 初始化阶段

类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

主动初始化

●当虚拟机启动时,初始化用户指定的主类;

●当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

●当遇到访问或者调用静态方法的指令时,初始化该静态方法所在的类;

●子类的初始化会触发父类的初始化;

●如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

●使用反射 API 对某个类进行反射调用时,初始化这个类;

●当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

被动初始化

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

●通过数组定义来引用类,不会触发此类的初始化。

●常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

问题:新建数组,会触发此类的链接吗

public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
    static {
      System.out.println("LazyHolder.<clinit>");
    }
  }
  public static Object getInstance(boolean flag) {
    if (flag) return new LazyHolder[2];
    return LazyHolder.INSTANCE;
  }
  public static void main(String[] args) {
    getInstance(true);
    System.out.println("----");
    getInstance(false);
  }
}
{
  public static java.lang.Object getInstance(boolean);
    descriptor: (Z)Ljava/lang/Object;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: ifeq          9
         4: iconst_2
         5: anewarray     #2                  // class Singleton$LazyHolder
         8: areturn
         9: getstatic     #3                  // Field Singleton$LazyHolder.INSTANCE:LSingleton;
        12: areturn
      LineNumberTable:
        line 10: 0
        line 11: 9
      StackMapTable: number_of_entries = 1
        frame_type = 9 /* same */

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #4                  // Method getInstance:(Z)Ljava/lang/Object;
         4: pop
         5: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #6                  // String ----
        10: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: iconst_0
        14: invokestatic  #4                  // Method getInstance:(Z)Ljava/lang/Object;
        17: pop
        18: return
      LineNumberTable:
        line 14: 0
        line 15: 5
        line 16: 13
        line 17: 18
}

java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Singleton\$LazyHolder.class > Singleton\$LazyHolder.jasm.1
  
awk 'NR==1,/stack 1/{sub(/stack 1/, "stack 0")} 1' Singleton\$LazyHolder.jasm.1 > Singleton\$LazyHolder.jasm

java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Singleton\$LazyHolder.jasm

java -verbose:class Singleton

[0.101s][info][class,load] java.lang.PublicMethods$MethodList source: jrt:/java.base
[0.101s][info][class,load] java.lang.PublicMethods$Key source: jrt:/java.base
[0.102s][info][class,load] java.lang.Void source: jrt:/java.base
[0.102s][info][class,load] Singleton$LazyHolder source: file:/Users/wiam/project/jvm-learn/
[0.102s][info][class,load] java.lang.Readable source: jrt:/java.base
[0.102s][info][class,load] java.nio.CharBuffer source: jrt:/java.base
[0.103s][info][class,load] java.nio.HeapCharBuffer source: jrt:/java.base
[0.103s][info][class,load] java.nio.charset.CoderResult source: jrt:/java.base
----
[0.103s][info][class,load] java.lang.VerifyError source: jrt:/java.base
Exception in thread "main" [0.103s][info][class,load] java.lang.Throwable$PrintStreamOrWriter source: jrt:/java.base
[0.103s][info][class,load] java.lang.Throwable$WrappedPrintStream source: jrt:/java.base
[0.104s][info][class,load] java.util.IdentityHashMap source: jrt:/java.base
[0.104s][info][class,load] java.util.IdentityHashMap$KeySet source: jrt:/java.base
java.lang.VerifyError: Operand stack overflow
Exception Details:
  Location:
    Singleton$LazyHolder.<init>()V @0: aload_0
  Reason:
    Exceeded max stack size.
  Current Frame:
    bci: @0
    flags: { flagThisUninit }
    locals: { uninitializedThis }
    stack: { }
  Bytecode:
    0000000: 2ab7 0007 b1

	at Singleton.getInstance(Singleton.java:11)
	at Singleton.main(Singleton.java:16)
[0.104s][info][class,load] jdk.internal.misc.TerminatingThreadLocal$1 source: jrt:/java.base
[0.104s][info][class,load] java.lang.Shutdown source: jrt:/java.base
[0.104s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base

Singleton\$LazyHolder.jasm内容

super class Singleton$LazyHolder
        version 55:0
{

static final Field INSTANCE:"LSingleton;";

private Method "<init>":"()V"
        stack 0 locals 1                           //不符合jvm规范
{
                aload_0;
                invokespecial   Method java/lang/Object."<init>":"()V";
                return;
}
          static Method "<clinit>":"()V"
        stack 2 locals 0
{
                new     class Singleton;
                dup;
                invokespecial   Method Singleton."<init>":"()V";
                putstatic       Field INSTANCE:"LSingleton;";
                getstatic       Field java/lang/System.out:"Ljava/io/PrintStream;";
                ldc     String "LazyHolder.<clinit>";
                invokevirtual   Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
                return;
}

新建数组不会链接元素类LazHolder,在getInstance(false)时才真正链接和初始化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值