理解RPC

in Architecture

什么是RPC(Remote procedure call)

Remote procedure call is the synchronous language-level transfer of control between programs in address spaces whose primary communication is a narrow channel.—— Bruce Jay Nelson,Remote Procedure Call,Xerox PARC,1981

RPC是一种语言级别的通讯协议,它允许运行于一台计算机上的程序以某种管道作为通讯媒介(即某种网络传输协议),去调用另外一个地址空间(通常为网络上的另外一台计算机)。

所有RPC协议都需要解决哪些问题?

基于 TCP/IP 网络的、支持 C 语言的 RPC 协议,后来也被称为是ONC RPC(Open Network Computing RPC/Sun RPC)。这两个RPC鼻祖。

  1. 如何表示数据
  2. 如何传递数据
  3. 如何标识方法

如何表示数据

将交互双方涉及的数据,转换为某种事先约定好的中立数据流格式来传输,将数据流转换回不同语言中对应的数据类型来使用,即序列化与反序列化

如何传递数据

这里“传递数据”通常指的是应用层协议,实际传输一般是基于标准的 TCP、UDP 等传输层协议来完成的。

如何标识方法

标识调用方法入口,能够找到这个方法。

已经实现的RPC框架

RPC 框架有明显朝着更高层次(不仅仅负责调用远程服务,还管理远程服务)与插件化方向发展的趋势,不再选择自己去解决表示数据、传递数据和表示方法这三个问题,而是将全部或者一部分问题设计为扩展点,实现核心能力的可配置,再辅以外围功能,如负载均衡、服务注册、可观察性等方面的支持。这一类框架的代表,有 Facebook 的 Thrift 和阿里的 Dubbo(现在两者都是 Apache 的)。尤其是断更多年后重启的 Dubbo 表现得更为明显,它默认有自己的传输协议(Dubbo 协议),同时也支持其他协议,它默认采用 Hessian 2 作为序列化器,如果你有 JSON 的需求,可以替换为 Fastjson;如果你对性能有更高的需求,可以替换为Kryo、FST、Protocol Buffers 等;如果你不想依赖其他包,直接使用 JDK 自带的序列化器也可以。这种设计,就在一定程度上缓解了 RPC 框架必须取舍,难以完美的缺憾。

RMI(Sun/Oracle)、Thrift(Facebook/Apache)、Dubbo(阿里巴巴 /Apache)、gRPC(Google)、Motan2(新浪)、Finagle(Twitter)、brpc(百度)、.NET Remoting(微软)、Arvo(Hadoop)、JSON-RPC 2.0(公开规范,JSON-RPC 工作组)

Comment and share

Java执行流转图

img

Java8内存布局

Image

JVM内存与本地内存的区别

  • JVM内存

    • 受JVM内存大小限制,当大小超过参数设置的大小就会报OOM
  • 本地内存

    • 本地内存不受虚拟机内存参数限制,只受物理容量限制
    • 虽然不受参数限制,但是如果内存占用超过了物理内存大小,也会报OOM

Java运行时数据区域

Insert picture description here

  • 程序计数器(PC Registers)

    JVM Program counter 当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行需要执行的指令。每个线程都有自己的程序计数器。所以程序计数器的结构如下:

    Insert picture description here
    • 程序计数器只占用很小的内存空间,所以可以被忽略。它也是存储最快的空间。
    • JVM规定每个线程都有自己的程序计数器,它的生命周期就是随着线程的执行周期。
    • 线程中任何时候都只有一个叫做当前方法。程序计数器里存储了当前线程正在执行的方法的指令地址。如果是本地方法正在执行则是undefined value。
    • 程序的分支,循环,跳转,异常处理都需要程序计数器控制。
    • 当字节码解释器工作时,它通过改变这个计数器的值来选择下一条要执行的字节码指令。
    • 程序计数器是对物理寄存器的抽象。
    • 它是 Java 虚拟机规范中唯一没有指定任何 outOtMemoryError 条件的区域。 (没有 GC,OOM)
    • 参考原文
  • 虚拟机栈(JVM Stacks)

    虚拟机栈是线程私有的,随线程毁灭,结构示意图:

    Image

每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧(stack frame)

每个栈帧的包含: 局部变量表中存储着方法里的java基本数据类型, 局部变量表, 操作数栈, 动态连接, 方法返回地址

  • 线程创建入栈,线程结束出栈。

虚拟机栈可能会抛出两种异常:

- 如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢出
- 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
  • 本地方法栈

    • 线程私有执行native方法
  • Java堆

    • 对象实例
      • 类初始化生成的对象
      • 基本类型的数组也是对象实例
    • 字符串常量池
      • 字符串常量池原本存放于方法区,jdk7开始放置于堆中
      • 字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
    • 静态变量
      • 静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
    • 线程分配缓冲区(Thread Local Allocation Buffer)
      • 线程私有,但是不影响java堆的共性
      • 增加线程分配缓冲区是为了提升对象分配时的效率

方法区(Method Area)

方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

  • 类元信息

    • 类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table)
    • 常量池表(Constant Pool Table)存储了类在编译期间生成的字面量、符号引用(什么是字面量?什么是符号引用?),这些信息在类加载完后会被解析到运行时常量池中
  • 运行时常量池(Runtime Constant Pool)

    • 运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些
    • 运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法

直接内存

在jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据。

FAQ

类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?

类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。

在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。

Comment and share

类加载器从应用程序和Java API加载类文件。只有运行中的程序实际需要Java API中的类的时候才会被加载到虚拟机。

img

加载

  • 加载是指查找字节流,并且据此创建类的过程。除了数组类没有对应的字节流是由JVM直接生成的。其他类,JVM则需要借助类加载器来完成查找字节流的过程。
img
  • 在JVM中,类的唯一性是由类加载器实例以及类的全名一同决定的。即便是同一串字节流,经由不同的类加载器加载,也会得到不同的类。在大型应用中我们往往借助这一特性,来运行同一个类的不同版本。

    启动类加载器(Bootstrap class loader)

  1. 由C++实现,没有对应的Java对象,因此在Java中只能用null来指代。
  2. 除了Bootstrap类加载器其他类加载器都是ClassLoader的子类,因此有对应的Java对象。这些类加载器都必须先由另外一个类加载器,比如启动类加载器加载至JVM当中,方能执行类加载。
  3. Java9之前启动类加载器只负责加载最为基础,最为重要的类:JRE的lib目录下的jar(以及虚拟机参数 -Xbootclasspath指定的类)。

扩展类加载器(Extension class loader)

  1. 扩展类加载器的父类加载器是启动类加载器。
  2. 负责加载相对次要、但又通用的类,比如存放在JRE的lib/ext目录下的jar以及由系统变量java.ext.dirs指定的类。

应用类加载器(Application class loader)

  1. 应用类加载器的父类加载器是扩展类加载器。
  2. 负责加载应用程序路径下的类。虚拟机参数 -cp/-classpath,系统变量 java.class.path或者环境变量CLASSPATH所指定的类。

Java 9引入了模块系统,并且略微更改了上述类加载器。扩展类加载器被更改为**平台类加载器(platform class loader)**。Java SE中除了少数几个关键模块,比如java.base是由启动类加载器加载之外,其他模块均由平台类加载器所加载。

自定义类加载器

除了JVM提供的类加载器外,可以自定义类加载器,通过继承ClassLoader类实现,主要重写findClass方法。实现特殊的加载方式。举例:对class文件进行加密,加载时再利用自定义类加载器对其进行解密。

双亲委派模型

在JVM中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给自己的父类加载器完成。每个类加载器都是如此,只有当自己的父类加载器在自己负责加载的范围内找不到指定类(ClassNotFoundException)时,子类才会进行自己去尝试加载。

作用:

  1. 提高安全性:防止覆盖系统类库中的类,提高安全性。
  2. 防止程序混乱:重复加载。
图片描述

链接

验证

确保被加载的类符合JVM规范。

准备

为被加载类中的静态字段分配内存。

解析

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

解析阶段的目的是将符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载的字段或者方法。那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化)。

初始化

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

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;子类的初始化会触发父类的初始化;
  5. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  6. 使用反射 API 对某个类进行反射调用时,初始化这个类;
  7. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

Comment and share

关于写博客

in 杂思

技术人的工作思考

刚开始作为一个coder,可能会好奇各种各样的新技术,进而去探索,然后搁置(因为工作上可能并不会用到)。最后这些知识可能就会藏在脑海深处。

作为一个backend开发,我几乎学习了java周边所有的技术栈。从Java,MySQL,Redis,Spring全家桶,Kafka,K8s,Netty,,,但是说我算精通吗?我的答案是否定的。

  1. 从全局看我缺乏上层架构图,将这些知识串联起来。
  2. 从细节看我没有对技术细节的整理输出。从而会导致捡了西瓜丢了芝麻的尴尬场面。

让博客成为你的学习输出

我层使用过各种笔记软件和博客网站:“有道云笔记”,“Enovy note”, “CSDN blog”,“博客园”,“InfoQ”等等。用了一阵后,我发现这些软件都不能让我保持专注于自己的写作,并且没有版本跟踪。而且在“种类”和“标签”检索上也没有hexo+github page这种更清爽。如图

image-20220503222913905

如何写好博客

  1. 使用markdown,这是技术人的必备第二语言。

  2. 为你的博客起一个好名字

    为博客起一个好名字,更能让你再接下来复盘的时候检索到你这篇博客的目的。所以你的博客名字最好有总结性。

  3. 博客段落设置

    • 解释这篇博客包含哪些内容
    • 切入主题,分1,2,3带上配图
    • 总结
  4. 技术是不断迭代的,记得一定要维护你的博客跟上技术更新的步骤。

持之以恒

好记性不如烂笔头,只有自己的记录才最靠谱。

Comment and share

  • page 1 of 1
Author's picture

Topsion

Fullstack Developer


Coder


Xi'an China