首页 > 其他分享 >字节码角度看synchronized和反射的实现原理

字节码角度看synchronized和反射的实现原理

时间:2023-08-14 22:44:12浏览次数:42  
标签:调用 字节 invoke Object reflect synchronized 角度看 public

前几天,关于字节码技术,我们讲了字节码的基础, 常见的字节码框架以及在软件破解和APM链路监控方面的一些应用.

今天我们回到Java本身, 看下我们常用的synchronized关键字和反射在字节码层面是如何实现的.

synchronized

代码块级别的 synchronized

如下方法的内部使用了synchronized关键字

private Object lock = new Object();
public void foo() {
    synchronized (lock) {
        bar();
    }
}

public void bar() { }

编译成字节码如下

 public void foo();
    Code:
       0: aload_0
       1: getfield      #3                  // Field lock:Ljava/lang/Object;
       4: dup
       5: astore_1
       
       6: monitorenter
       
       7: aload_0
       8: invokevirtual #4                  // Method bar:()V
       
      11: aload_1
      12: monitorexit
      13: goto          21
      
      16: astore_2
      17: aload_1
      18: monitorexit
      19: aload_2
      20: athrow
      21: return
    Exception table:
       from    to  target type
           7    13    16   any
          16    19    16   any

Java 虚拟机中代码块的同步是通过 monitorenter 和 monitorexit 两个支持 synchronized 关键字语意的。比如上面的字节码

  • 0 ~ 5:将 lock 对象入栈,使用 dup 指令复制栈顶元素,并将它存入局部变量表位置 1 的地方,现在栈上还剩下一个 lock 对象
  • 6:以栈顶元素 lock 做为锁,使用 monitorenter 开始同步
  • 7 ~ 8:调用 bar() 方法
  • 11 ~ 12:将 lock 对象入栈,调用 monitorexit 释放锁

monitorenter 对操作数栈的影响如下

  • 16 ~ 20:执行异常处理,我们代码中本来没有 try-catch 的代码,为什么字节码会帮忙加上这段逻辑呢?
    因为编译器必须保证,无论同步代码块中的代码以何种方式结束(正常 return 或者异常退出),代码中每次调用 monitorenter 必须执行对应的 monitorexit 指令。为了保证这一点,编译器会自动生成一个异常处理器,这个异常处理器的目的就是为了同步代码块抛出异常时能执行 monitorexit。这也是字节码中,只有一个 monitorenter 却有两个 monitorexit 的原因

可理解为这样的一段 Java 代码

public void _foo() throws Throwable {
    monitorenter(lock);
    try {
        bar();
    } finally {
        monitorexit(lock);
    }
}

根据我们之前介绍的 try-catch-finally 的字节码实现原理,复制 finally 语句块到所有可能函数退出的地方,上面的代码等价于

public void _foo() throws Throwable {
    monitorenter(lock);
    try {
        bar();
        monitorexit(lock);
    } catch (Throwable e) {
        monitorexit(lock);
        throw e;
    }
}

方法级的 synchronized

方法级的同步与上述有所不同,它是由常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

synchronized public void testMe() {
}

对应字节码
public synchronized void testMe();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED

JVM 不会使用特殊的字节码来调用同步方法,当 JVM 解析方法的符号引用时,它会判断方法是不是同步的(检查方法 ACC_SYNCHRONIZED 是否被设置)。如果是,执行线程会先尝试获取锁。如果是实例方法,JVM 会尝试获取实例对象的锁,如果是类方法,JVM 会尝试获取类锁。在同步方法完成以后,不管是正常返回还是异常返回,都会释放锁.

反射

在 Java 中反射随处可见,它底层的原也比较有意思,这篇文章来详细介绍反射背后的原理。

先来看下面这个例子:

public class ReflectionTest {

    private static int count = 0;
    public static void foo() {
        new Exception("test#" + (count++)).printStackTrace();
    }

    public static void main(String[] args) throws Exception {
        Class<?> clz = Class.forName("ReflectionTest");
        Method method = clz.getMethod("foo");
        for (int i = 0; i < 20; i++) {
            method.invoke(null);
        }
    }
}

运行结果如下

可以看到同一段代码,运行的堆栈结果与执行次数有关系,在 0 ~ 15 次调用方式为sun.reflect.NativeMethodAccessorImpl.invoke0,从第 16 次开始调用方式变为了sun.reflect.GeneratedMethodAccessor1.invoke。原因是什么呢?继续往下看。

反射方法源码分析

Method.invoke 源码如下:

可以最终调用了MethodAccessor.invoke方法,MethodAccessor 是一个接口

public interface MethodAccessor {
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException;
}

从输出的堆栈可以看到 MethodAccessor 的实现类是委托类DelegatingMethodAccessorImpl,它的 invoke 函数非常简单,就是把调用委托给了真正的实现类。

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate;
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        return delegate.invoke(obj, args);
    }

通过堆栈可以看到在第 0 ~ 15 次调用中,实现类是 NativeMethodAccessorImpl,从第 16 次调用开始实现类是 GeneratedMethodAccessor1,为什么是这样呢?玄机就在 NativeMethodAccessorImpl 的 invoke 方法中

前 0 ~ 15 次都会调用到invoke0,这是一个 native 的函数。

private static native Object invoke0(Method m, Object obj, Object[] args);

有兴趣的同学可以去看一下 Hotspot 的源码,依次跟踪下面的代码和函数:

./jdk/src/share/native/sun/reflect/NativeAccessors.c
JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)

./hotspot/src/share/vm/prims/jvm.cpp
JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))

./hotspot/src/share/vm/runtime/reflection.cpp
oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS)

这里不详细展开 native 实现的细节。
15 次以后会走新的逻辑,使用 GeneratedMethodAccessor1 来调用反射的方法。MethodAccessorGenerator 的作用是通过 ASM 生成新的类 sun.reflect.GeneratedMethodAccessor1。为了查看整个类的内容,可以使用阿里的 arthas 工具。修改上面的代码,在 main 函数的最后加上System.in.read();让 JVM 进程不要退出。 执行 arthas 工具中的./as.sh,会要求输入 JVM 进程

选择在运行的 ReflectionTest 进程号 7 就进入到了 arthas 交互性界面。执行 dump sun.reflect.GeneratedMethodAccessor1文件就保存到了本地。

来看下这个类的字节码

翻译一下这个字节码,忽略掉异常处理以后的代码如下

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    @Override
    public Object invoke(Object obj, Object[] args)
            throws IllegalArgumentException, InvocationTargetException {
        ReflectionTest.foo();
        return null;
    }
}

那为什么要采用 0 ~ 15 次使用 native 方式来调用,15 次以后使用 ASM 新生成的类来处理反射的调用呢?

一切都是基于性能的考虑。JNI native 调用的方式要比动态生成类调用的方式慢 20 倍,但是又由于第一次字节码生成的过程比较慢。如果反射仅调用一次的话,采用生成字节码的方式反而比 native 调用的方式慢 3 ~ 4 倍。

inflation 机制

因为很多情况下,反射只会调用一次,因此 JVM 想了一招,设置了 15 这个 sun.reflect.inflationThreshold 阈值,反射方法调用超过 15 次时(从 0 开始),采用 ASM 生成新的类,保证后面的调用比 native 要快。如果小于 15 次的情况下,还不如生成直接 native 来的简单直接,还不造成额外类的生成、校验、加载。这种方式被称为 「inflation 机制」。inflation 这个单词也比较有意思,它的字面意思是「膨胀;通货膨胀」。

JVM 与 inflation 相关的属性有两个,一个是刚提到的阈值 sun.reflect.inflationThreshold,还有一个是是否禁用 inflation的属性 sun.reflect.noInflation,默认值为 false。如果把这个值设置成true 的话,从第 0 次开始就使用动态生成类的方式来调用反射方法了,不会使用 native 的方式。

增加 noInflation 属性重新执行上述 Java 代码

java -cp . -Dsun.reflect.noInflation=true ReflectionTest

输出结果为

java.lang.Exception: test#0
        at ReflectionTest.foo(ReflectionTest.java:10)
        at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at ReflectionTest.main(ReflectionTest.java:18)
java.lang.Exception: test#1
        at ReflectionTest.foo(ReflectionTest.java:10)
        at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at ReflectionTest.main(ReflectionTest.java:18)

可以看到,从第 0 次开始就已经没有使用 native 方法来调用反射方法了。

小结

这篇文章主要从字节码角度看了Java中的synchronized和射调用底层的原理,当然还有一些其他比较有意思的语法比如lambda, switch等, 感兴趣的小伙伴也可以从字节码角度去了解一下, 相信你会有很多不一样的收获.

本文由mdnice多平台发布

标签:调用,字节,invoke,Object,reflect,synchronized,角度看,public
From: https://www.cnblogs.com/sharloon/p/17629991.html

相关文章

  • 【字节跳动】9-22秋招测试开发一面面经
    作者:洛枫、1、接口如何保证幂等2、数据库索引种类按数据结构分类可分为:B+tree索引、Hash索引、Full-text索引。按物理存储分类可分为:聚簇索引、二级索引(辅助索引)。按字段特性分类可分为:主键索引、普通索引、前缀索引。按字段个数分类可分为:单列索引、联合索引(复合索引、组合......
  • synchronized的作用?synchronized的原理?
    引言​在并发编程中,为了保证线程安全和数据一致性,Java提供了synchronized关键字来实现对共享资源的同步访问。synchronized关键字可以应用于方法和代码块,它在多线程环境下起到了重要的作用。本文将深入探讨synchronized的作用和原理,并给出相应的代码示例。synchronized的作用sy......
  • synchronized和volatile
    synchronizedsynchronized关键字的主要作用是保证同一时刻,只有一个线程可以执行某一个方法或者一个代码块。包含三个特性:可见行、原子性和禁止代码重排序volatilevolatile关键字的主要作用是让其他线程可以看到最新的值,volatile只能修饰变量。包含三个特征:可见性、原子性和禁......
  • JVM之字节码的编译原理
    JVM之字节码的编译原理Java最初诞生的目的就是为了在不依赖特定的物理硬件和操作系统环境下运行,那么也就是说Java程序实现跨平台我的基石其实就是字节码。Java之所以能够解决程序的安全性问题、跨平台移植性等问题,最主要的原因就是Java源代码的编译结果并非是本地机器指令,而是字......
  • 字、字节、字符、编码方式
    参考文章:详解计算机中的字、字节(Byte)、比特(bit)及它们之间的关系字、字节字由若干个字节组成,一个字节是8个比特bit。字的位数叫做字长,即cpu一次处理二进制代码的位数。换算:1字节(Byte/byte)=8位(Bit/bit)1字符占用的字节不确定(不同的编码方式不同)1KB=1024Byte;1MB=1024KB。。。......
  • 字节跳动基于火山引擎DataLeap的一站式数据治理架构实践
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群在7月22日举行的ArchSummit全球架构师峰会(深圳站)上,来自火山引擎DataLeap的技术专家为大家带来了字节跳动基于火山引擎DataLeap的全域数据治理方案分享。本次分享共分为机遇挑战、字节数......
  • java之Socket通信,Socket服务端返回数据,基本数据类型占用字节数。
    参考:https://blog.csdn.net/qq_43842093/article/details/129964892https://blog.csdn.net/weixin_42408447/article/details/126437276数据类型占用字节数://Java一共有8种基本数据类型://1、int占4字节,取值范围为“-2147483648~2147483647”;//2、short占2字节,......
  • 以太网数据帧详细解析 逐字节分析
    目录UDP段、IP数据包,以太网帧图示测试环境抓包客户端向服务端发送'helloworld’Ethernet_II格式、数据帧首部链路层总长度14B以太网帧图示IP协议数据包首部网络层总长度20B+实例TCP协议头传输层图示总长度20B+实例附录UDP协议例题讲解UDP段、IP数据包,以太网帧图示通信......
  • 将中文汉字转为字节数组
     ///<summary>       ///将中文汉字转为字节数组       ///</summary>       ///<paramname="chineseStr"></param>       ///<returns></returns>       publicstaticbyte[]parseChineseToByte(stringchineseStr)......
  • 《流畅的Python第二版》读书笔记——文本和字节序列
    引言这是《流畅的Python第二版》抢先版的读书笔记。Python版本暂时用的是python3.8。为了使开发更简单、快捷,本文使用了JupyterLab。Python3明确区分了人类可读的字符串和原始的字节序列。新内容简介新增了对emoji表示字符的描述。字符问题字符串是个简单的概念:一个字符串是一个字......