Java虚拟机原理(上)-揭秘Java GC黑匣子-知其所以然,从此不再捆手捆脚


对于Java开发者来说,GC(垃圾回收器)就如同一个神秘的黑匣子,它在背后不知疲倦地运作,却也时常给我们带来诸多疑惑和挫折。今天,就让我们切开这个黑匣子,深入解析Java GC的工作原理,助你了解其中的奥秘,从此不再被GC所扰。


这篇文章的主要内容包括:

  1. 对象存活判断及引用类型辨析
  2. 三种主流GC算法的原理和特点
  3. 如何正确解读GC日志

一、对象存活与引用类型


在深入GC算法前,我们先了解一下Java中对象的生命周期。通过判断对象是否可被访问,JVM会决定其是否会被回收。这里涉及一个重要的概念——引用。

1、对象的生命周期


对象的生命周期开始于它的创建,结束于它的回收。在Java中,对象的回收由垃圾回收器(GC)负责,GC会根据引用的类型和数量来决定何时回收对象。
  • 强引用(Strong Reference) - 最普通的对象引用方式,只要存在强引用指向对象,它就不会被GC回收。

  • 软引用(Soft Reference) - 有时候用于实现内存敏感的高速缓存,当内存不足时会被GC回收。

  • 弱引用(Weak Reference) - 如WeakHashMap所使用,即使没被GC回收,也可能会被回收。

  • 虚引用(Phantom Reference) - 最弱的引用,唯一目的是能在对象被GC回收时收到系统通知。

理解了引用类型,我们就能判断对象在何种情况下会被回收。比如,当一个对象没有任何强引用指向它,那它就处于可被回收状态。


### 2、强引用(Strong Reference)
  • 定义: 强引用是最常见的引用类型,如果一个对象具有强引用,那么它永远不会被垃圾回收器回收,直到这个引用被显式地设置为null

  • 示例

Object obj = new Object();
// obj是一个强引用,只要obj存在,对象就不会被回收

3、软引用(Soft Reference)


  • 定义: 软引用用来描述一些有用但非必需的对象。当系统内存不足时,这些对象会被垃圾回收器回收。
  • 用途: 软引用通常用于实现内存敏感的缓存。
  • 示例
import java.lang.ref.SoftReference;Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
obj = null; // 显式地清空强引用// 当系统内存不足时,软引用指向的对象可能会被回收
if (softRef.get() == null) {System.out.println("对象已被回收");
}

4、弱引用(Weak Reference)


  • 定义: 弱引用不足以阻止对象的垃圾回收。也就是说,只要垃圾回收器发现了弱引用,不管当前内存是否足够,都会回收其指向的对象。
  • 用途: 弱引用通常用于监听对象的消失,例如,用于实现弱键(WeakHashMap)。
  • 示例
import java.lang.ref.WeakReference;Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null; // 显式地清空强引用// 对象几乎立即会被回收,因为弱引用不会阻止垃圾回收
if (weakRef.get() == null) {System.out.println("对象已被回收");
}

5、虚引用(Phantom Reference)


  • 定义: 虚引用是最弱的一种引用类型。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。
  • 用途: 虚引用主要用于在对象被回收后收到一个系统通知,用来跟踪对象被垃圾回收的状态。
  • 示例
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, new ReferenceQueue<Object>());
obj = null; // 显式地清空强引用// 虚引用可以注册到引用队列上
// 当对象被回收后,虚引用会被放入引用队列
if (phantomRef.isEnqueued()) {System.out.println("对象已被回收");
}

二、三种主流GC算法原理


JVM的GC算法负责探测并回收上述可回收的对象,以释放内存空间。目前主流的GC算法主要有以下三种:


1、标记-清除(Mark-Sweep)算法原理

这是最基础和常见的GC算法。

(1)、它分两个阶段工作

  • 标记阶段(Marking Phase)

    • 垃圾回收器从根对象(如类变量、局部变量等)开始,递归地访问所有可达的对象。
    • 所有被访问到的对象都会被标记为“存活”。
  • 清除阶段(Sweeping Phase)

    • 在标记阶段结束后,垃圾回收器会遍历堆内存。
    • 对于未被标记的对象,垃圾回收器会认为它们是“垃圾”,并进行回收。
    • 清除后,这些内存空间会被重新整理,为新对象的分配做准备。

(2)、标记-清除算法的问题

  • 内存碎片:清除阶段可能会导致内存碎片,因为回收的对象可能不是连续的。

  • 效率问题:标记和清除阶段可能需要暂停整个应用程序(Stop-The-World,STW),影响性能。


(3)、Java案例代码演示

下面演示了标记-清除算法的工作原理:

public class MarkSweepDemo {public static void main(String[] args) {// 创建一些对象Object obj1 = new Object();Object obj2 = new Object();Object obj3 = new Object();// 假设obj1和obj2是根对象,它们被标记为存活// obj3没有被根对象直接或间接引用,将被标记为垃圾// 模拟标记阶段mark(obj1); // 标记obj1mark(obj2); // 标记obj2// obj3不会被标记// 模拟清除阶段sweep();// 检查对象是否被回收System.out.println("obj1 is alive: " + (obj1 == null ? "No" : "Yes"));System.out.println("obj2 is alive: " + (obj2 == null ? "No" : "Yes"));System.out.println("obj3 is alive: " + (obj3 == null ? "No" : "Yes"));}// 模拟标记过程private static void mark(Object obj) {// 这里只是模拟,实际的标记过程由GC执行System.out.println(obj + " is marked as alive.");}// 模拟清除过程private static void sweep() {// 清除未被标记的对象,这里只是模拟System.out.println("Sweeping phase: clearing unmarked objects.");}
}

在这个示例中,我们创建了三个对象obj1obj2obj3。在模拟的标记阶段,obj1obj2被标记为存活,而obj3没有被标记。在模拟的清除阶段,未被标记的obj3将被“回收”(在实际的Java程序中,对象的回收是由JVM的垃圾回收器自动完成的)。

请注意,这个示例只是为了演示标记-清除算法的原理,并不是实际的Java垃圾回收过程。在实际的Java程序中,你不需要手动进行标记和清除,这些工作都是由JVM自动完成的。


2、复制(Copying)

复制(Copying)算法,也被称为“半区算法”或“新生代算法”,是一种简单且高效的垃圾回收算法,尤其适用于对象生命周期短的场景,如Java中的新生代(Young Generation)。

复制算法将堆内存分为两个相等的区域:一个用于分配新对象(称为From区),另一个用于垃圾回收时的复制操作(称为To区)。

// 初始内存布局
// ----容器1----      ----容器2----
// [obj1,obj2]          []// 进行GC后
// ----容器1----      ----容器2----  
//    []              [obj1,obj2] 

(1)、算法的步骤如下

  • 对象分配

    • 新对象首先在From区分配。
  • 标记阶段

    • 从根对象开始,标记所有存活的对象。这一步与标记-清除算法相同。
  • 复制阶段

    • 将所有存活的对象从From区复制到To区,同时更新所有引用,使其指向To区的新位置。
    • 复制完成后,From区的所有对象都可以被清除。
  • 角色交换

    • 复制完成后,From区和To区的角色互换。新的To区变为新的From区,用于下一次垃圾回收。


(2)、复制算法的优点

  • 内存碎片问题:由于每次回收后都会进行复制,因此不会产生内存碎片。

  • 简单高效:复制算法实现简单,且在对象生命周期短的情况下效率很高。


    (3)、复制算法的缺点

  • 内存空间浪费:由于需要两个区域,因此需要额外的内存空间。

  • 复制开销:复制存活对象需要一定的时间开销。


(4)、Java案例代码演示

下面代码演示了复制算法的工作原理:

public class CopyingGCDemo {public static void main(String[] args) {// 模拟From区和To区Object[] fromArea = new Object[10];Object[] toArea = new Object[10];// 假设fromArea中前5个对象是新分配的for (int i = 0; i < 5; i++) {fromArea[i] = new Object();}// 模拟标记阶段mark(fromArea);// 模拟复制阶段int toIndex = 0;for (int i = 0; i < fromArea.length; i++) {if (fromArea[i] != null) {toArea[toIndex++] = fromArea[i];}}// 角色交换fromArea = toArea;toArea = new Object[10]; // 为下一次回收准备新的To区// 检查对象是否被复制for (int i = 0; i < fromArea.length; i++) {if (fromArea[i] != null) {System.out.println("Object at index " + i + " is alive and copied.");}}}// 模拟标记过程private static void mark(Object[] area) {// 这里只是模拟,实际的标记过程由GC执行for (Object obj : area) {if (obj != null) {System.out.println(obj + " is marked as alive.");}}}
}

在这个示例中,我们使用两个数组fromAreatoArea来模拟From区和To区。我们首先在fromArea中分配了一些新对象,然后模拟了标记阶段,接着将存活的对象复制到toArea,并更新索引。最后,我们交换了fromAreatoArea的角色,准备下一次垃圾回收。

请注意,这个示例只是为了演示复制算法的原理,并不是实际的Java垃圾回收过程。在实际的Java程序中,你不需要手动进行复制,这些工作都是由JVM的垃圾回收器自动完成的。


3、标记-整理(Mark-Compact)

标记-整理算法是垃圾回收中的另一种算法,它结合了标记-清除算法和复制算法的优点,旨在解决清除算法中的内存碎片问题。


(1)、标记-整理算法三个阶段

  • 标记阶段(Marking Phase)

    • 从根对象开始,递归地标记所有可达对象。被标记的对象被认为是存活的。
  • 整理阶段(Compacting Phase)

    • 将所有存活的对象向内存的一端移动,未被标记的对象则被忽略。
    • 移动对象时,会更新所有指向这些对象的引用,确保它们指向新的位置。
  • 清除阶段(Clearing Phase)(可选):

    • 在整理完成后,可能需要清除未被移动的对象占用的空间,以避免内存碎片。
// 初始内存布局
// ----内存区----
// [obj1,  ,obj2, ,  ,  ,obj3]// 标记整理后
// ----内存区----  
// [obj1,obj2,obj3,        ]

(2)、标记-整理算法的优点

  • 内存碎片:通过整理阶段,可以减少或消除内存碎片。
  • 内存利用率:由于整理了存活对象,所以可以更有效地利用内存空间。

(3)、标记-整理算法的缺点

  • 移动开销:移动对象和更新引用需要额外的时间开销。
  • 暂停时间:标记和整理阶段可能需要暂停应用程序,影响性能。

(4)、案例代码演示

以下模拟了标记-整理算法的工作原理。

public class MarkCompactDemo {static class ObjectWithIndex {Object object;int index;ObjectWithIndex(Object object, int index) {this.object = object;this.index = index;}}public static void main(String[] args) {// 假设有5个对象,其中3个是存活的Object[] objects = new Object[5];objects[0] = new Object();objects[1] = new Object();objects[2] = new Object();objects[3] = null; // 假设这个对象是垃圾objects[4] = new Object();// 模拟根引用Object root = objects[0];// 标记阶段mark(objects, root);// 整理阶段int newIndex = 0;for (int i = 0; i < objects.length; i++) {if (objects[i] != null) {objects[newIndex] = objects[i];objects[i] = null; // 清除原位置newIndex++;}}// 清除阶段(可选)// 在实际的GC中,这一步通常由垃圾回收器自动完成// 输出整理后的对象for (int i = 0; i < newIndex; i++) {System.out.println("Object at index " + i + " is alive.");}}private static void mark(Object[] objects, Object root) {// 使用一个集合来记录已访问的对象HashSet<Object> marked = new HashSet<>();// 使用队列模拟递归过程Queue<Object> queue = new LinkedList<>();queue.add(root);while (!queue.isEmpty()) {Object current = queue.poll();if (marked.add(current)) { // 如果对象未被标记for (int i = 0; i < objects.length; i++) {if (objects[i] == current) {System.out.println("Object at index " + i + " is marked as alive.");break;}}// 假设current对象有引用属性,模拟递归标记// 这里简化处理,实际情况需要根据对象的实际引用进行标记}}}
}

在这个示例中,我们创建了一个对象数组objects,其中包含几个对象和一个null。我们模拟了一个根对象root,它引用了数组中的第一个对象。mark方法模拟了标记阶段,使用一个队列和一个集合来递归地标记所有可达对象。然后,我们在main方法中模拟了整理阶段,将所有存活的对象移动到数组的开始位置,并清除了原位置。

请注意,这个示例仅用于演示标记-整理算法的基本原理,实际的Java垃圾回收过程要复杂得多,并且由JVM自动管理。


三、解读GC日志


理解了垃圾回收的原理后,我们来看看如何解读GC日志 。

GC(Garbage Collection)日志是Java虚拟机(JVM)在执行垃圾回收时生成的日志信息,它记录了GC的触发时间、持续时间、回收的内存量、使用的GC算法等信息。通过分析GC日志,我们可以了解应用的内存使用情况,发现潜在的问题和性能瓶颈。


1、如何开启GC日志

在Java应用启动时,可以通过设置JVM参数来开启GC日志:

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
  • PrintGC:打印GC发生的基本日志。
  • PrintGCDetails:打印GC的详细日志。
  • PrintGCDateStamps:在日志中包含日期时间戳。
  • PrintGCTimeStamps:在日志中包含自JVM启动以来的时间戳。

2、GC日志的关键信息

  • 时间戳:GC事件发生的时间。

  • GC类型:如Minor GC(新生代回收)、Full GC(全堆回收)等。

  • 持续时间:GC事件持续的时间。

  • 回收前后的内存使用情况:包括新生代、老年代等内存区域的内存使用情况。

  • GC原因:触发GC的原因,如分配失败、系统内存不足等。


3、解读GC日志示例

假设我们有以下GC日志片段:

2024-05-23T14:37:12.123+0000 [GC [PSYoungGen: 73328K->6336K(94208K)] 73328K->6336K(190464K), 0.003602 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2024-05-23T14:37:12.126+0000 [Full GC [PSYoungGen: 6336K->0K(94208K)] [ParOldGen: 0K->5120K(95296K)] 6336K->5120K(189504K), [Metaspace: 3258K->3258K(1056768K)], 0.006773 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

解读

(1)、时间戳

  • 第一条日志发生在 2024-05-23T14:37:12.123+0000

  • 第二条日志发生在 2024-05-23T14:37:12.126+0000

(2)、GC类型

  • 第一条日志是一次Minor GC。

  • 第二条日志是一次Full GC。

(3)、持续时间

  • 第一次Minor GC持续了 0.003602 秒。

  • 第二次Full GC持续了 0.006773 秒。

(4)、内存使用情况

  • 第一次Minor GC前,新生代使用了 73328K,回收后剩余 6336K
  • 第一次Minor GC后,整个堆使用了 6336K
  • 第二次Full GC前,新生代使用了 6336K,老年代未使用,回收后新生代 0K,老年代 5120K
  • 第二次Full GC后,整个堆使用了 5120K

(5)、GC原因:

  • 第一次Minor GC可能因为新生代空间不足。
  • 第二次Full GC可能因为Minor GC后仍然有内存需求,或者达到了Full GC的条件。

4、监控应用内存使用状况

通过定期检查GC日志,我们可以监控应用的内存使用情况:

  • 频繁的GC:如果GC频繁发生,可能表明内存使用率高,需要优化内存使用。

  • 长时间的GC:长时间的GC可能导致应用响应变慢,需要关注。

  • 内存泄漏:如果老年代的内存持续增长,可能存在内存泄漏。


5、发现潜在问题和性能瓶颈

  • 内存分配率:如果内存分配率持续高于GC回收率,可能导致内存不足。
  • Full GC频率:频繁的Full GC可能影响性能,需要优化。
  • 内存碎片:如果老年代的内存使用不连续,可能导致内存碎片问题。

通过分析GC日志,我们可以对应用的内存使用情况有一个清晰的了解,并据此进行性能调优。在实际的生产环境中,还可以使用专业的监控工具来自动化这一过程,及时发现并解决潜在的问题。


四、结语


以上内容涵盖了GC的常见知识,但Java GC为主题的探讨绝不止于此。比如说,JDK中还引入了全新的ZGC算法,用于低延迟处理;G1作为一种优秀的分代实现,如何工作;怎样有效地配置GC参数…等等,这些都是值得我们去学习和思考的重要话题。


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

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

相关文章

SpringBoot anyline

1、定义通用处理 public interface ClickHouseBaseService extends IService<DataRow> {/*** 根据sql查询数据库&#xff0c;返回集合对象** param sql 执行sql* return {link DataSet} 数据集*/DataSet querys(String sql);/*** 根据sql查询数据库&#xff0c;返回单个…

云手机在软件测试中的作用,为软件测试工程师减负

针对每家企业来说&#xff0c;对于即将上线的软件进行测试这一步骤是不可忽视的&#xff0c;这决定产品上线后的质量和口碑&#xff1b; 传统的的真机测试可能面临设备大量的采购&#xff0c;管理和维护的成本提高&#xff0c;现在不少企业都开始用云手机来代替真机&#xff0…

24.zabbix高可用

环境准备 准备三台机器 主机名字IP地址软件环境zabbix-server01192.168.111.70httpdphpkeepalivedsshpasszabbix serveragentzabbix-server02192.168.111.71httpdphpkeepalivedsshpasszabbix serveragentzabbix-agent192.168.111.80zabbix agentmysql VIP规划&#xff1a;19…

AWS计算之Batch

AWS Batch是亚马逊提供的一项批量计算服务&#xff0c;旨在帮助用户高效地处理大规模的计算工作负载。AWS Batch可以自动调度、运行和监控批处理作业&#xff0c;用户无需管理底层的计算资源&#xff0c;可以专注于编写和提交作业。AWS Batch提供了灵活的配置选项&#xff0c;包…

《Ai-企业知识库》-讨论、构思01

阿丹&#xff1a; 经过几天的Ai学习&#xff0c;开始对于整个大模型&#xff0c;开始有清晰的认知了。开始准备上手操作&#xff0c;编程自己去写一些东西了。 什么是会话AI? 一个计算机程序&#xff0c;允许人类使用各种输入方法&#xff0c;如语音&#xff0c;文字&#x…

使用STM32F103读取TF卡并模拟U盘:使用标准库实现

使用STM32F103读取TF卡并模拟U盘&#xff1a;使用标准库实现 STM32F103是一款流行的ARM Cortex-M3微控制器&#xff0c;在嵌入式系统中广泛应用。本文将介绍如何使用STM32F103读取TF卡&#xff0c;并将其模拟成U盘&#xff0c;让电脑可以读取TF卡的内容。我们将使用标准库&…

Spring常见问题

如何理解spring属于低侵入式设计&#xff1f; 在代码中不需要写明具体依赖对象&#xff0c;在运行时进行自动注入&#xff0c;降低了组件的耦合依赖的是接口&#xff0c;而接口的实现类具有拓展性 Spring IOC 实现了什么功能&#xff0c;谈谈你对IOC的理解。 负责创建对象&…

【云原生】K8s管理工具--Kubectl详解(一)

一、陈述式管理 1.1、陈述式资源管理方法 kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用 apiserver 的接口kubectl 是官方的 CLI 命令行工具&#xff0c;用于与 apiserver 进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为apiserver 能识…

Elasticsearch集群和Logstash、Kibana部署

1、 Elasticsearch集群部署 服务器 安装软件主机名IP地址系统版本配置ElasticsearchElk10.3.145.14centos7.5.18042核4GElasticsearchEs110.3.145.56centos7.5.18042核3GElasticsearchEs210.3.145.57centos7.5.18042核3G 软件版本&#xff1a;elasticsearch-7.13.2.tar.gz 示…

ubuntu18 安装python3.8

在Ubuntu 18.04上安装Python 3.8可以通过以下步骤完成&#xff1a; 1.更新包列表和已安装的包&#xff1a; sudo apt update sudo apt upgrade 2.安装依赖项&#xff1a; sudo apt install -y software-properties-common 3.添加Python 3.8的PPA&#xff08;个人包归档&am…

【Docker系列】 Docker容器具体信息查询

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Python函数进阶:四大高阶函数、匿名函数、枚举、拉链与递归详解

系列文章目录 Python数据类型&#xff1a;编程新手的必修课深入探索Python字符串&#xff1a;技巧、方法与实战Python 函数基础详解Python正则表达式详解&#xff1a;掌握文本匹配的魔法Python文件操作宝典&#xff1a;一步步教你玩转文件读写Python面向对象基础与魔法方法详解…

databricks~Unity Catalog

Unity Catalog hierarchy 包含了用户授权管理信息和元数据信息 workspace with unity catalog hierarchy unity metastore Ref: https://www.youtube.com/playlist?listPLY-V_O-O7h4fwcHcXgkR_zTLvddvE_GfC

[力扣题解] 494. 目标和

题目&#xff1a;494. 目标和 思路 01背包 转换为01背包问题 难点在于看出可以用背包问题解决本题&#xff1b; 题目字面意思是划分出一堆再减去另一堆&#xff0c;得到的结果想要等于target&#xff0c;设定一堆为正&#xff0c;记为left&#xff0c;另一堆为负&#xff0…

ChatGPT类大模型应用入门了解与使用

一 前言 ChatGPT大众热情逐渐褪去&#xff0c;但在后台技术人的探索还处于热火朝天状态。如果我们生活的世界是一杯清水&#xff0c; 那类似ChatGPT的语言大模型技术的横空出世就如滴入水杯的一滴墨汁&#xff0c;第一滴很显眼&#xff0c;但实际上是后续墨汁慢慢扩散渗透才是…

Windows11下使用Qt5.14.2编译QtXlsx驱动详细步骤

原有&#xff1a;由于系统需要将QTableWidget表格中的数据导出、在Windows下最开始使用Excel.Application组件实现了导出功能&#xff0c;后面将代码转换到Ubuntu20.04下进行编译&#xff0c;发现项目.pro文件中的QT axcontainer和代码.h文件中的#include <QAxObject>跟…

基于图鸟UI的资讯名片模版开发与应用

一、引言 在前端技术日新月异的今天&#xff0c;快速、高效、美观的UI组件库和模板成为了开发者们关注的焦点。图鸟UI作为一款集成了基础布局元素、配色体系、图标icon和精选组件的UI框架&#xff0c;为前端开发者提供了极大的便利。本文将以图鸟UI为基础&#xff0c;探讨基于…

接口测试工具有哪些,哪些比较火

接口测试工具可以帮助开发人员和测试人员更高效地进行接口测试&#xff0c;以下是一些常用的接口测试工具&#xff1a; 1. **Postman** Postman 是一款广受欢迎的接口测试工具&#xff0c;它提供了丰富的功能和直观的用户界面&#xff0c;帮助开发人员和测试人员轻松进行 API…

如何让外网访问内网服务?

随着互联网的快速发展&#xff0c;越来越多的企业和个人需要将内网服务暴露给外网用户访问。由于安全和隐私等因素的考虑&#xff0c;直接将内网服务暴露在外网是非常不安全的做法。如何让外网用户安全访问内网服务成为了一个重要的问题。 在这个问题上&#xff0c;天联公司提供…

golang rune类型解析,与byte,string对比,以及应用

Golang中的rune类型是一个32位的整数类型(int32)&#xff0c;它是用来表示Unicode码点的。rune类型的值可以是任何合法的Unicode码点&#xff0c;它通常用来处理字符串中的单个字符。 在Golang中&#xff0c;字符常量使用单引号来表示&#xff0c;例如 a。使用单引号表示的字符…