JVM 详解

JVM的内存模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUVCmNAO-1662466861930)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\jvm.jpg)]

Java 编译加载过程

类的生命周期

类的整个生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备、解析3个部分统称为连接。

1.写好的 jar 包 被编译器 (javac)编译成class文件, 然后被装载到内存中,这个过程叫做classloader

2.加载 loading

  • Loading:是把一个class文件load到内存中去

3.连接 linking

  • 其中linking又分为三个步骤:校验 verification 、准备 preparation 、解析 resolution;

  • verification 是用来校验加载进来的class文件是否符合class文件标准,如果不符合直接就会被拒绝了

  • preparation 是将class文件静态变量赋默认值而不是初始值,如static int i =8;这个步骤并不是将i赋值为8,而是赋为默认值0

  • resolution 是把class文件常量池中用到的符号引用转换成直接内存地址,可以访问到的内容;

4.初始化 initializing

  • initializing 成为初始化,静态变量在这个时候才会被赋值为初始值

四个类加载器

-- BootstrapClassLoader类加载器用来加载jdk中的核心类库,如String.class、Object.class等这些class文件都是位于JDK核心类库里面
bootstrap
-- ExtClassLoader是用来加载扩展类的,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包里面的所有class文件
Extension
-- AppClassLoader 又称为系统类加载器,负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径,说白了就是我们编写的class文件
Application
-- CustomClassLoader(自定义加载器)
Custom 自定义

Java 运行的数据区

概览JVM运行时数据区主要包括以下几个部分:程序计数器、虚拟机栈、本地方法栈、方法区、堆;

1.程序计数器

程序计数器可以看作是当前线程所执行的字节码的 行号指示器 可以通过javap -c xxx.class执行查看字节码文件

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。

2.虚拟机栈

虚拟机栈描述的是 Java方法执行的内存模型:

每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧(Stack Frame)是用于支持虚拟机进行`方法调用`和`方法执行`的数据结构,是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里从入栈到出栈的过程。简单来讲:每个方法都会对应在虚拟机栈中生成一个栈帧,以栈的数据结构进行存放所有的栈帧。

虚拟机栈

  • 虚拟机栈描述的是 Java方法执行的内存模型:每个方法都会对应在虚拟机栈中生成一个栈帧,以栈的数据结构进行存放所有的栈帧。

局部变量表

  • 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量

操作数栈

  • 在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,这就是入栈和出栈

动态连接

  • 动态连接就是指向常量池中该栈帧所属方法的引用

方法返回地址

  • 一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

3.堆

Java堆在虚拟机启动时创建,唯一目的就是存放对象实例 以及 数组。

(1)从内存回收的角度来看,Java堆是垃圾收集器管理的主要区域。

由于目前的垃圾收集器都采用分代收集算法,因此Java堆中还可细分为:新生代和老年代,默认占比为Young:Old = 1:2。同时新生代中采用复制算法,将新生代分为三个区域,默认占比为:Eden:from:to=8:1:1。

(2)从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer)

4.方法区(元空间)

方法区用于存储已被虚拟机加载的类元信息(即Class对象)常量池(运行时常量池)静态变量、即时编译器编译后的代码等数据。

额外解释:类元信息(class metadata),相当于java类编译后(.class类对象)在JVM中的信息。

类元信息中包含了:

  1. Klass 结构,这个非常重要,把它理解为一个 Java 类在虚拟机内部的表示;

  2. method metadata,包括方法的字节码、局部变量表、异常表、参数信息等;

  3. 注解;

  4. 方法计数器,记录方法被执行的次数,用来辅助 JIT 决策;

对于HotSpot虚拟机来说,也称为“永久代”(Permanent Generation)

方法区的变化:

jdk1.6及以前:有永久代(方法区的实现),运行时常量池逻辑包含字符串常量池在方法区中

jdk1.7:有永久代,但已逐步“去永久代”,字符串常量池转移到堆中,运行时常量池还在方法区中

jdk1.8及之后:无永久代,替换为元空间,字符串常量池还在堆, 运行时常量池还在方法区中(元空间)

5.本地方法栈

简单来讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java方法:该方法的实现由非java语言实现。

Java 执行引擎

加载到内存之后 就有了 java 的执行引擎执行

  • 解析器
  • 既时编译器 [ 汇编 或者 c++ 编写的]

MAT工具对(堆) dump文件分析

MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。

官网地址:https://www.eclipse.org/mat/

VirtualVM工具使用 (栈)

VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。

  • 内存信息
  • 线程信息
  • Dump堆(本地进程)
  • Dump线程(本地进程)
  • 打开堆Dump。堆Dump可以用jmap来生成。
  • 打开线程Dump
  • 生成应用快照(包含内存信息、线程信息等等)
  • 性能分析
  • CPU分析(各个方法调用时间,检查哪些方法耗时多)
  • 内存分析(各类对象占用的内存,检查哪些类占用内存多)
  • ……
// 可查看 查询 运行中的死锁

溢出

栈溢出

// 原理
栈帧超过了栈的深度
// 栈溢出的原因
递归 可能导致栈溢出
局部数组过大

堆溢出

// heap space表示堆空间,堆中主要存储的是对象。如果不断的new对象则会导致堆中的空间溢出
对象过大 可能导致堆溢出

内存泄漏:

分配出去的内存没有被回收回来,失去对内存区域的控制,造成资源的浪费,比如:new出来了对象并没有引用,垃圾回收器不会回收他,造成内存泄漏

JVM GC垃圾回收器

程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了。

垃圾回收的常见算法

自动化的管理内存资源,垃圾回收机制必须要有一套算法来进行计算,哪些是有效的对象,哪些是无效的对象,对于无效的对象就要进行回收处理。常见的垃圾回收算法有:引用计数法、标记清除法、标记压缩法、复制算法、分代算法等。

引用计数法

原理

假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,

当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。

优缺点

优点:

实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。在垃圾回收过程中,应用程序无需挂起。如果申请内存时,内存不足,则立刻报out of memery 错误。局部更新对象的计数器时,只是影响到该对象,不会扫描全部对象。

缺点:

每次对象被引用时,都需要去更新计数器,有一点时间开销。浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。无法解决循环引用问题。(最大的缺点)

标记清除法

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
标记:从根节点开始标记引用的对象。
清除:未被标记引用的对象就是垃圾对象,可以被清理。

没有被标记的对象将会回收清除掉,而被标记的对象将会留下,并且会将标记位重新归0。接下来就不用说了,唤醒停止的程序线程,让程序继续运行即可。

优缺点

优点:可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。

缺点:效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。

标记压缩算法

原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BbwS9KHb-1662466861936)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20210806164040783.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6EJ9IKl-1662466861937)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20210806164104071.png)]

优缺点

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

复制算法

复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,然后再重新交换两个内存的角色,完成垃圾的回收。如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7Y2a96o-1662466861938)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20210806164419661.png)]

JVM中年轻代内存空间

young_gc

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l99bcUZ6-1662466861939)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20210806165332889.png)]

  1. 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。

  2. 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。

  3. 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。

  4. GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

优缺点:
优点:在垃圾对象多的情况下,效率较高,清理后,内存无碎片
缺点:在垃圾对象少的情况下,不适用,如:老年代内存,分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

分代算法

前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的选择。分代算法其实就是这样的,根据回收对象的特点进行选择

在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。

垃圾收集器以及内存分配

在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器

串行垃圾收集器(Serial)

新生代采用复制算法,老年代采用标记-整理算法

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。
它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃 圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束 ,
Serial一般和Serial Old搭配在一起,Serial对年轻代收集,Serial Old对老年代收集。

并行垃圾收集器

ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为 (控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
ParNew收集器一般和Cms收集器或者Serial Old收集器搭配使用,ParNew收集器主要作用是对年轻代进行垃圾收集,采用的是复制算法,老年代Cms收集器或者Serial Old收集器采用的是标记-整理算法

Parallel Scavenge垃圾收集器

Parallel Scavenge收集器工作机制和ParNewGC收集器一样,Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。
CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。
Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择
Parallel Scavenge收集器主要作用在年轻代上,采用复制算法,老年代采用ParallelOld收集器,采用标记整理算法

CMS垃圾收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。一般用在老年代,它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器, 它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它 的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤

  • 初始标记: 暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
  • 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记 产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫

G1垃圾收集器(重点)

G1(Garbage-First)垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ehRhjcoh-1662466861940)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20220825214304605.png)]

G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。

一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以 用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。

G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且 一个大对象如果太大,可能会横跨多个Region来存放。Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老 年代空间不够的GC开销。 Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

G1收集器一次GC的运作过程大致分为以下几个步骤:

  • 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用 的对象,速度很快 ;
  • 并发标记(Concurrent Marking):同CMS的并发标记
  • 最终标记(Remark,STW):同CMS的重新标记
  • 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本 次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个 Region刚好需要200ms,那么就只会回收800个Region,尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个 region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制 算法回收几乎不会有太多内存碎片
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃 圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面 这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集 器在有限时间内可以尽可能高的收集效率。

G1重要进化特征

并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者 CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行

分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法 实现的收集器;从局部上来看是基于“复制”算法实现的。

可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同 的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集

G1垃圾收集分类

YoungGC

YoungGC并不是说现有的Eden区放满了就会马上触发,而且G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代 的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

MixedGC

MixedGC不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发,值默认是45%,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

Full GC 停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的

如何选择垃圾收集器

  • 优先调整堆的大小让服务器自己来选择
  • 如果内存小于100M,使用串行收集器
  • 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
  • 如果允许停顿时间超过1秒,选择并行或者JVM自己选
  • 如果响应时间最重要,并且不能超过1秒,使用并发收集器

可视化GC日志分析工具

GC Easy是一款在线的可视化工具,易用、功能强大,网站:

http://gceasy.io/

JVM 原理补充

JDK、JRE、JVM三者间的联系与区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kJjxhXd1-1662466861941)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211105165311145-1661176154228.png)]

JDK(Java SE Development Kit):Java标准开发包

它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B185rqpF-1662466861942)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211105172514921.png)]

JRE( Java Runtime Environment):Java运行环境

用于解释执行Java的字节码文件。普通用户而只需要安装 JRE(Java Runtime Environment)来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kYHPGnJ4-1662466861942)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211105172650557.png)]

JVM(Java Virtual Mechinal):Java虚拟机,是JRE的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。所有平台的上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。不同平台的JVM都是不同的,但它们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行

堆内存如何划分

堆内存如何划分,如何回收这些内容对象,有哪些回收算法?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kWUM4ih-1662466861942)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20220323102238295.png)]

说明:

  • 有GC垃圾收集器回收这些对象;
  • 老年代一般采用标记清除算法;新生代一般复制算法

Class 文件加载过程

  • 编译 jar 包 成 class文件,当我们调用Java命令的时候class文件会被装载到内存中,这个过程叫做classloader

  • 一般情况下自己写代码的时候会用到Java的类库,所以在加载的时候也会把Java类库相关的类也加载到内存中

  • 装载完成之后会调用字节码解释器和JIT即时编译器来进行解释和编译,

  • 编译完之后由执行引擎开始执行,执行引擎下面对应的就是操作系统硬件了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HhCPghzO-1662466861943)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211105150412414.png)]

类加载主要有三个过程:loading 、linking 、initializing;其中linking又分为三个步骤:verification 、preparation 、resolution;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUD8zJY1-1662466861943)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211105150557697.png)]

说明:

1.Loading:是把一个class文件load到内存中去

2.Linking:接下来是Linking分为了三小步

  • verification 是用来校验加载进来的class文件是否符合class文件标准,如果不符合直接就会被拒绝了
  • preparation 是将class文件静态变量赋默认值而不是初始值,如static int i =8;这个步骤并不是将i赋值为8,而是赋为默认值0
  • resolution 是把class文件常量池中用到的符号引用转换成直接内存地址,可以访问到的内容;

3.initializing 成为初始化,静态变量在这个时候才会被赋值为初始值

双亲委派模式

  • 通俗的话来解释这个过程,当有一个类需要被加载时,首先要判断这个类是否已经被加载到内存,判断加载与否的过程是有顺序的,

  • 如果有自己定义的类加载器,会先到custom class loader 的cache(缓存)中去找是否已经加载,若已加载直接返回结果,否则到App的cache中查找,

  • 如果已经存在直接返回,如果不存在,到Extension中查找,存在直接返回,不存在继续向父加载器中寻找直到Bootstrap顶层,

  • 如果依然没找到,那就是没有加载器加载过这个类,需要委派对应的加载器来加载,先看看这个类是否在自己的加载范围内,如果是直接加载返回结果,

  • 若不是继续向下委派,以此类推直到最下级,如果最终也没能加载,就会直接抛异常 ClassNotFoundException,这就是双亲委派模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGGC1p8d-1662466861944)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211105151417765.png)]

简单来讲:

  • 依次从下往上加载 如果还没加载到 则再次从上往下加载 直至加载完成 然后最后还没加载则抛出异常
  • 注意的是 自定义的类加载器 如果跟jdk中的核心类库有一样的 则必须加载BootstrapClassLoader类加载器用来加载jdk中的核心类库
// 每一个class文件都是有一个class loader(类加载器)加载到内存的,上图一共有4个类加载器,不同的类加载器负责加载不同的class;注意一点这个的层级关系并没有继承的关系在里面,只是单单纯纯的语法上的继承;父加载器:不是类加载器的加载器,也不是类加载器的父类加载器1.BootstrapClassLoader类加载器用来加载jdk中的核心类库,如String.class、Object.class等这些class文件都是位于JDK核心类库里面,在C:\Program Files\Java\jdk1.8.0_151\jre\lib\rt.jar这个jar里面,解压这个jar包就能看到这些class文件,所以当我们输出某个类的类加载器如果是一个null,就证明是有顶层的BootstrapClassLoader类加载器加载的
System.out.println(String.class.getClassLoader());  // null2.ExtClassLoader是用来加载扩展类的,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包里面的所有class文件
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());3.第三个类加载器的层次为:AppClassLoader 又称为系统类加载器,负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径,说白了就是我们编写的class文件
System.out.println(TestClassLoader.class.getClassLoader());4.第四个类加载器的层次为:CustomClassLoader(自定义加载器)// 搞双亲委派模式主要是为了安全,这里可以使用反证法,如果任何类加载器都可以把class加载到内存中,我们就可以自定义类加载器来加载Java.lang.string。在打包时可以把密码存储为String对象,偷偷摸摸的把密码发送到自己的邮箱,这样会造成安全问题

JAVA 集合

java 集合补充

List

ArrayList

ArrayList底层是由动态数组实现的

// 有序 | 内容可重复 | 线程不安全

LinkedList

LinkedList底层是由双向链表的数据结构实现的

// 线程不安全 没锁 | 增删 效率高 , 查询需要一个个找使用效率低

Set

HashSet

HashSet底层是采用HashMap实现的

// 不能保证元素的顺序,元素是无序的
// HashSet不存入重复元素的规则:使用hashcode和equals

TreeSet

TreeMap的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树

// 如果没有提供比较器,则采用key的自然顺序进行比较大小,可指定的比较器进行key值大小的比较。
// TreeSet是一个有序的集合,基于TreeMap实现,支持两种排序方式:自然排序和定制排序。
// TreeSet是非同步的,线程不安全的。

LinkedHashSet

LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表

// 不允许添重复元素
// 有序 确保插入顺序和遍历顺序一致

Map

HashMap

先数组 hashCode冲突使用链表 链表长度超过8个转换红黑树储存

// 无序的,根据 hash 值随机插入
// HashMap 线程不安全

HashTable

HashTable是继承与Dictionary类,实现了Map接口,HashTable的主体还是Entry_数组

// HashMap是非线程安全的
// HashMap的key可以使用null(只能有一个),value可以为null,而HashTable都不允许存储key和value值为空的元素
// 不会转换为红黑

LinkedHashMap

LinkedHashMap底层是数组 + 单项链表 + 双向链表

// key和value都允许为空
// key重复会覆盖,value可以重复
// 有序的
// LinkedHashMap是非线程安全的

TreeMap

TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。

// 所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较

ArrayList

ArrayList底层是由动态数组实现的。

  • 动态数组就是长度不固定,随着数据的增多而变长。如果不指定它的长度,则默认为10

  • 当ArrayList增加元素时,它是按照顺序从头部开始往后添加,它是有顺序的。

  • 如果当添加的元素超过当前数组的长度时,它会新创建一个数组,长度为当前数组的1.5倍,然后将当前数组的元素复制到新的数组

  • 新的数组由原来的长度10变为现在的15.

  • 线程不安全 没锁

    数组是用来存储固定大小的同类型元素,数组存放位置是在jvm的堆中。
    当有新的元素需要存储时,都会存储在最前面,因此每次存储,所有的元素都会向后移动位置。
    同理,如果删除一个元素,后面的元素都会向前移动一个位置。因此,ArrayList在存储和删除的时候效率比较低。

    但是由于每个元素占用的内存相同且是连续排列的,因此在查找的时候,根据元素的下标可以迅速访问数组中的任意元素,查询效率非常高。

LinkedList

LinkedList底层是由双向链表的数据结构实现的。线程不安全 没锁

  • 由上图可以看到:双向链表是由三个部分组成:prev、data、next.
prev:由用来存储上一个节点的地址;
data:是用来存储要存储的数据;
next:是用来存储下一个节点的地址。
  • 链表画的分布不均匀是因为它不像数组一样是连续排列的,双向链表是可以占用一段不连续的内存空间。

  • 当我们有新元素插入时,只需要修改所要插入位置的前一个元素的next值和后一个元素的prev值即可

  • 增删 效率高 , 查询需要一个个找使用效率低

删除也是同理,比如要删除数据8的元素,只需要修改数据7的next值和数据9的prev值即可,然后数据8没有元素指向它,它就成了垃圾对象,最后被回收。
因此在增加和删除的时候只需要更改前后元素的next和prev值,效率非常高。
但是在查询的时候需要从第一个元素开始查找,直到找到我们需要的数据为止,因此查询的效率比较低。

HashSet

HashSet按照Hash算法来存储集合中的元素,存在以下特点:

  • 不能保证元素的顺序,元素是无序的
  • HashSet是不同步的,需要外部保持线程之间的同步问题,Collections.synchronizedSet(new XXSet());
  • 集合元素值允许为null

HashSet底层是采用HashMap实现的

// HashSet存放的是哈希值,Hashset存储元素的顺序并不是按照存入时的顺序(和List显然不同),是按照哈希值来存的,所以取数据也是按照哈希值取的。// HashSet不存入重复元素的规则:使用hashcode和equals。
// 那么HashSet是如何检查重复?其实原理:HashSet会通过元素的hashcode()和equals()方法进行判断,当试图将元素加入到Set集合中,HashSet首先会使用对象的hashcode来判断对象加入的位置。// 同时也会与其他已经加入的对象的hashcode进行比较,如果没有相等的hashcode,HashSet就认为这个对象之前不存在,如果之前存在同样的hashcode值,就会进一步的比较equals()方法,如果equals()比较返回结果是true,那么认为该对象在集合中的对象是一模一样的,不会将其加入;如果比较返回的是false,那么HashSet认为新加入的对象没有重复,可以正确加入。// 当两个对象的hashcode不一样时,说明两个对象是一定不相等的, 当两个对象的hashcode相等,但是equals()不相等,在实际中,会在同一个位置,用链式结构来保存多个对象,而HashSet访问集合元素时也是根据元素的hashCode值快速定位,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。

1、使用HashSet集合时, 首先应该知道它是无序的,其次不存在重复元素。
2、如何判断是否是重复的元素也是应该很清楚的
3、如果计算两者的hashCode值一样,但是equals不一样,也是不一样的对象,在存储时,会采用链式结构进行存储。

TreeSet

TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。

TreeSet实现了SortedSet接口,它是一个有序的集合类,TreeSet的底层是通过TreeMap实现的。TreeSet并不是根据插入的顺序来排序,而是根据实际的值的大小来排序。TreeSet也支持两种排序方式:

  • 自然排序
  • 自定义排序
// 如果没有提供比较器,则采用key的自然顺序进行比较大小,如果指定的比较器,则采用指定的比较器,进行key值大小的比较。
// TreeSet是一个有序的集合,基于TreeMap实现,支持两种排序方式:自然排序和定制排序。
// TreeSet是非同步的,线程不安全的。

LinkedHashSet

底层机制

  • LinkedHashSet是HashSet的子类
  • LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表
  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序, 使得元素以插入顺序保存的。(有序)
  • LinkedHashSet 不允许添重复元素
  • 每一个节点有pre和next属性,这样可以形成双向链表 遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

HashMap

底层存储结构

先数组 hashCode冲突使用链表 当链表长度到达8时,升级成红黑树结构

  • 无序的,根据 hash 值随机插入
  • HashMap 线程不安全

hashmap是如何减少hash冲突的

对 key 做 hash 操作时,执行了 (h = key.hashCode()) ^ (h >>> 16)

原 hashCode 值: 10110101 01001100 10010101 11011111
右移 16 位后的值: 00000000 00000000 10110101 01001100
异或后的值:      10110101 01001100 00100000 10010011

这个操作是把 key 的 hashCode 值右移 16 位做异或(不同为 1,相同为 0),这样就是把哈希值的高位和低位一起混合计算,这样就能使生成的 hash 值更离散,减少hash碰撞,注意hash(Object key)只是算出hash值

这里需要我解释下,通过前面的介绍,我们知道数组的容量范围是 [0,2^30],这个数还是比较大的,平时使用的数组容量还是比较小的,比如默认的大小 16,假设三个不同的 key 生成 1`的 hashCode 值如下所示:

19305951   00000001 00100110 10010101 11011111128357855  00000111 10100110 10010101 1101111138367      00000000 00000000 10010101 11011111

可见经过右移 16位后再进行异或操作,然后计算其对应的数组下标后,就被分到了不同的桶中,解决了哈希碰撞问题,思想就是把高位和低位混合进行计算,提高分散性

插入到链表的方法

头插法: 在多线程操作下就会出现链表死循环

尾插法: 在相同的前提下就不会出现这样的问题,因为扩容前后链表顺序是不变的,他们之间的引用关系也是不变的

也就是说:如果在并发环境下不扩容的话,采用头插法和尾插法都可以

HashMap扩容机制?

Capacity:HashMap当前长度。

LoadFactor:负载因子,默认值0.75f。

  • 负载因子: 比如HashMap的容量是100,负载因子是0.75,乘以100就是75,所以当你增加第76个的时候就需要扩容了

  • 首先是创建一个新的数组,容量是原来的二倍,之所以要为原来的二倍(也就是2 的整数次幂)目的是减少更多的hash冲突,然后会经过重新hash,把原来的数据放到新的数组上,至于为啥要重新hash,那必须啊,你容量变了,相应的hash算法规则也就变了,得到的结果自然不一样了

关于链表转红黑树

在jdk1.8中加入了红黑树,

// 就是当链表长度为8时会将链表转换为红黑树,
// 为6时又会转换成链表,是为了提高了性能,因为在链表的长度没有超过6时,链表由于使用尾插法,在新增和删除的时候快,查询效率也高,// 当长度为8时,链表的插入和删除操作比红黑树还是要快,因为红黑树要不断的维护树的平衡和节点的自旋操作,但是查询效率没有红黑树高
// (如:一个元素在链表的最末端,用链表的话要一一个个的找,用红黑树的话效率很高),// 为了综合性能,故在长度超过8之后,就用红黑树,不用链表

因为经过计算,在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。

因为 8 够用了,至于为什么转回来是 6,因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,

为了预防这种情况的发生,所以为6

HashMap增加新元素的主要步骤

下面我们分析一下HashMap增加新元素的时候都会做哪些步骤:

1、判断数组是否为空,为空进行初始化;
2、不为空,计算 k 的 hash 值,....,然后通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中;
4、存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false);
5、如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
6、如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
7、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍

重点就是判断放入HashMap中的元素要不要替换当前节点的元素,那怎么判断呢?总结起来只要满足以下两点即可替换:

1、hash值相等。2、==或equals的结果为true。

HashMap 是线程安全的吗

不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以 1.8 为例,当 A 线程判断 index 位置为空后正好挂起,B 线程开始往 index 位置的写入节点数据,这时 A 线程恢复现场,执行赋值操作,就把 A 线程的数据给覆盖了;还有++size 这个地方也会造成多线程同时扩容等问题

解决HashMap线程不安全

Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map

HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大,Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 对象,内部定义了一个对象锁,方法内通过对象锁实现;ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高

HashTable

HashMap和HashTable的区别

  • HashTable是继承与Dictionary类,实现了Map接口,HashTable的主体还是Entry_数组
  • 线程安全 所有的操作都是通过synchronized锁保护实现的,只有获得了对应的锁,才能进行后续的读写操作
HashMap是非线程安全的,在多线程环境下,HashMap会产生线程安全问题;而HashTable中的大部分方法都使用synchronized关键字来确保线程同步,因此HashTable是线程安全的,不过性能要比HashMap低一些HashMap的key可以使用null(但只能有一个),value可以为null,而HashTable都不允许存储key和value值为空的元素
HashMap继承了AbstractMap,HashTable继承了Dictionary抽象类,两者都实现了Map接口
HashMap的初始容量为16,HashTable的初始容量为11
HashMap的扩容机制为扩容两倍,而HashTable的扩容机制为两倍-1
HashTable不会转换为红黑

LinkedHashMap

LinkedHashMap的基本结构

  1. LinkedHashMap底层是数组 + 单项链表 + 双向链表。

  2. 也就是说LinkedHashMap = HashMap+双向链表,数组 + 单向链表就是HashMap的结构,

  3. 双向链表是用来给节点排序的,默认情况下LinkedHashMap是按照插入元素的顺序给元素排序的,当然我们也可以按照访问元素的方式 给链表排序

LinkedHashMap的特点

  • key和value都允许为空
  • key重复会覆盖,value可以重复
  • 有序的
  • LinkedHashMap是非线程安全的
next 是用于维护 HashMap指定table位置上连接的  Entry顺序的;   before、after是用于维护Entry插入的先后顺序的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-11aDPsV7-1662466861944)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20211116103552680.png)]

TreeMap

TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。

所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较

JAVA 多线程

创建线程的几种方式

1)继承 Thread 类创建线程2)实现 Runnable 接口创建线程3)使用Callable和Future创建线程4)使用线程池例如用Executor框架

二、实现 Runnable 接口

三、实现Callable接口

BIO-NIO-AIO 流

BIO编程

BIO 有的称之为 basic(基本) IO,有的称之为 block(阻塞) IO,主要应用于文件 IO 和网络 IO,默认情况下服务端需要对每 个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果 没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行, 这就是阻塞式 IO

直接带来的问题就是,假如说有10000个客户端和服务器连接,那么服务器需要创建10000个线程来解决,问题是如果只有10个客户端需要和服务端互动,其它的所有客户端都只连接上,没有和服务器互动,那么浪费了多少资源

NIO编程

NIO 和 BIO 有着相同的目的和作用,但是它们的实现方式完全不同,BIO 以流的方式操作数据,同时需要自己定义缓冲区(数组)来取数据,而 NIO 以Channel(通道)的方式操作数据(块IO),同时,内部不需要我们去维护创建数组缓冲区,内部提供了buffer缓冲区;块 I/O 的效率比流 I/O 高很多。另外,NIO 是非阻塞式的, 这一点跟 BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。

NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统的 BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

AIO编程

JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的 处理。

AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式, 简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

IO 对比总结

IO 的方式通常分为几种:同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIO。

  • BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。

  • NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。

  • AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持

举个例子再理解一下:

  • 同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,饭馆没做好,你就必须等着!
  • 同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没啊!
  • 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于现在的外卖。
对比总结 BIO NIO AIO
IO 方式 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
API 使用难度 简单 复杂 复杂
可靠性
吞吐量

Spring 框架

Spring 中的 IOC 容器

Spring 中的 IOC 容器: 就是把创建对象的权利交给框架去控制,而不需要人为的去创建,有效地降低代码的耦合度,降低了扩展和维护的成本。

Spring 中的依赖注入

依赖注入是指组件之间的依赖关系由容器在运行期决定,即由容器动态的将某个依赖关系注入到组件之中

通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,谁实现

IoC 和 DI 有什么关系

IoC 是 Spring 中一个极为重要的概念,提供了对象管理的功能,从而省去了人为创建麻烦,而 DI 正是实现 IoC 的方法和手段

Spring 中 bean 的作用域有几种类型?

答:Spring 中 bean 的作用域有四种类型,如下列表:

  • 单例(Singleton):整个应用程序,只创建 bean 的一个实例;
  • 原型(Prototype):每次注入都会创建一个新的 bean 实例;
  • 会话(Session):每个会话创建一个 bean 实例,只在 Web 系统中有效;
  • 请求(Request):每个请求创建一个 bean 实例,只在 Web 系统中有效。

Spring 默认的是单例模式。

Spring 注入方式有哪些?

答:Spring 的注入方式包含以下五种:

  • setter 注入
  • 构造方法注入
  • 注解注入
  • 静态工厂注入
  • 1实例工厂注入

其中最常用的是前三种,官方推荐使用的是注解注入,相对使用更简单,维护成本更低,更直观。

Spring 实现事务

Spring 实现事务有两种方式:编程式事务和声明式事务

  • 编程式事务,使用 TransactionTemplate 或 PlatformTransactionManager 实现

  • 声明式事务,底层是建立在 Spring AOP 的基础上,在方式执行前后进行拦截,并在目标方法开始执行前创建新事务或加入一个已存在事务,最后在目标方法执行完后根据情况提交或者回滚事务。

  • 声明式事务的优点:不需要编程,减少了代码的耦合,在配置文件中配置并在目标方法上添加 @Transactional 注解来实现

Spring 中的 AOP 的底层实现原理是什么?

答:Spring AOP 的底层实现原理就是动态代理。

Spring AOP 的动态代理有两种实现方式,对于接口使用的是 JDK 自带的动态代理来实现的,而对比非接口使用的是 CGLib 来实现的。

16. Spring 中的 Bean 是线程安全的吗?

答:Spring 中的 Bean 默认是单例模式,Spring 框架并没有对单例 Bean 进行多线程的封装处理,因此默认的情况 Bean 并非是安全的,最简单保证 Bean 安全的举措就是设置 Bean 的作用域为 Prototype(原型)模式,这样每次请求都会新建一个 Bean 。

21.spring中的BeanFactory和FactoryBean区别?

  • BeanFactory:该接口是IoC容器的顶级接口,是IoC容器的最基础实现,也是访问Spring容器的根接口,负责对bean的创建,访问等工作
  • FactoryBean:是一种工厂bean,可以返回bean的实例,我们可以通过实现该接口对bean进行额外的操作,例如根据不同的配置类型返回不同类型的bean—参考构建bean的过程
Mybatis扫描到的@Mapper接口的Bean,存放到BeandefinitionMap里面的时候会关联上一个MapperFactoryBean.class,然后实例化的时候都是通过MapperFactoryBean.class去实例化,里面就通过动态代理创建出代理对象

22、spring常用的注解有哪些?

答:

1、用于创建对象的注解
作用:和在xml配置文件中编写一个标签实现的功能一样。

1.@Component : 用于把当前类对象存入Spring容器中。属性:value --- 用于指定bean的id。如果不写该属性,id的默认值是当前类名,且首字母改为小写。
2.@Controller : 一般用在表现层。@RestController
3.@Service : 一般用在业务层。
4.@Repository : 一般用在持久层(dto层)

2、用于注入数据的注解

​ 作用:和在xml配置文件中的标签中写一个标签的功能一样。

1.@Autowired:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。可以作用在变量或者方法上
2.@Qualifier: 在按照类型注入的基础之上再按照名称注入,它在给类成员注入时要和@Autowired配合使用,但是在给方法参数注入是可以单独使用。
3.@Resource : 直接按照bean的id注入,可以独立使用。

3、用于改变作用范围的注解

​ 作用:和在xml配置文件中的标签中使用scope属性实现的功能一样。

@Scope : 用于指定bean的作用范围。

4、和生命周期相关的注解

​ 作用:和在xml配置文件中的标签中使用init-method和destory-method属性实现的功能一样。

@PreDestory : 用于指定销毁方法。
@PostConstruct : 用于指定初始化方法。

5、Spring新注解

@Configuration : 用于指定当前类是一个配置类。
@ComponentScan : 用于通过注解指定Spring在创建容器时要扫描的包。
@Bean : 用于把当前方法的返回值作为bean对象存入Spring的IOC容器中。属性:name --- 用于指定bean的id。当不写时,默认值为当前方法的名称。
@Import : 用于导入其他的配置类。
@PropertySource : 用于指定properties文件的位置
@Value

23、BeanFactory和applicationContext区别

答:

1、描述:

BeanFactory:是Spring里面最顶层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能;

ApplicationContext:应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能

2、两者装载bean的区别:

BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化即延迟实例化

ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化;

3、使用场景:

BeanFactory延迟实例化的优点:

应用启动的时候占用资源很少;对资源要求较高的应用,比较有优势;

ApplicationContext立即实例化的有点

  1. 所有的Bean在启动的时候都加载,系统运行的速度快;
  2. 在启动的时候所有的Bean都加载了,我们就能在系统启动的时候,尽早的发现系统中的配置问题
  3. 建议web应用,在启动的时候就把所有的Bean都加载了。(把费时的操作放到系统启动中完成)

Spring 通知类型

Spring 通知类型总共有 5 种:前置通知、环绕通知、后置通知、异常通知、最终通知。

Spring 声明式事务无效原因

可能的原因如下:

  • MySQL 使用的是 MyISAM 引擎,而 MyISAM 是不支持事务的;
  • @Transactional 使用在非 public 方法上,@Transactional 注解只能支持 public 级别,其他类型声明的事务不会生效;
  • @Transactional 在同一个类中无事务方法 A() 内部调用有事务方法 B(),那么此时 B() 事物不会生效。

Spring 中 Bean 的生命周期

Spring Bean的完整生命周期从创建Spring容器开始,直到最终Spring容器销毁Bean

1.通过BeanDefinition接口得到类的信息
2.如果有多个构造方法,则要推析构造方法
3.确定好构造方法后,进行实外化得到一个对象
4.对对象中的属性加了@Autowired注解的进行注值
5.回调aware方法,如BeanNameAware, BeanFactoryAware
6.如果想对 Bean 进行一些自定义的处理,那么可以让 Bean 实现了 BeanPostProcessor 接口,重写postProcessBeforeInitialization(该方法在bean初始化方法调用前被调用)和postProcessAfterInitialization方法(bean初始化方法调用后被调   用),例如:我们可以修改bean的属性,可以给bean生成一个动态代理实例
7.继承InitializingBean接口,凡是继承该接口的类,在初始化bean的时候都会执行该方法afterPropertiesSet()
8.調用BeanPostProcessor的机始化后的万法, 在这里会进行AOP
9.如果当前创建的bean是单例的则会把bean放入单例池
10.使用bean
11.Spring容器关闭时调用DisposableBean接口的destory()方法,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会自动调用其配置的销毁方法。
(1)实例化Bean:
(2)设置对象属性(依赖注入):
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String
beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传
递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用
setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会
调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调
用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
(8)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

Spring 有哪些优点?

答:Spring 优点如下:

  • 开源免费的热门框架,稳定性高、解决问题成本低;
  • 方便集成各种优秀的框架;
  • 降低了代码耦合性,通过 Spring 提供的 IoC 容器,我们可以将对象之间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度程序耦合;
  • 方便程序测试,在 Spring 里,测试变得非常简单,例如:Spring 对 Junit 的支持,可以通过注解方便的测试 Spring 程序;
  • 降低 Java EE API 的使用难度,Spring 对很多难用的 Java EE API(如 JDBC、JavaMail、远程调用等)提供了一层封装,通过 Spring 的简易封装,让这些 Java EE API 的使用难度大为降低。

Spring | Boot | Cloud 区别

它们的区别如下:

  • Spring Framework 简称 Spring,是整个 Spring 生态的基础。
  • Spring Boot 是一个快速开发框架,让开发者可以迅速搭建一套基于 Spring 的应用程序,并且将常用的 Spring 模块以及第三方模块,如 MyBatis、Hibernate 等都做了很好的集成,只需要简单的配置即可使用,不需要任何的 XML 配置文件,真正做到了开箱即用,同时默认支持 JSON 格式的数据,使用 Spring Boot 进行前后端分离开发也非常便捷。
  • Spring Cloud 是一套整合了分布式应用常用模块的框架,使得开发者可以快速实现微服务应用。作为目前非常热门的技术,有关微服务的话题总是在各种场景下被大家讨论,企业的招聘信息中也越来越多地出现对于微服务架构能力的要求。

Spring 中都是用了哪些设计模式?

答:Spring 中使用的设计模式如下:

  • 工厂模式:通过 BeanFactory、ApplicationContext 来创建 bean 都是属于工厂模式;
  • 单例、原型模式:创建 bean 对象设置作用域时,就可以声明 Singleton(单例模式)、Prototype(原型模式);
  • 观察者模式:Spring 可以定义一下监听,如 ApplicationListener 当某个动作触发时就会发出通知;
  • 责任链模式:AOP 拦截器的执行;
  • 代理模式:在创建代理类时,如果代理的是接口使用的是 JDK 自身的动态代理,如果不是接口使用的是 CGLIB 实现动态代理。

Spring MVC

1. 简述一下 Spring MVC 的执行流程?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDWUvt3a-1662466861945)(C:\Users\Lujun\iCloudDrive\风吹着花海香\那一刻的月亮\imges\image-20210721165146893.png)]

答:

①用户发送请求至前端控制器DispatcherServlet。

②DispatcherServlet收到请求调用HandlerMapping处理器映射器。

③处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

④DispatcherServlet调用HandlerAdapter处理器适配器。

⑤HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

⑥Controller执行完成返回ModelAndView。

⑦HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

⑧DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

⑨ViewReslover解析后返回具体View。

⑩DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。DispatcherServlet响应用户

3. 如何实现跨域访问?

答:常见的跨域的实现方式有两种:使用 Nginx 代理或者在服务器端设置运行跨域。服务器运行跨域的代码如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MyConfiguration {@Beanpublic WebMvcConfigurer corsConfigurer() {return new WebMvcConfigurer() {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的请求规则registry.addMapping("/api/**");}};}
}

8. forward 和 redirect 有什么区别?

答:forward 和 redirect 区别如下:

  • forward 表示请求转发,请求转发是服务器的行为;redirect 表示重定向,重定向是客户端行为;
  • forward 是服务器请求资源,服务器直接访问把请求的资源转发给浏览器,浏览器根本不知道服务器的内容是从哪来的,因此它的地址栏还是原来的地址;redirect 是服务端发送一个状态码告诉浏览器重新请求新的地址,因此地址栏显示的是新的 URL;
  • forward 转发页面和转发到的页面可以共享 request 里面的数据;redirect 不能共享数据;
  • 从效率来说,forward 比 redirect 效率更高。

11. Spring MVC 的常用注解有哪些?

答:Spring MVC 的常用注解如下:

常用注解 说明
@Controller 用于标记某个类为控制器
@ResponseBody 标识返回的数据不是 html 标签的页面,而是某种格式的数据,如 JSON、XML 等
@RestController 相当于 @Controller 加 @ResponseBody 的组合效果
@Component 标识为 Spring 的组件
@Configuration 用于定义配置类
@RequestMapping 用于映射请求地址的注解
@Autowired 自动装配对象
@RequestHeader 可以把 Request 请求的 header 值绑定到方法的参数上

Spring Boot 面试题

2.Spring、Spring Boot、Spring Cloud 是什么关系?

答:它们都是来自于 Spring 大家庭,Spring Boot 是在 Spring 框架的基础上开发而来,让更加方便使用 Spring;Spring Cloud 是依赖于 Spring Boot 而构建的一套微服务治理框架。

16.如何理解 Spring Boot 中的 Stater?

答:Stater 可以理解为启动器,它是方便开发者快速集成其他框架到 Spring 中的一种技术。比如,spring-boot-starter-data- jpa 就是把 JPA 快速集成到 Spring 中。

23、简述springboot启动流程

答:

首先在启动类中发现@SpringBootApplication注解,该注解是一个组合注解,有@Configuration(@SpringBootConfiguration中其实用的也是@Configuration);@EnableAutoConfiguration;@ComponentScan三个是最重要的注解,@SpringBootApplication整合了三个注解使用者写起来看起来都比较简洁

1、@Configuration

它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration,这里的启动类标注了@Configuration之后,本身其实也是一个IoC容器的配置类

2、@EnableAutoConfiguration

@EnableAutoConfiguration简单的说它的作用就是引入@Import的支持,加载AutoConfigurationImportSelector类,利用AutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。借助于Spring框架原有的一个工具类SpringFactoriesLoader,它springFactoriesLoader其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。
SpringBoot启动的流程总览
每一个SpringBoot程序都有一个主入口,这个主入口就是main方法,而main方法中都会调用SpringBootApplication.run方法,查看SpringBootApplication.run方法的源码就可以发现SpringBoot启动的流程主要分为两大阶段:
初始化SpringApplication运行SpringApplication
运行SpringApplication的过程其中运行SpringApplication的过程又可以细分为以下几个部分:
1)SpringApplicationRunListeners 引用启动监控模块
2)ConfigrableEnvironment配置环境模块和监听:包括创建配置环境、加载属性配置文件和配置监听
3)ConfigrableApplicationContext配置应用上下文:包括配置应用上下文对象、配置基本属性和刷新应用上下文

Mybatis 相关面试题

4. # 和 $ 有什么区别?

答:

1、#{ }是预编译处理,MyBatis在处理#{ }时,它会将sql中的#{ }替换为?,然后调用PreparedStatement的set方法来赋值,传入字符串后,会在值两边加上单引号,如上面的值 “4,44,514”就会变成“ ‘4,44,514’ ”

2、是字符串替换,MyBatis在处理{ }是字符串替换, MyBatis在处理是字符串替换,MyBatis在处理{ }时,它会将sql中的${ }替换为变量的值,传入的数据不会加两边加上单引号。

注意:使用${ }会导致sql注入,不利于系统的安全性!

SQL注入:就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。常见的有匿名登录(在登录框输入恶意的字符串)、借助异常获取数据库信息等

SQL注入:就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。常见的有匿名登录(在登录框输入恶意的字符串)、借助异常获取数据库信息等

5.在 MyBatis 中怎么解决实体类属性名和表字段名不一致的问题?

答:通常的解决方案有以下两种方式。

  1. 在 SQL 语句中重命名为实体类的属性名,可参考以下配置:

    <select id="selectorder" parametertype="int" resultetype="com.interview.order">select order_id id, order_no orderno form order where order_id=#{id};
    </select>
    
  2. 通过 映射对应关系,可参考以下配置:

    <resultMap id="BaseResultMap" type="com.interview.mybatislearning.model.UserEntity" ><id column="id" property="id" jdbcType="BIGINT" /><result column="username" property="userName" jdbcType="VARCHAR" /><result column="password" property="passWord" jdbcType="VARCHAR" /><result column="nick_name" property="nickName" jdbcType="VARCHAR" />
    </resultMap>
    <select id="getAll" resultMap="BaseResultMap">select * from t_user
    </select>
    

10.什么是动态 SQL?

答:动态 SQL 是指可以根据不同的参数信息来动态拼接的不确定的 SQL 叫做动态 SQL,MyBatis 动态 SQL 的主要元素有:if、choose/when/otherwise、trim、where、set、foreach 等。 以 if 标签的使用为例:

<select id="findUser" parameterType="com.interview.entity.User" resultType="com.interview.entity.User">select * from t_user where<if test="id!=null">id = #{id}</if><if test="username!=null">and username = #{username}</if><if test="password!=null">and password = #{password}</if>
</select>

13.什么是 MyBatis 的一级缓存和二级缓存?

答:

  • 一级缓存是 SqlSession 级别的,是 MyBatis 自带的缓存功能,并且无法关闭,因此当有两个 SqlSession 访问相同的 SQL 时,一级缓存也不会生效,需要查询两次数据库;
  • 二级缓存是 Mapper 级别的,只要是同一个 Mapper,无论使用多少个 SqlSession 来操作,数据都是共享的,多个不同的 SqlSession 可以共用二级缓存,MyBatis 二级缓存默认是关闭的,需要使用时可手动开启,二级缓存也可以使用第三方的缓存,比如,使用 Ehcache 作为二级缓存。

手动开启二级缓存,配置如下:

<configuration><settings><!-- 开启二级缓存 --><setting name="cacheEnabled" value="true"/></settings>
</configuration>

“VARCHAR” />

select * from t_user


### 10.什么是动态 SQL?答:动态 SQL 是指可以根据不同的参数信息来动态拼接的不确定的 SQL 叫做动态 SQL,MyBatis 动态 SQL 的主要元素有:if、choose/when/otherwise、trim、where、set、foreach 等。 以 if 标签的使用为例:```xml
<select id="findUser" parameterType="com.interview.entity.User" resultType="com.interview.entity.User">select * from t_user where<if test="id!=null">id = #{id}</if><if test="username!=null">and username = #{username}</if><if test="password!=null">and password = #{password}</if>
</select>

13.什么是 MyBatis 的一级缓存和二级缓存?

答:

  • 一级缓存是 SqlSession 级别的,是 MyBatis 自带的缓存功能,并且无法关闭,因此当有两个 SqlSession 访问相同的 SQL 时,一级缓存也不会生效,需要查询两次数据库;
  • 二级缓存是 Mapper 级别的,只要是同一个 Mapper,无论使用多少个 SqlSession 来操作,数据都是共享的,多个不同的 SqlSession 可以共用二级缓存,MyBatis 二级缓存默认是关闭的,需要使用时可手动开启,二级缓存也可以使用第三方的缓存,比如,使用 Ehcache 作为二级缓存。

手动开启二级缓存,配置如下:

<configuration><settings><!-- 开启二级缓存 --><setting name="cacheEnabled" value="true"/></settings>
</configuration>

Java_开发面试_补充相关推荐

  1. 软件开发面试_如何为成功的软件开发工作面试做准备

    软件开发面试 Job interviews are stressful for many people. Besides the pressure of getting hired, you have ...

  2. python web前端开发面试_面试前端,听听别人怎么说!

    分享一个人的面试经验: 一年半经验,百度.有赞.阿里面试总结 前言 人家都说,前端需要每年定期出来面面试,衡量一下自己当前的技术水平以及价值,本人17年7月份,毕业到现在都没出来试过,也没很想换工作, ...

  3. 开发 面试_农行软件开发中心实习面试

    想看实战的可以直接跳到第五部分:农行一面 ps:之前看到同学转发的农行实习,就投了一个测试开发岗(我以为是软件开发岗)下面是面试前一天的准备和面试实战. 目录 1.c++面试准备 2.简历复盘 3.数 ...

  4. 浦发银行java开发面试_浦发银行成都分行研发岗

    为了帮助职业圈网友能够及时了解浦发银行成都分行研发岗的面试流程以及面试过程所涉及的面试问题,职业圈小编把刚获得的浦发银行成都分行研发岗面试经验马上编辑好,快速提供给大家,以便能够尽快帮助到有需要的人. ...

  5. python服务器端开发面试_【网易游戏Python面试】python 服务端开发-看准网

    10.21终面已参加,希望能顺利通过终面拿到offer-❤一共三轮,电话面试+笔试+视频面试,视频面试3V110月19日投的新媒体运营的简历,HR说因为是周末,等工作日再联系我,在周一下午三点我接到了 ...

  6. 博学谷java题库判断_博学谷Java开发面试基础笔试题及答案分享

    博学谷Java开发面试基础笔试题分享:char 型变量中能不能存贮一个中文汉字?为什么?"=="和 equals 方法究竟有什么区别?静态变量和实例变量的区别?是否可以从一个 st ...

  7. 数据库查询某一列大写转化小写字母表示_算法/开发 面试必看! 【数据库】面试题合集...

    本合集整理了计算机专业相关算法/开发面试中遇到的[数据库]相关面试题,后续会不断更新,有需要的小伙伴可以点赞or收藏随时查阅哦! Q:数据库四大特性ACID? Atomicity(原子性):一个事务( ...

  8. datagrid只传入了一部分的数据 未显示全_软件开发面试之数据库事务篇

    软件开发面试之数据库事务篇 不少的小伙伴正在准备或是即将准备后端开发的岗位,对于这个岗位而言数据库是必问的一个知识点,而数据库的事务和数据库的隔离级别又是问到数据库时必问的重点.小编从年初开始也是不断 ...

  9. 程序开发,面试恐惧症_如何克服恐惧并停止讨厌的工作面试

    程序开发,面试恐惧症 by Reuben Reyes 由鲁本·雷耶斯(Reuben Reyes) 如何克服恐惧并停止讨厌的工作面试 (How to conquer your fear and stop ...

最新文章

  1. 学习UI设计能做什么
  2. WinCE Heartbeat Message的实现
  3. LVM和raid结合使用
  4. AD 修改密码返回错误 Set-ADAccountPassword : 从服务器返回了一个参照。
  5. 牛客练习赛69 解方程
  6. “化鲲为鹏,我有话说”如何用鲲鹏弹性云服务器部署《Hadoop伪分布式》
  7. mysql数据库函数详解_MySQL数据库之字符函数详解
  8. yolov3损失函数改进_YOLO V3 深度解析 (下)
  9. win10安装tensorflow-gpu
  10. Frobenius companion matrix
  11. 高通MTK 安卓手机 手机更改SN 序列号 识别码 教程视频
  12. mapboxgl绘制3D线教程
  13. 旭元数艺:只争朝夕 不负韶华
  14. 提高优秀人才忠诚度的六条法则
  15. 【Beta】 第一次Daily Scrum Meeting
  16. 咏南ISAPI中间件
  17. 如何更换AirTag电池?
  18. When you want to give up, remember why you started.
  19. H264编码中Baseline Main High简介
  20. 从网页端进入1加(one plus)手机云空间

热门文章

  1. Python实现TCP客户端和服务器(多线程)
  2. 帮我给余慧写一封2000字的表白信
  3. 实测Windows Mobile下卡巴斯基手机安全软件表现
  4. 我的硬汉观——《丧钟为谁而鸣》读书感悟
  5. xp系统总是弹出宽带连接服务器,XP系统电脑总是弹出拨号连接怎么办
  6. 关于住房公积金支取问题的解答
  7. Pandas处理csv英国降雨数据
  8. 【人工智能与深度学习】基于能量的模型中的对比方法
  9. python安装tensorflow 国内源安装 速度快
  10. MirrorLink(四 VNC --Ubuntu下编译)