jvm的类加载

文章目录

    • 概要
    • 加载
      • 类加载器分类
      • 双亲委派模型
      • 自定义加载器
    • 验证
    • 准备
    • 解析
    • 初始化
      • <cinit>与<init>

概要

jvm运行时的整体结构如下
在这里插入图片描述
一个Car类,类跟Car对象的转换过程如下:
在这里插入图片描述

  • 加载后的class类信息存放于方法区;
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
  • 如果调用构造器实例化对象,则该对象存放在堆区;

其中类的加载总体流程如下:
在这里插入图片描述

加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

加载是类加载的第一个阶段。有两种时机会触发类加载

  • 预加载
    虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常
    常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起加载
  • 运行时加载
    虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类

那么,加载阶段做了什么,其实加载阶段做了有三件事情:

  • 获取.class文件的二进制流
  • 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
  • 在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的

类加载器分类

jvm提供了3个系统加载器,分别是Bootstrp loaderExtClassLoaderAppClassLoader

在这里插入图片描述

这三个加载器互相成父子继承关系

  • Bootstrp loader: Bootstrp加载器是用C++语言写的,它在Java虚拟机启动后初始化;它主要负责加载以下路径的文件:
    %JAVA_HOME%/jre/lib/*.jar
    %JAVA_HOME%/jre/classes/*
    -Xbootclasspath参数指定的路径
    可通过:System.out.println(System.getProperty("sun.boot.class.path"));打印查看

  • ExtClassLoaderExtClassLoader是用Java写的,具体来说就是sun.misc.Launcher$ExtClassLoader,其主要加载:
    %JAVA_HOME%/jre/lib/ext/
    ext下的所有classes目录
    java.ext.dirs系统变量指定的路径中类库
    可通过:System.getProperty("java.ext.dirs")打印查看

  • AppClassLoader: AppClassLoader也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的就是它。
    负责加载 -classpath 所指定的位置的类或者是jar文档,也是Java程序默认的类加载器
    System.getProperty("java.class.path")

双亲委派模型

什么是双亲委派

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。

为什么需要双亲委派模型?

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父加载器已经加载了该类时,就没有必要子加载器再加载一次。
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委派模型传递到启动类加载器,而启动类加载器发现这个名字的类,发现该类已被加载,就不会重新加载网络传递过来的 java.lang.Integer ,而直接返回已加载过的Integer.class ,这样便可以防止核心API库被随意篡改。

双亲委派能否打破?
可以的,比如在tomcat中,tomcat通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则,
看一下tomcat类加载的层次结构如下:
在这里插入图片描述

比如:Tomcat的 webappClassLoader 加载web应用下的class文件,不会传递给父类加载器,问题:tomcat的类加载器为什么要打破该模型?
首先一个tomcat启动后是会起一个jvm进程的,它支持多个web应用部署到同一个tomcat里,为此

  • 对于不同的web应用中的class和外部jar包,需要相互隔离,不能因为不同的web应用引用了相同的jar或者有相同的class导致一个加载成功了另一个加载不了。
  • web容器支持jsp文件修改后不用重启,jsp文件也是要编译成.class文件的,每一个jsp文件对应一个JspClassLoader,它的加载范围仅仅是这个jsp文件所编译出来的那一个.class文件,当Web容器检测到jsp文件被修改时,会替换掉目前JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热部署功能。

如何实现双亲委派模型

双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了,我们无需去写

几个重要的函数

loadClass 默认实现如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}

再看看 loadClass(String name, boolean resolve) 函数:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

从上面代码可以明显看出, loadClass(String, boolean) 函数即实现了双亲委派模型!整个大致过程如下:

  • 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  • 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用 parent.loadClass(name, false); ).或者是调用 bootstrap 类加载器来加载
  • 如果父加载器及 bootstrap 类加载器都没有找到指定的类,那么调用当前类加载器的 findClass 方法来完成类加载。

也就是说,如果要自定义类加载器,就要重写fiindClass方法。

抽象类 ClassLoaderfindClass 函数默认是抛出异常的。而前面我们知道, loadClass 在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的 findeClass 函数,因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象

自定义加载器

除了上面的系统提供的3种loader,jvm允许自己定义类加载器,典型的在tomcat上:
在这里插入图片描述

为什么要自定义类加载器:

  • 隔离加载类
    模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载器,用于隔离web应用服务器上的不同应用程序。
  • 修改类加载方式
    除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。
  • 扩展加载源
    比如还可以从数据库、网络、或其他终端上加载
  • 防止源码泄露
    java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码

自定义类加载器的加载流程

在这里插入图片描述
自定义加载器:

public class MyClassLoader extends ClassLoader {private String codePath;protected MyClassLoader(ClassLoader parent, String path) {super(parent);this.codePath = path;}public MyClassLoader(String classPath) {this.codePath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {String fileName = codePath + name + ".class";try (   // 输入流BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));// 输出流ByteArrayOutputStream baos = new ByteArrayOutputStream()) {int len;byte[] data = new byte[1024];while ((len = bis.read(data)) != -1) {baos.write(data, 0, len);}//5.获取内存中字节数组byte[] byteCode = baos.toByteArray();//6.调用defineClass 将字节数组转成Class对象Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);return defineClass;} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}
}

有以下注意点:

  • 所有用户自定义类加载器都应该继承ClassLoader类

  • 在自定义ClassLoader的子类是,我们通常有两种做法:

    • 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
    • 重写findClass方法 (推荐)

验证

连接阶段的第一步,这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
Java语言本身是相对安全的语言(相对C/C++来说),但是前面说过,.class文件未必要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上,Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段主要做以下几方面的工作:

  • 文件格式验证:是不是CAFEBABYE开头,主次版本号是否在当前jvm虚拟机可运行的范围内等
  • 元数据验证:段主要验证属性、字段、类关系、方法等是否合规
  • 字节码验证:这里主要验证class里定义的方法,看方法内部的code是否合法
  • 符号引用验证:字节码里有的是直接引用,有的是指向了其他的字节码地址。而符号引用验证的就是,这些引用的对应的内容是否合法

准备

准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。关于这点,有两个地方注意一下

  • 这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
  • 这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如public static int value = 123value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如public static final int value =123;就不一样了,在准备阶段,虚拟机就会给value赋值为123。

各个数据类型的零值如下表:
在这里插入图片描述

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。来了解一下符号引用和直接引用有什么区别:

符号引用
符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:

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

看一段代码:

public class TestMain {private static int i;private double d;public static void print() {}private boolean trueOrFalse() {return false;}
}

反编译后得到:

Constant pool:#1 = Methodref          #3.#17         // java/lang/Object."<init>":()V#2 = Class              #18            // com/ocean/classloading/TestMain#3 = Class              #19            // java/lang/Object#4 = Utf8               i#5 = Utf8               I#6 = Utf8               d#7 = Utf8               D#8 = Utf8               <init>#9 = Utf8               ()V#10 = Utf8               Code#11 = Utf8               LineNumberTable#12 = Utf8               print#13 = Utf8               trueOrFalse#14 = Utf8               ()Z#15 = Utf8               SourceFile#16 = Utf8               TestMain.java#17 = NameAndType        #8:#9          // "<init>":()V#18 = Utf8               com/ocean/classloading/TestMain#19 = Utf8               java/lang/Object

可以看到常量池中有22项内容,其中带"Utf8"的就是符号引用。比如#2,它的值是"com/ocean/classloading/TestMain",表示的是这个类的全限定名;又比如#4为i,#5为I,它们是一对的,表示变量时Integer(int)类型的,名字叫做i;#12、#16表示的都是方法的名字。

符号引用就是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。

直接引用

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同

解析阶段负责把整个类激活,串成一个可以找到彼此的网。那这个阶段都做了哪些工作呢?大体可以分为:

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析

初始化

最后一个步骤,经过这个步骤后,类信息完全进入了jvm内存,直到它被垃圾回收器回收。
前面几个阶段都是虚拟机来搞定的。我们也干涉不了,从代码上只能遵从它的语法要求。而这个阶段,是赋值,才是我们应用程序中编写的有主导权的地方

初始化阶段就是执行类构造器()方法的过程。 ()方法并不是程序员在Java代码中直接编写 的方法, 它是Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的 语句合并产生的。

()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法) 不同, 它不需要显 式地调用父类构造器, Java虚拟机会保证在子类的()方法执行前, 父类的()方法已经执行 完毕。 因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object

由于父类的()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值 操作

()方法对于类或接口来说并不是必需的, 如果一个类中没有静态语句块, 也没有对变量的 赋值操作, 那么编译器可以不为这个类生成()方法。 接口中不能使用静态语句块, 但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成 ()方法
但接口与类不同的是, 执行接口的()方法不需要先执行父接口的()方法, 因为只有当父接口中定义的变量被使用时, 父接口才会被初始化。 此外, 接口的实现类在初始化时也 一样不会执行接口的()方法

Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步, 如果多个线程同 时去初始化一个类, 那么只会有其中一个线程去执行这个类的()方法, 其他线程都需要阻塞等 待, 直到活动线程执行完毕()方法。 如果在一个类的()方法中有耗时很长的操作, 那就 可能造成多个进程阻塞, 在实际应用中这种阻塞往往是很隐蔽的

class TestDeadLoop {static class DeadLoopClass {static {
// 如果不加上这个if语句, 编译器将提示“Initializer does not complete normally”并拒绝编译if (true) {System.out.println(Thread.currentThread() + "init DeadLoopClass");while (true) {}}}}public static void main(String[] args) {Runnable script = new Runnable() {public void run() {System.out.println(Thread.currentThread() + "start");DeadLoopClass dlc = new DeadLoopClass();System.out.println(Thread.currentThread() + " run over");}};Thread thread1 = new Thread(script);Thread thread2 = new Thread(script);thread1.start();thread2.start();}
}

上面说的()方法可以理解为是<cinit>,对象的初始化方法(构造函数),也就是反编译之后看到方法是<init>,这两者什么区别呢?
看一段代码

public class ParentA {static {System.out.println("1");}public ParentA() {System.out.println("2");}
}public class SonB extends ParentA {static {System.out.println("a");}public SonB() {System.out.println("b");}public static void main(String[] args) {ParentA ab = new SonB();ab = new SonB();}}

上面的打印结果是:

1
a
2
b
2
b

其中 static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的 static 代码块只会执行一次,它对应的是 <cinit>方法。

在这里插入图片描述

所以,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继承关系的先后原则,不难分析出正确结果
小结:
方法<cinit>的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作) ,只执行一次
方法 <init>的执行时期: 对象的初始化阶段

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/331681.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ vector类

目录 0.前言 1.vector介绍 2.vector使用 2.1 构造函数(Constructor) 2.1.1. 默认构造函数 (Default Constructor) 2.1.2 填充构造函数 (Fill Constructor) 2.1.3 范围构造函数 (Range Constructor) 2.1.4 拷贝构造函数 (Copy Constructor) 2.2 迭代器(Iterator) 2.2.…

多项式重构的平滑和法线估计-------PCL

多项式重构的平滑和法线估计 /// <summary> /// 多项式重构的平滑和法线估计 /// </summary> /// <param name"cloud"></param> /// <returns>输出一个包含平滑后的点云数据以及相应法线信息的数据结构</returns> pcl::PointCl…

《计算机网络微课堂》课程概述

​ 课程介绍 本专栏主要是 B 站课程《计算机网络微课堂》的文字版&#xff0c;作者是湖南科技大学的老师。 B 站地址&#xff1a;https://www.bilibili.com/video/BV1c4411d7jb 该课程好评如潮&#xff0c;包含理论课&#xff0c;实验课&#xff0c;考研真题分析课&#xf…

阅读笔记——《ProFuzzBench: A Benchmark for Stateful Protocol Fuzzing》

【参考文献】Natella R, Pham V T. Profuzzbench: A benchmark for stateful protocol fuzzing[C]//Proceedings of the 30th ACM SIGSOFT international symposium on software testing and analysis. 2021: 662-665.【注】本文仅为作者个人学习笔记&#xff0c;如有冒犯&…

Day01-Web开发、介绍、HTML

一、什么是 Web ? Web:全球广域网&#xff0c;也称为万维网(www World Wide Web)&#xff0c;能够通过浏览器访问的网站。 <!-- 文档类型为HTML --> <!DOCTYPE html> <html lang"en"> <head><!-- 字符集 --><meta charset"U…

移动端开发 笔记01

目录 01 移动端的概述 02 移动端的视口标签 03 开发中的二倍图 04 流式布局 05 弹性盒子布局 01 移动端的概述 移动端包括:手机 平板 便携式设备 目前主流的移动端开发: 安卓设备 IOS设备 只要移动端支持浏览器 那么就可以使用浏览器开发移动端项目 开发移动端 使用…

AI视频教程下载:全面掌握ChatGPT和LangChain开发AI应用(附源代码)

这是一门深入的课程&#xff0c;涉及ChatGPT、LangChain和Python。打造专注于现实世界AI集成的AI应用&#xff0c;课件附有每一节涉及到的源代码。 **你将学到什么&#xff1a;** - 将ChatGPT集成到LangChain的生产风格应用中 - 使用LangChain组件构建复杂的文本生成管道 - …

开放式耳机哪个品牌音质好用又实惠耐用?五大公认卷王神器直入!

​在现今耳机市场&#xff0c;开放式耳机凭借其舒适的佩戴体验和独特的不入耳设计&#xff0c;备受消费者追捧。它们不仅让你在享受音乐时&#xff0c;仍能察觉周围的声音&#xff0c;确保与人交流无障碍&#xff0c;而且有利于耳朵的卫生与健康。对于运动爱好者和耳机发烧友而…

源码编译安装LAMP

LAMP架构 LAMP架构是目前成熟的企业网站应用模式之一&#xff0c;指的是协同工作的一整套系统和相关软件&#xff0c;能够提供动态Web站点服务及其应用开发环境。LAMP是一个缩写词&#xff0c;具体包括Linux操作系统、Apache网站服务器、MySQL数据库服务器、PHP&#xff08;或…

sqlserver的查询(三)

目录 10. group by(分组) 11. having(对分组后的信息过滤) 可能从这里开始&#xff0c;执行顺序越来越显得重要了&#xff01;&#xff01;&#xff01; 10. group by(分组) 这个查询相比前面会有一些困难&#xff1b; 格式&#xff1a;group by 字段的集合&#xff1b; 功…

Maven多环境打包配置

一、启动时指定环境配置文件 在启动springboot应用的jar包时&#xff0c;我们可以指定配置文件&#xff0c;通常把配置文件上传到linux服务器对应jar包的同级目录&#xff0c;或者统一的配置文件存放目录 java -jar your-app.jar --spring.config.location/opt/softs/applicat…

NodeJS安装并生成Vue脚手架(保姆级)

文章目录 NodeJS下载配置环境变量Vue脚手架生成Vue脚手架创建项目Vue项目绑定git 更多相关内容可查看 NodeJS下载 下载地址&#xff1a;https://nodejs.org/en 下载的速度应该很快&#xff0c;下载完可以无脑安装&#xff0c;以下记得勾选即可 注意要记住自己的安装路径&…

Linux--线程的认识(一)

线程的概念 线程&#xff08;Thread&#xff09;是操作系统中进行程序执行的最小单位&#xff0c;也是程序调度和分派的基本单位。它通常被包含在进程之中&#xff0c;是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流&#xff0c;一个进程中可以并发多个线…

Redis内存回收-内存淘汰策略

LFU的访问次数之所以叫做逻辑访问次数&#xff0c;是因为并不是每次key被访问都计数&#xff0c;而是通过运算&#xff1a; 生成0~1之间的随机数R计算 (旧次数 * lfu_log_factor 1)&#xff0c;记录为P如果 R < P &#xff0c;则计数器 1&#xff0c;且最大不超过255访问…

二叉树详解

目录 一、二叉树的实现 1.1 二叉树的前序遍历 1.2 二叉树的中序遍历 1.3 二叉树的后续遍历 1.4 二叉树的节点个数 1.5 二叉树叶子节点个数 1.6 二叉树查找值为x的节点 1.7 二叉树第k层节点个数 1.8 二叉树的高度 1.9 二叉树的销毁 二、代码展示 BTNode.h BTNode.c 最后 一…

skynet.newservice简介:服务的启动

skynet是一个轻量级的游戏服务器框架。 简介 在skynet的体系中&#xff0c;服务是一个基础概念。通常&#xff0c;我们使用skynet.newservice来启动一个snlua服务。 那么&#xff0c;当我们写下local addr skynet.newservice("test")这行代码时&#xff0c;系统是怎…

【Java Web】前端利用 form 表单传多项数据,后端 Servlet 取出的各项数据均为空

前端利用 form 表单传多项数据&#xff0c;后端 Servlet 取出的各项数据均为空 文章目录 1.问题引入2.问题解决 1.问题引入 最近在写一个 java web 项目时&#xff0c;遇到一个让我头疼了一下午的问题&#xff1a;前端通过 post 提交的 form 表单数据可以传到后端&#xff0c…

Windows远程连接命令?

Windows操作系统提供了多种远程连接命令&#xff0c;使用户可以通过网络连接到远程计算机&#xff0c;并在远程操作系统上执行操作。远程连接命令可方便实现远程工作、故障排查和系统维护等任务。本文将介绍几种常见的Windows远程连接命令及其基本使用方法。 远程连接命令 Win…

心链2---前端开发(整合路由,搜索页面,用户信息页开发)

心链——伙伴匹配系统 接口调试 说书人&#x1f4d6;&#xff1a;上回书说到用了两种方法查询标签1.SQL查询&#xff0c;2.内存查询&#xff1b;两种查询效率是部分上下&#xff0c;打的是难解难分&#xff0c;是时大地皴裂&#xff0c;天色聚变&#xff0c;老祖斟酌再三最后决…

仪器校准中移液器的使用规范,应当注意哪些细节?

校准行业中&#xff0c;移液器的使用是非常多的&#xff0c;尤其是理化室&#xff0c;经常需要借助到移液器来校准。作为常规的溶液定量转移器具&#xff0c;其在校准过程中的使用也需要遵守规范&#xff0c;既是保证校准结果准确低误差&#xff0c;也是为了规范实验室校准人员…