一、JAVA面向对象的三大特征
封装、继承、多态
1.封装:
隐藏实现细节,提高代码的复用性,提高了安全性。
2.继承:
多个事物直接有共同的属性和行为放到父类中,特有的放在子类中,子类可以继承父类的属性和行为。通过extends关键字。
注:JAVA只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。但可以多层继承。子类访问父类的成员变量通过super关键字。
- 优点:可以继承父类的特征
- 缺点:如果多层父类有相同名字的实例变量或者相同的方法,调用可能会产生歧义,不知道用的哪个父类的。
3.多态:
子类的对象放在父类的引用中,如 Animal a=new Cat(),子类的对象当父类对象来用。
优点:提高了程序的扩展性和复用性
缺点:通过父类引用操作子类对象时,只能用父类中有的方法,不能操作子类特有的方法。
注:多态的前提:1、必须有关系:继承、实现 2、通常都有重写的操作
3.1 向上转型:
当父类的引用指向子类的对象时,就发生了向上转型,即把子类类型转成了父类类型。
优点:隐藏了子类类型,提高了代码的扩展性
弊端:只能使用父类的内容,无法使用子类特有的功能
3.2 向下转型:
当要使用子类特有的功能时,就需要使用向下转型
优点:可以使用子类的 特有功能
弊端:需要面对具体的子类对象时,向下转型时容易发生ClassCastException类型转换异常。所以转型前必须要做判断。
3.3 对象的强制转换:
格式: 引用 instanceof 类型 判断当前对象是否是引用类
用法: Animal a1=new Dog();
if( !Cat instanceof a1){ //判断当前对象是不是Cat类型
}
Cat d=(Cat)a1;
二、java异常

1.异常
Java主要分为Error错误和Exception异常两类。Error(错误)是程序无法处理的错误,表示运行应用程序中较严重问题。Exception(异常)是程序本身可以处理的异常。Exception(异常)又包含运行时异常和非运行时异常(编译异常)
- Error:OOM等
- 运行时异常:ArrayIndexOutOfBoundException数组越界、NullPointerException空指针等。
- 非运行时异常:如IOException IO异常。
2.可查的异常和不可查异常
Java的异常(Throwable)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。

- 可查异常必须要去处理,否则编译不通过,如IOException和ClassNotFoundException等。
处理异常方法:要么用try-catch语句捕获它,要么用throws子句声明抛出它。
- 不可查异常(编译器不要求强制处置的异常):包括运行时异常和错误,不用捕获对应异常,应该找出错误程序进行修改。
三、基础知识点汇总
1.java中extends和implements
extends是继承类,implements是实现接口。 类只能继承一个,接口可以实现多个。
extends继承父类的时候可以重写父类的方法,implements实现接口,必须实现接口的所有方法。
abstract class A {
abstract m(): void;
}
class B extends A{
}
class C implements A {
m(): void { } //必须要实现定义在A中的所有方法
}
2.基本类型和包装类型
2.1 区别
- 包装类型对应变量default值是
null
,而基本类型有默认值且不是 null
。
- 包装类型可用于泛型,而基本类型不可以。
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,几乎所有对象实例都存在于堆中。
- 相比于对象类型, 基本数据类型占用的空间非常小。
2.2 装箱与拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱
int n = i; //拆箱
3.接口和抽象类
3.1 共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
3.2 区别 :
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
4.深拷贝、浅拷贝、引用拷贝

- 引用拷贝:两个不同的引用指向同一个对象
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),对象内部的属性是引用类型的话,指向的是同一地址。
- 深拷贝 :深拷贝会完全复制整个对象。
5.== 和 equals()
- 对于基本数据类型来说,
==
比较的是值。
- 对于引用数据类型来说,
==
比较的是对象的内存地址。
==:比较的是值是否相等(基本类型比较值,引用类型(对象)比较的是内存地址)
equals():只能用来判断对象,不能用于基本数据类型。不重写等同于==,重写可以自定义。
6.String、StringBuffer、StringBuilder 的区别
- String :里面的对象是不可变的,线程安全。适用于操作少量数据
- StringBuffer :线程安全。适用于多线程操作大量数据。
- StringBuilder:非线程安全的。适用于单线程操作大量数据。
四、反射
赋予了我们在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法。
优点:代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
缺点:不安全(比如无视泛型参数的安全检查),性能会变差。
应用场景:业务场景使用较少,框架使用较多。像 Spring/Spring Boot、MyBatis这些框架, Spring 里面的注解,都用了反射机制。里面也用了动态代理,动态代理的实现也依赖反射。
五、集合
1.java集合框架

两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。
2. List, Set, Queue, Map 四者的区别
List: 有序、可重复。
Set: 无序、不可重复。
Queue: 排队,有序、可重复。一般用于排队功能的叫号机。
Map: 使用键值对(key-value)存储。
List
ArrayList
: Object[]
数组
Vector
:Object[]
数组
LinkedList
: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
HashSet
(无序,唯一): 基于 HashMap
实现,底层采用 HashMap
来保存元素
LinkedHashSet
: 通过 LinkedHashMap
实现。
TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
Queue
PriorityQueue
: Object[]
数组来实现二叉堆
ArrayQueue
: Object[]
数组 + 双指针
Map
HashMap
: 数组+链表组成,链表长度大于阈值(默认为 8)会出现红黑树转换。
LinkedHashMap
: 继承自 HashMap
,增加了一条双向链表,可以实现顺序访问。
Hashtable
: 数组+链表组成的,数组是 Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap
: 红黑树(自平衡的排序二叉树)

注:上图是LinkedHashMap的存储结构,在hashMap的基础上多了一个双向链表,可以实现顺序访问。
3.区别
3.1 ArrayList 和 Vector :
- ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,线程不安全 ;
- Vector 是 List 的古老实现类,底层使用Object[ ] 存储,线程安全的。
3.2 ArrayList 与 LinkedList:
- 数据结构:一个数组一个链表数组。LinkedList 是 双向链表 (JDK1.6 之前为循环链表,JDK1.7 取消了循环)
- 访问速度:数组可以快速随机访问,链表不行
- 更新:链表对中间数据插入较友好好。
- 线程安全:都不是线程安全的;
注:很少使用LinkedList
3.3 HashSet、LinkedHashSet 和 TreeSet
- 都是 Set 接口的实现类,元素唯一,并且都不是线程安全的。
- HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于 FIFO 的场景,TreeSet 用于元素需要自定义排序场景。
3.4 Queue 与 Deque
- Queue :单端队列,一端入,另一端出,一般遵循 先进先出(FIFO) 规则
- Deque :双端队列,在队列的两端均可以插入或删除元素。
3.5HashMap 和 Hashtable
- HashMap 非线程安全,Hashtable 是线程安全的(内部方法基本都经过synchronized 修饰)。
- HashMap 要比 Hashtable 效率高一点。
注:基本不会用Hashtable,要线程安全就用ConcurrentHashMap
3.6ConcurrentHashMap在jdk1.7和1.8的区别
- 数据结构:1.7是Segment 数组 + hashMap(HashEntry 数组 + 链表),1.8是 Node 数组 + 链表 / 红黑树。相当于原来是Segment和hashMap的数组是映射关系,现在合并了。
- 锁1.7是Segment 分段锁,针对Segment(段/槽)加锁,1.8后锁的力度更细了,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
- 1.7的分段锁继承自 ReentrantLock。1.8 放弃了分段锁设计,采用 Node + CAS + synchronized 保证线程安全。
- 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大
3.7 HashMap实现
默认大小16,负载因子0.75,超过对应阈值会扩容。
put过程:先计算key的hash值,然后低16位和高16位做异或(充分利用每一位),然后与数组长度-1做与操作确定数组的位置,然后在链表最后插入对应的节点(如果已经有相同的key会替换)。如果超过8则会有红黑树的转换。
六、并发编程
1.进程和线程
进程:是系统运行程序的基本单位。一个进程在其执行的过程中可以产生多个线程。
线程:是一个比进程更小的执行单位。同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。多线程之间切换负担要比进程小得多。
2.并发与并行的区别
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
3.同步和异步的区别
同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。
4.线程的生命周期和状态
5.sleep() 方法和 wait() 方法
- 都可以暂停线程的执行。
- sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
- sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法(本质是每个对象都有对象锁,要释放当前线程占有的对象锁,所以操作的是对象而不是线程)。
- wait() 需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法唤醒。sleep()方法执行完成后,线程会自动苏醒。
6.可以直接调用 Thread 类的 run 方法吗
可以但不会以多线程的方式执行,用当前线程执行的。
new 一个 Thread后,需要调用 start()方法,start()方法作用是启动一个线程处于就绪状态,当分配到时间片后就可以开始运行,这时候该线程会自动执行 run() 方法进行执行,这样就是多线程了。省略了start,那么就没有额外的线程,会用当前线程执行。
7.volatile
作用:保证变量的可见性,禁止指令重排。
7.1 如何保证变量可见性

volatile修饰的,不会有在工作内存有变量副本,线程直接从主内存取。
7.2 如何禁止指令重排
单例模式(线程安全) :
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
- 为
uniqueInstance
分配内存空间
- 初始化
uniqueInstance
- 将
uniqueInstance
指向分配的内存地址
JVM 的指令重排可能导致执行顺序不是1->2->3,而是 1->3->2。这样再多线程情况下,1->3的时候,另一个线程读到的就不是null了。
8.synchronized
8.1 作用域
修饰方法
修饰对象:根据修饰的对象不同可以分为全局锁(xxx.class)或者代码块.
8.2 底层原理
通过 JDK 自带的 javap 命令,可以查看java字节码.class信息,发现synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令;synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。不过两者的本质都是对对象监视器 monitor 的获取。
8.3 锁升级
偏向锁、轻量级锁、重量级锁
用synchronized修饰的会有锁升级过程,如果只有一个线程获取锁,那么这个锁会偏向这个线程,叫做偏向锁;如果这时候有其他线程也来获取这个锁,那么这个锁会变成轻量级锁,获取不到锁的线程会自旋,然后重新获取;如果还是获取不到,则会变为重量级锁,获取不到的线程会阻塞(锁池状态)。
8.4 synchronized 和 ReentrantLock 的区别
- 两者都是可重入锁
- synchronized非公平锁,ReentrantLock默认也是非公平锁,不过可以有参数可以配置变成公平锁
- synchronized 依赖于 JVM , ReentrantLock 依赖于 API(JDK 层面实现),需要lock() 和 unlock() 方法配合 try/finally 语句块来完成
- ReentrantLock功能更多,如可以变成公平锁
9.ThreadLocal
为了实现每个线程都有自己的专属本地变量。通过创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,从而避免了线程安全问题。
9.1 ThreadLocal 原理
// 源码:
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
ThreadLocal内部有一个ThreadLocalMap,每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

10.线程池
10.1为什么使用线程池
- 降低资源消耗:可以重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,用已有线程可以直接执行。
- 方便管理:统一创建,分配,调优和监控。
10.2 实现 Runnable 接口和 Callable 接口的区别
- Runnable自 Java 1.0 以来一直存在,Callable在 Java 1.5 中引入
- Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以如果任务不需要返回结果或抛出异常就使用 Runnable 接口 ,看起来更简洁。
10.3 execute()方法和 submit()方法的区别
- execute()方法:没有返回值,无法判断任务是否被线程池执行成功;
- submit()方法:线程池会返回一个 Future 类型的对象,可以判断是否执行成功。此外可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。
10.4 四类线程池
CachedThreadPool: 可根据实际情况动态调整线程数量的线程池。
FixedThreadPool : 固定线程数量的线程池。
SingleThreadExecutor: 只有一个线程的线程池。
ScheduledThreadPool:调度类的线程池。
10.4.1 几个核心参数:
- corePoolSize : 核心线程数。
- maximumPoolSize : 最大线程数。
- workQueue: 工作队列。
- keepAliveTime:非核心线程(临时线程)存活时间;
- handler :饱和策略。主要有报错、丢弃当前任务、丢弃最早未处理的任务、用提交任务的线程执行这四种。
10.4.2 大致流程
进来任务时,用核心线程做任务;如果任务超过核心线程数,则放入队列;如果任务还在增加,则创建临时线程执行,直到最大线程数;如果还在增加,则按照对应的饱和策略处理。
11.JUC 包中的4大Atomic原子类
基本类型:使用原子的方式更新基本类型
AtomicInteger
:整型原子类
AtomicLong
:长整型原子类
AtomicBoolean
:布尔型原子类
数组类型:使用原子的方式更新数组里的某个元素
AtomicIntegerArray
:整型数组原子类
AtomicLongArray
:长整型数组原子类
AtomicReferenceArray
:引用类型数组原子类
引用类型:
AtomicReference
:引用类型原子类
AtomicStampedReference
:原子更新带有版本号的引用类型。
AtomicMarkableReference
:原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater
:原子更新整型字段的更新器
AtomicLongFieldUpdater
:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器
12.AQS
AQS 的全称为(AbstractQueuedSynchronizer),是一个用来构建锁和同步器的框架。如: ReentrantLock,Semaphore信号量等都是用的AQS框架。
12.1 AQS 原理
通过ReentrantLock加锁说明:简单的说就是,AQS对象内部有一个volatile的int变量,初始state的值是0,一个线程要来加锁时,通过CAS把0改为1,代表加锁成功,释放锁就是再减回去,减到0就代表锁释放了。
另外AQS内部还维护着一个先进先出的等待队列,如果其他线程也来加锁,但是因为锁已经被其他线程占用而加锁失败,就会存放这些加锁失败的线程。


注:CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
12.2 AQS 组件总结
- ReentrantLock:一次只允许一个线程访问某个资源,可重入。
Semaphore
(信号量)-允许多个线程同时访问问: synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch
(倒计时器): 可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier
(循环栅栏): 让一组线程到达栅栏时被阻塞,直到最后一个线程到达时,栅栏被推倒,所有线程开始执行。
七、反射
1.概念
可以在运行时分析类以及执行类中方法,通过反射可以获取和调用对应类的属性和方法。
像Spring/Spring Boot、MyBatis 等框架中都大量使用了反射机制,动态代理也是基于反射。
2.优缺点
- 优点 : 更加灵活、为各种框架实现对应功能提供了便利。
- 缺点 :无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。性能也会稍差些。
3.获取 Class 对象的四种方式
3.1. 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
3.2. 通过 Class.forName()
传入类的全路径获取:
Class alunbarClass1 = Class.forName("cn.test.TargetObject");
3.3. 通过对象实例instance.getClass()
获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
3.4. 通过类加载器xxxClassLoader.loadClass()
传入类路径获取:
ClassLoader.getSystemClassLoader().loadClass("cn.test.TargetObject");
八、语法糖
1.概念:
计算机语言中的某种特殊语法,方便程序员使用。可以让程序更加简洁,有更高的可读性。
注:反编译,可以用java自带的javap,对.class文件进行反编译,可读性稍差。有一些网站可以把字节码反编译成对应的代码。所以查看语法糖依赖反编译。
2.常见语法糖
- switch 支持 String 与枚举:java7开始支持switch string,正常switch对比的是基本数据类型的值或者ascii码。switch string,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查。
- 泛型:编译阶段通过类型擦除的方式进行解语法糖。
- 自动装箱与拆箱:装箱通过调用包装器的 valueOf 方法实现,而拆箱通过调用包装器的 xxxValue 方法实现的。
- 可变长参数:把可变长参数变成了一个数组,方法调用传的是数组。
注:在编译阶段,如果有一些代码根本不会走到(废代码),那么编译器会不进行编译。
九、jvm内存模型
十、垃圾回收
1.垃圾回收算法
1.1 标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
1.2 标记-复制算法
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
注:新生代采用标记-复制算法
1.3 标记-整理算法
先标记,然后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
注:老年代有使用标记-整理算法
1.4 分代收集算法
根据对象存活周期的不同将内存分为几块。分为新生代和老年代。新生代朝生夕死的多,所以用标记-复制算法。老年代存活几率比较高,没有额外的空间来对它进行担保,所以用标记-清除或者标记-整理算法。
2.分代回收细节
分为新生代、老年代和永久代(1.8后改成meta space),新生代里面有一个Eden区,两个Survivor区,每次回收时,复制Eden区和Survivor区存活对象到另外一个Survivor区,如此反复。如果超过15次还存活的对象,则进入老年代。
注:大的对象直接进入老年代,如大的数组、字符串等。
注2:对象一般在Eden区分配,当 Eden 区没有足够空间进行分配时,将会进行 Minor GC
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):对老年代进行垃圾收集。
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC):收集整个 Java 堆和方法区。
3.死亡对象判断
3.1 引用计数法
对象有一个引用计数器,每次被引用,计数器加 1,引用失效,计数器就减 1;为 0 的对象可以被回收。
优点:效率高
缺点:无法解决循环引用的问题
3.2 可达性分析算法
通过根节点 “GC Roots”向下搜索,当一个对象到 GC Roots 没有任何引用链相连的话,就可以被回收。
可以作为 GC Roots 的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
4. 引用类型
4.1.强引用(StrongReference):
我们平常使用的Object ob = new Object(),内存不足抛出 OOM,只有ob=null,才会回收。
4.2.软引用(SoftReference)
内存空间足够,就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
4.3.弱引用(WeakReference)
垃圾收集器只要扫到,都会回收。
4.4.虚引用(PhantomReference)
被回收的时候会有一个通知,主要用来跟踪对象被垃圾回收的活动。
十一、高cpu占用优化
1.背景:
saas服务启动时,cpu占用太高。
2.过程:
- 首先确定CPU占比高的进程,线程ID。通过windows提供的工具Process Explorer可查到saas服务的进程ID(PID), 以及内部线程占用cpu情况。实时显示选中的进程内部线程ID,占用CPU情况。 操作saas,记录下占比CPU比较高的线程ID。
- 通过Jstack工具查看进程内部的线程执行情况: jstack -l PID > XXX.stack .
- 通过线程ID在jstack文件中找到消耗cpu比较大的线程为:C2 CompilerThread0 .在server模式下启动jar包后默认是开启tiered compiler对javac产生的字节码进行优化,这个线程消耗资源比较多。
- 用 -client 模式启动jar包后, 初步看内存占用从400+MB降到200+MB,CPU占比高的持续时间明显降低。
3.分析:
因为我们都知道JIT( just in time ), 也就是即时编译编译器。使用即时编译器技术,能够加速 Java 程序的执行速度。主要有Server 模式和 client 模式两种启动模式,主要的差别在于:-server 模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。原因是:当虚拟机运行在-client 模式的时候,使用的是一个代号为 C1 的轻量级编译器,而-server 模式启动的虚拟机采用相对重量级代号为 C2 的编译器。C2 比 C1 编译器编译的相对彻底,服务起来之后,性能更高。