JVM+Redis+SpringBoot的面试题有哪些
发表于:2025-01-23 作者:千家信息网编辑
千家信息网最后更新 2025年01月23日,这篇文章主要介绍"JVM+Redis+SpringBoot的面试题有哪些",在日常操作中,相信很多人在JVM+Redis+SpringBoot的面试题有哪些问题上存在疑惑,小编查阅了各式资料,整理出简
千家信息网最后更新 2025年01月23日JVM+Redis+SpringBoot的面试题有哪些
这篇文章主要介绍"JVM+Redis+SpringBoot的面试题有哪些",在日常操作中,相信很多人在JVM+Redis+SpringBoot的面试题有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"JVM+Redis+SpringBoot的面试题有哪些"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
1、JVM
1.1、类加载机制
加载:把.java文件编译成.class文件,生成Class对象验证:验证字节码的准确性准备:给类的静态变量做分配内存,并赋予默认值解析:符号引用和动态链接都变为直接引用初始化:给类的静态变量初始化为指定的值,执行静态代码块
1.2、类加载器
1、根类加载器(Bootstrap classLoader):负责加载lib下的核心类库2、扩展加载器(ExtClassLoader):负责加载lib目录下的ext的jar类包3、应用加载器(AppClassLoader):负责加载ClassPath路劲下的类包(自定义的类)4、自定义类加载器:继承ClassLoader,重写loadClass(),findClass(),一般是只需要重写findClass
1.3、双亲委派机制
双亲加载机制中源码有两个方法:1、loadClass 1)先检查指定的类是否已经加载过了,若已经加载过,则直接返回加载的类 2)若没有加载,则判断有没有父类,有的话则调用父类加载器,或者调用根类加载器(Bootstrap)加载。 3)若父类加载器与Bootstrap加载器都没有找到指定的类,则调用下面的方法(findClass)来完成类加载2、findClass
1.4、为什么要设计双亲加载机制
1、保证类的唯一性2、沙箱安全机制
1.5、全盘委托机制
如果没有显示的使用其他类加载器,则类下的所有依赖与及引用的类都将会有加载该类的类加载器加载
1.6、Tomcat如何自定义类加载机制
1、CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;2、CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;3、SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;4、WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类, 每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;5、模拟实现Tomcat的JasperLoader热加载 原理:后台启动线程监听jsp文件变化,如果变化了找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值给引用,然后加载新的jsp对应的servlet类,之前的那个加载器因为没有gcroot引用了,下一次gc的时候会被销毁 =>总结:每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
1.7、内存模型
私有: 程序计时器:记录当前线程执行到字节码行号 虚拟机栈:内部有许多栈帧,每个栈帧里面包括局部变量表,操作数栈,动态链接,方法出口。 本地方法栈:执行本地的Native方法共享: 堆:内部分为eden区,s0,s1,老年代,保存对象和数组 方法区/永久代(1.8后元空间):保存类信息、常量、静态变量、即时编译器编译后的代码;内部有个运行时常量池,用于保存类的版本、字段、方法、接口等;扩展=>直接内存:通过unsafe,或者netty的DirectByteBuffer申请
1.8、对象创建与分配
1.8.1、创建
1、类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等2、分配内存
//划分内存 1、指针碰撞内存规整,用过的内存放一边,没用过的放一边2、空闲列表内存不规整,使用的和空闲的相互交错,需要一个列表进行存储//并发问题解决1、CAS2、本地线程分配缓冲区(TLAB)把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize指定TLAB大小。3、初始化
为分配到的内存初始化为零值,不设置对象头,若是呀TLAB,可以提前至TLAB分配时进行,保证对象即使不赋初始值也可以直接使用4、设置对象头
对象布局:1、对象头(Header)2、实例数据(Instance Data)3、对齐填充(Padding)5、执行方法
执行方法,也就是所谓的属性赋值与执行构造器
1.8.2、分配
1、栈上分配 通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。/** *对象逃逸分析:分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中 *标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所 代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配 */'结论:栈上分配依赖于逃逸分析和标量替换'2、堆上分配(eden区) 1、先eden区分配,满了young GC,把存活的对象放入s02、再eden区分配,满了young GC,把s0存活的对象和eden区存活的对象放入s1,3、重复1,2操作3、大对象进入老年代 大量连续的内存空间的对象4、长期存活对象进入老年代 在2(堆上分配)中,每次移动都会给当前对象设置个计数器,默认15,CMS默认6,则会young gc放入老年代5、对象动态年龄判断 当一批对象的总大小大于s区内存大小的50%,则大于等于这批对象年龄最大值的对象,就可以进入老年代6、空间担保机制 年轻代每次young gc之前JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会看一个"-XX:-HandlePromotionFailure"(jdk1.8默认就设置了) 的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"。
1.9、何判断一个类是无用的类
1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。2、加载该类的 ClassLoader 已经被回收。3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
1.10、finalize()方法最终判定对象是否存活
1. 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。2. 第二次标记 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救 自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出"即将回收"的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。//注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
1.11、四大引用
1、强引用:普通的变量引用2、软引用(SoftReference):将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。 //使用场景:浏览器的后退按钮3、弱引用(WeakReference):将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用4、虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
1.12、对象回收算法
1、引用计数法:循环引用无法解决2、Gc root算法 将"GC Roots" 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
1.13、四大垃圾回收算法
1、标记复制算法//定义:将内存分两块,每使用一块,都会在内存用完之后,将存活的对象复制到另一块中,再把使用过的空间清理//问题:浪费空间,永远浪费一半空间2、标记清除算法//定义:标记存活对象,统一回收未被标记的对象//问题:1、效率问题:效率不高,对象过多,就要浪费时间标记对象 2、空间问题:产生大量的不连续的碎片3、标记整理算法 跟标记清除一样,多了个整理存活对象的过程4、分代收集算法 年轻代复制算法,老年代标记整理
1.14、CMS(标记-清除算法=>写屏障 + 增量更新)
1.14.1、运作过程
1、初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。2、并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。3、重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到'增量更新算法'做重新标记。4、并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为`三色标记法`里面的黑色不做任何处理5、并发重置:重置本次GC过程中的标记数据。
1.14.2、三色标记法
黑色:'表示对象已经被垃圾收集器访问过',且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过, 它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。灰色:'表示对象已经被垃圾收集器访问过',但这个对象上至少存在一个引用还没有被扫描过。白色:'表示对象尚未被垃圾收集器访问过'。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
1.14.3、漏标-读写屏障(解决方案)
1、增量更新(Incremental Update)+写屏障 增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。2、原始快照(Snapshot At The Beginning,SATB)+写屏障 原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
1.5、G1(复制算法=>写屏障 + SATB)
1.5.1、运作过程
初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 并发标记(Concurrent Marking):并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。最终标记(Remark,STW):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到'增量更新算法'做重新标记。筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的==回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划
1.5.2、G1的RS和CT
'已记忆集合RememberedSets:', 存储着其他分区中的对象对本分区对象的引用,每个分区有且只有一个RSet。用于提高GC效率。YGC时,GC root主要是两类:栈空间和老年代分区到新生代分区的引用关系。所以记录老年代分区对新生代分区的引用Mixed GC时,由于仅回收部分老年代分区,老年代分区之间的引用关系也将被使用。所以记录老年代分区之间的引用因此,我们仅需要记录两种引用关系:老年代分区引用新生代分区,老年代分区之间的引用。因为每次GC都会扫描所有young区对象,所以RSet只有在扫描old引用young,old引用old时会被使用。'卡表,Card Table:'Java堆划分为相等大小的一个个区域,这个小的区域(一般size在128-512字节)被当做Card,而Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用单字节的信息映射着一个Card。当Card中存储了对象时,称为这个Card被脏化了(dirty card)。 对于一些热点Card会存放到Hot card cache。同Card Table一样,Hot card cache也是全局的结构。
1.5.3、Collect Set
Collect Set(CSet)是指,在Evacuation阶段,由G1垃圾回收器选择的待回收的Region集合。G1垃圾回收器的软实时的特性就是通过CSet的选择来实现的。对应于算法的两种模式fully-young generational mode和partially-young mode,CSet的选择可以分成两种: 在fully-young generational mode下:顾名思义,该模式下CSet将只包含young的Region。G1将调整young的Region的数量来匹配软实时的目标; 在partially-young mode下:该模式会选择所有的young region,并且选择一部分的old region。old region的选择将依据在Marking cycle phase中对存活对象的计数。G1选择存活对象最少的Region进行回收。
1.5.4、Marking bitmaps/TAMS
Marking bitmap是一种数据结构,其中的每一个bit代表的是一个可用于分配给对象的起始地址bitmap其中addrN代表的是一个对象的起始地址。绿色的块代表的是在该起始地址处的对象是存活对象,而其余白色的块则代表了垃圾对象。G1使用了两个bitmap,一个叫做previous bitmap,另外一个叫做next bitmap。previous bitmap记录的是上一次的标记阶段完成之后的构造的bitmap;next bitmap则是当前正在标记阶段正在构造的bitmap。在当前标记阶段结束之后,当前标记的next bitmap就变成了下一次标记阶段的previous bitmap。TAMS(top at mark start)变量,是一对用于区分在标记阶段新分配对象的变量,分别被称为previous TAMS和next TAMS。在previous TAMS和next TAMS之间的对象则是本次标记阶段时候新分配的对象。previous TMAS 和 next TAMS白色region代表的是空闲空间,绿色region代表是存活对象,橙色region代表的在此次标记阶段新分配的对象。注意的是,在橙色区域的对象,并不能确保它们都事实上是存活的。
1.11、ZGC的颜色指针
组成
GC信息保存在指针中。每个对象有一个64位指针,这64位被分为18位:预留给以后使用1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合)1位:Marked1标识1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC42位:对象的地址(所以它可以支持2^42=4T内存)
优势:
一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
1.12、100%CPU排查
1使用top命令查看cpu占用资源较高的PID2、通过jps 找到当前用户下的java程序PID(jps -l 能够打印出所有的应用的PID)3、使用 pidstat -p4、找到cpu占用较高的线程TID5、将TID转换为十六进制的表示方式6、通过jstack -l(使用jstack 输出当前PID的线程dunp信息)7、 查找 TID对应的线程(输出的线程id为十六进制),找到对应的代码
1.13、JIT
JIT是一种提高程序运行效率的方法。通常,程序有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,而动态解释执行的则是一句一句边运行边翻译。
1.14、逃逸分析
逃逸分析是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。
2、Disruptor的原理(生产消费者模型)
2.1、解决队列速度慢
1、环形数组结构: 为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好(CPU加载空间局部性原则)。2、元素位置定位: 数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。3、无锁设计: 每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据
2.2、数据结构
框架使用RingBuffer来作为队列的数据结构,RingBuffer就是一个可自定义大小的环形数组。除数组外还有一个序列号(sequence),用以指向下一个可用的元素,供生产者与消费者使用
2.3、核心组件
1、RingBuffer--Disruptor底层数据结构实现,核心类,是线程间交换数据的中转地;2、Sequencer--序号管理器,生产同步的实现者,负责消费者/生产者各自序号、序号栅栏的管理和协调,Sequencer有单生产者,多生产者两种不同的模式,里面实现了各种同步的算法;3、Sequence--序号,声明一个序号,用于跟踪ringbuffer中任务的变化和消费者的消费情况,disruptor里面大部分的并发代码都是通过对Sequence的值同步修改实现的,而非锁,这是disruptor高性能的一个主要原因;4、SequenceBarrier--序号栅栏,管理和协调生产者的游标序号和各个消费者的序号,确保生产者不会覆盖消费者未来得及处理的消息,确保存在依赖的消费者之间能够按照正确的顺序处理, Sequence Barrier是由Sequencer创建的,并被Processor持有;5、EventProcessor--事件处理器,监听RingBuffer的事件,并消费可用事件,从RingBuffer读取的事件会交由实际的生产者实现类来消费;它会一直侦听下一个可用的号,直到该序号对应的事件已经准备好。6、EventHandler--业务处理器,是实际消费者的接口,完成具体的业务逻辑实现,第三方实现该接口;代表着消费者。7、Producer--生产者接口,第三方线程充当该角色,producer向RingBuffer写入事件。8、Wait Strategy:Wait Strategy决定了一个消费者怎么等待生产者将事件(Event)放入Disruptor中。
2.4、Wait Strategy
1、BlockingWaitStrategy Disruptor的默认策略是BlockingWaitStrategy。在BlockingWaitStrategy内部是使用锁和condition来控制线程的唤醒。BlockingWaitStrategy是最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现。2、SleepingWaitStrategy SleepingWaitStrategy 的性能表现跟 BlockingWaitStrategy 差不多,对 CPU 的消耗也类似,但其对生产者线程的影响最小,通过使用LockSupport.parkNanos(1)来实现循环等待。一般来说Linux系统会暂停一个线程约60µs,这样做的好处是,生产线程不需要采取任何其他行动就可以增加适当的计数器,也不需要花费时间信号通知条件变量。但是,在生产者线程和使用者线程之间移动事件的平均延迟会更高。它在不需要低延迟并且对生产线程的影响较小的情况最好。一个常见的用例是异步日志记录。3、YieldingWaitStrategy YieldingWaitStrategy是可以使用在低延迟系统的策略之一。YieldingWaitStrategy将自旋以等待序列增加到适当的值。在循环体内,将调用Thread.yield(),以允许其他排队的线程运行。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。4、BusySpinWaitStrategy 性能最好,适合用于低延迟的系统。在要求极高性能且事件处理线程数小于CPU逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
2.5、写数据
单线程写数据的流程: 1、申请写入m个元素; 2、若是有m个元素可以入,则返回最大的序列号。这儿主要判断是否会覆盖未读的元素; 3、若是返回的正确,则生产者开始写入元素。
3、Spring
3.1、Spring流程(IOC下的Bean的生命周期,循环依赖,构造函数)
1. 启动ApplicationContext 两个重要的子类: AnnotationConfigApplicationContext(用的最多) ClassPathXmlApplicationContext2. 初始化AnnotationBeanDefinitionReader a.读取spring内部的初始的 beanFactoryPostProcess 和 其他的几种 beanPostProcess(AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)) 1. AnnotationAwareOrderComparator:解析@Order进行排序 2. ContextAnnotationAutowireCandidateResolver 3. ConfigurationClassPostProcessor:解析加了@Configuration、@ComponentScan、@ComponentScans、@Import等注解(最重要的类) 4. AutowiredAnnotationBeanPostProcessor:解析@Autowired5. RequiredAnnotationBeanPostProcessor:解析@Required6. CommonAnnotationBeanPostProcessor:负责解析@Resource、@WebServiceRef、@EJB7. EventListenerMethodProcessor:找到@EventListener8. DefaultEventListenerFactory:解析@EventListener b. 在ConfigurationClassPostProcessor类中有主要是为了解析加了@Configuration、@ComponentScan、@ComponentScans、@Import等注解,在这里面他有一个细节,就是加了@Configuration里面,他会把当前类标注成full类,就会产生一个aop的动态代理去加载当前类,没有的话就把当前类标注成lite类,也就是普通类处理。3. 初始化ClassPathBeanDefinitionScanner a. 程序员能够在外部调用doScan(), 或者 继承该类可以重写scan规则用来动态扫描注解,需要注册到容器。 b. spring内部是自己重新new 新的对象来扫描。4. 执行register()方法,一般来说就是注册我们的配置类 a. 先把此实体类型转换为一个BeanDefinition5. 执行refresh(),先初始化比如BeanFactory这类基础的容器。 a. 执行invokeBeanFactoryPostProcessors(),主要的作用是扫描包和parse (类->beanDefinition)1. 执行BeanFactoryPostProcessor的子接口BeanDefinitionRegistryPostProcessor方法postProcessBeanDefinitionRegistry(BeanDefinitionRegistry register) 作用:主要是扫描包找到合格的类,解析类 i. 先执行程序员通过 context.add的 ii. 再执行spring内部的和程序员通过注解注册的 并且特殊的比如 实现了PriorityOrdered,Order iii. 最后再执行其他的 BeanDefinitionRegistryPostProcessor 2. 再执行BeanFactoryPostProcessor接口 方法postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)**作用:1. 和子接口一样 扫描包找到合格的类,解析类 2. 为@Configuration的类做代理 i. 先执行子接口中的方法 ii. 再执行程序员通过 context.add添加的 iii. 再执行spring内部和程序员通过注解注册的 并且特殊的比如 PriorityOrdered,Order iv. 最后执行其他的 BeanFactoryPostProcessor 他们在spring中唯一的实现类是ConfigurationClassPostProcessor 将类变成beanDefinition的流程:1. 从BeanDefinitionRegistry中获取所有的bd2. 判断是否该bd是否被解析过,主要根据bd中是否有full或者lite属性。3. 将未解的bd去,循环解析bd a. 先处理内部类 b. 处理@PropertrySource 环境配置 c. 处理@ComponentScan 解析带有ComponentScan,会调用ClassPathBeanDefinitionScanner,根据包路径,和匹配规则扫描出合格类。 d. 处理@Import i. 先处理 ImportSelect,执行selectImports(), 事务的初始化和aop的代理类型,是否传递代理 就是在这里做的。 ii. 然后处理 ImportBeanDefinitionRegistrar接口,会放到该bd的一个Map中,循环map统一去执行实现方法registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry); iii. 最后处理普通的类,同样会递归去解析该bd e. 处理@ImportResourcef. 处理@Bean g.处理接口bd4. 然后将所有的合格的类,转换成bd,注册到beanDefinitionRegistry。 b. 然后会注册beanPostProcessor,国际化等等,不是很重要 c. 比较重要的,也是将bd变成bean的方法 finishBeanFactoryInitialization(),实例化非延迟的单例(循环依赖) d. 一般来说首先getBeanDefinition之前,都要合并bd。 1)第一次getSingleton,从单例池拿是否存在,单例的第一次一般是不存在,并且会判断是否在正在创建bean的set集合中。 singletonObjects 一级缓存,完整的bean singletonFactories 二级缓存,存的是代理bean工厂 earlySingletonObjects 三级缓存,一般是是半成品的bean a. 如果存在,直接返回 b. 如果不存在,并且不在正在创建bean的set集合中,直接返回null c. 如果不存在,并且在正在创建bean的set集合中。从三级缓存拿。 i. 存在,直接三级缓存拿。 ii. 不存在,通过二级缓存,代理的bean工厂拿,获得该bean,然后将得到bean放到三级缓存中,移出二级缓存。(原因是生产bean工厂周期比较长的。)2)第二次getSingleton a. 首先将beanName放到正在创建bean的set集合中,表示正在创建该bean b. 然后会调用二级缓存去获取bean,lambda延迟机制,就会调用表达式中,也就是createBean,这时候是正在获取代理bean工厂会走一个完整的bean 的生命周期。 c. 然后从bean工厂获取bean。 1. 构造函数:第一次 BeanPostProcessor,是否需要代理bean。如果代理bean直接返回,不会走下面的流程。 2. 第二次BeanPostProcessor,推断构造函数 a. 首先推断构造函数数组 i. 没提供构造函数=========设置构造函数数组为null ii. 一个默认的构造函数======设置构造函数数组为null iii. 一个不是默认的构造函数===设置构造函数数组为该构造函数 iv. 一个构造方法并且加了@Autowired====设置构造函数数组为该构造函数 v. 多个模糊构造函数========设置构造函数数组为null vi. 多个构造函数,有唯一加了@Autowired==设置构造函数数组为该构造函数 vii. 多个构造函数,多个@Autowired(required为false)===设置构造函数数组为多个@Autowired viii. 提供多个构造函数,多个@Autowired(required为true)=== 抛异常 b. 如果推断构造数组不为null 或者,自动注入类型为构造函数,或者设置了构造函数的属性(xml方式)等,还有一种传参数金来 i. 推断构造函数, 1. 只有个构造函数,最终被确定的构造函数,2. 有多个构造函数 a. 优先修饰符最开放的,public>protected>Default>private b. 修饰符一样找属性最多的 ii. 推断参数, 1. 首先找出所有候选的参数类型,实例化属性 2. 然后类型是接口,那么判断是否开启宽松构造 a. 未开启报错。 b. 开启了,判断子类的差值(spring有个算法),默认差值是-1024。 c. 差值低的为该参数,一样的丢到模糊集合中,随机取出。 c. 构造函数数组为null,直接通过无参实例化构造函数。 3. 第三次BeanPostProcessor ,缓存了注入元素的信息 injectionMetadataCache: key: beanName或者类名 value:为解析出的属性(包括方法)集合 InjectionMetadata。 InjectionMetadata:可以存放method 和 属性。类中有字段判断是否是属性 isField。 checkedInitMethods: 存放 @PostConstruct 。 checkedDestroyMethods:存放 @PreDestroy。 a. AutowiredAnnotationBeanPostProcessor 主要解析加了 @Autowired 和 @Value 方法和属性。 b. CommonAnnotationBeanPostProcessor 主要解析加了 @Resource属性。 c. InitDestroyAnnotationBeanPostProcessor 主要解析加了 @PostConstruct 和 @PreDestroy方法 d. 还有很多 4. 第四次 BeanPostProcessor,生产代理工厂,作用是可以解决循环依赖 a. 先判断是否允许循环依赖,可通过api修改属性,或者直接改源代码。 b. 然后判断当前bean是否是正在创建的bean c. 调用populateBean 主要作用,注入属性。 5. 第五次BeanPostProcessor,控制是否需要属性注入,目前没什么作用。 再注入缓存的属性之前,先通过 自动注入模型 a. byType byName,找到setter,注入。体现了@Autowired不是自动注入,而是手动注入。6. 第六次 BeanPostProcessor ,完成注解的属性填充** **@Autowired @Resource** a. 注入之前还是会再找一下是否有其他需要注入的属性和方法。 b. 属性的调用属性注入方法,函数调用函数的注入方法。 i. 通过属性的类型,从BeanDefinitionMap中找属性名称(接口则找找这个接口的子类), ii. 然后判断我们当前需要注入的属性是不是这几个类型,得到候选的类型。 iii. 当有多个类型,再通过属性名称去推断出唯一候选的属性名。如果找到多个候选的属性名,抛异常。 iv. 只有唯一的属性名,通过类名去获取类型。 v. 最终通过找到唯一匹配的beanName和类型去注入。当没有找到匹配的名称和类型,就会抛异常。 c. 在注入的时候,有循环依赖的时候,会去先去实例化该属性。 7. 第七次BeanPostProcessor ,处理实现各种aware接口的重写方法 + 生命周期回调 执行@PostConstruct方法 执行 实现InitializingBean接口的,重写方法,和 xml 中的 init-method="xxx"方法。 8. 第八次BeanPostProcessor ,做aop代理 a. 判断是否需要做代理 i. 找出所有的候选切面,比如 加了 @Aspect的类 , 事务的切面 ii. 做匹配逻辑,比如根据切面的连接点表达式 或者 类中方法是否加了@Transaction去 判断当前类是否匹配出,合适的切面集合。 iii. 然后对匹配出的切面集合,做排序。 iv. 能匹配上说明就做代理 b. 哪种代理(默认用JDK动态代理) i. 当代理工厂设置ProxyTargetClass为 true,则为CGLIB代理。 ii. 当目标对象为类,则也用为CGLIB代理。 iii. 只有proxyTarget为 false,并且为目标对象为接口,则用JDK动态代理 c. 执行代理invokeHandler(这里主要是JDK的代理,invoke方法) i. 首先会进行普通方法的判断比如hashcode eques等等,没有就给代理类创建。不是很重要 ii. 然后判断是否需要将代理传递下去,就是绑定到 ThreadLocal中(在事务中,这个特别的重要) iii. 获取执行链,也就是这个目标对象的通知集合。(也就是所有过滤器链,实现了MethodIntercept。) iv. 执行过滤器执行链,类似于火炬传递。(事务的methodInterceptor也在这里会被调用) 1. 判断通知是否执行完,没有执行完去,按顺序执行通知。 2. 依次调用对应的通知,最终都会去回调到proceed()方法。 3. 最终执行完代理方法,就会调用本身的方法。比较特殊的是around是在通知里,执行被代理的目标方法。
3.2、AOP
源码底层的实现是动态代理 动态代理有cglib和jdk实现1、JDK动态代理通过反射机制实现: 通过实现InvocationHandlet接口创建自己的调用处理器; 通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理; 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型; 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入; JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。 2、CGLib动态代理:CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过 CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程,底层是ASM实现 3、两者对比: JDK动态代理是面向接口的。 CGLib动态代理是通过字节码底层继承要代理类来实现(被代理类不能被final关键字所修饰,)。 4、使用注意: 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制); 如果要被代理的对象不是个实现类,那么Spring会强制使用CGLib来实现动态代理
4、SpringMVC
4.1、SpringMVC执行流程
1)前端控制器DispatcherServlet 由框架提供作用:接收请求,处理响应结果 2)处理器映射器HandlerMapping由框架提供 作用:根据请求URL,找到对应的Handler 3)处理器适配器HandlerAdapter由框架提供 作用:调用处理器(Handler|Controller)的方法 4)处理器Handler又名Controller,后端处理器 作用:接收用户请求数据,调用业务方法处理请求 5)视图解析器ViewResolver由框架提供 作用:视图解析,把逻辑视图名称解析成真正的物理视图 支持多种视图技术:JSTLView,FreeMarker... 6)视图View,程序员开发 作用:将数据展现给用户
5、SpringBoot
5.1、SpringBoot的自动装箱
1、@SpringBootApplication=>2、@EnableAutoConfiguration=>3、@Import(AutoConfigurationImportSelector.class)=>调用getCandidateConfigurations()方法,里面有个读取Meta-info/spring.factories
5.2、Starter自动装配
1.编写一个带有@Configuration注解的类,如果按条件加载可以加上@ConditionalOnClass或@ConditionalOnBean注解2.在classpath下创建META-INF/spring.factories文件,并在spring.factories中添加 org.springframework.boot.autoconfigure.EnableAutoConfiguretion =上面定义类的全类名
6、RabbitMQ
6.1、消息丢失
6.1.1、生产者丢失消息
RabbitMQ提供transaction和confirm模式来确保生产者不丢消息;transaction机制就是说:发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降;confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后;rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
6.1.2、消息队列丢数据:消息持久化。
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。那么如何持久化呢?1. 将queue的持久化标识durable设置为true,则代表是一个持久的队列2. 发送消息的时候将deliveryMode=2这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据
6.1.3、消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!
消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息; 解决方案:处理消息成功后,手动回复确认消息。(手动ACK)
6.1.4、消息不被重复消费(幂等性)
保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性=》redis,数据库自增
6.1.5、如何保证RabbitMQ消息的顺序性?
同一个queue里面消息是有序的,保证消息发送到同一个queue就好了。单线程消费保证消息的顺序性;对消息进行编号,消费者处理消息是根据编号处理消息;
7、Redis
7.1、Redis线程模型(reactor模型)
Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
7.2、Redis核心数据结构使用与原理
7.2.1、String
1、底层: 是SDS实现,其编码方式有int,raw,embstr,主要存在于redisObject的ptr属性中 a. 默认是int,正式类型是longb. 当字符串大于32字节的字符串值,设置为raw c.当 字符串保存的小于等于32字节,设置为embstr 总结: 在Redis中,存储long、double类型的浮点数是先转换为字符串再进行存储的。 raw与embstr编码效果是相同的,不同在于内存分配与释放,raw两次,embstr一次。 embstr内存块连续,能更好的利用缓存在来的优势int编码和embstr编码如果做追加字符串等操作,满足条件下会被转换为raw编码;embstr编码的对象是只读的,一旦修改会先转码到raw。2、应用场景 a. 单值缓存 b. 分布式锁 c. 计数器 d. Web集群session共享 e. 分布式系统全局序列号 f. 对象缓存
7.2.2、List
1、底层: List是一个有序(按加入的时序排序)的数据结构,Redis采用quicklist(双端链表) 和 ziplist 作为List的底层实现2、应用场景 a.Stack(栈) = LPUSH + LPOP b. Queue(队列)= LPUSH + RPOP c. Blocking MQ(阻塞队列)= LPUSH + BRPOP d. 微博和微信公号消息流 e. 微博消息和微信公号消息
7.2.3、Hash
1、底层: Hash 数据结构底层实现为一个字典( dict ),也是RedisBb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置2、应用场景: a. 对象缓存 b. 电商购物车 c. 购物车操作(添加商品,增加数量,商品总数,删除商品,获取购物车所有商品)
7.2.4、Set
1、底层: Set为无序的,自动去重的集合数据类型,Set数据结构底层实现为一个value为null的字典( dict ),当数据可以用整形表示时,Set集合将被编码为intset数据结构。两个条件任意满足时Set将用hashtable存储数据。 a. 元素个数大于 set-max-intset-entries ,b. 元素无法用整形表示 set-max-intset-entries 512 // intset 能存储的最大元素个数,超过则用hashtable编码2、应用场景: a. 微信抽奖小程序 b. 微信微博点赞,收藏,标签 c. 集合操作实现微博微信关注模型 d. 集合操作实现电商商品筛选
7.2.5、Sort Set
1、底层:Sort Set 为有序的,自动去重的集合数据类型,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储 zset-max-ziplist-entries 128 // 元素个数超过128 ,将用skiplist编码 zset-max-ziplist-value 64 // 单个元素大小超过 64 byte, 将用 skiplist编码2、应用场景: a. 点击新闻 b. 展示当日排行前十 c. 七日搜索榜单计算 d. 展示七日排行前十
7.2.6、GeoHash
1、底层: 空间填充曲线,也就是经纬度换编码,二分取右为1地球纬度区间是[-90,90], 如某纬度是39.92324,可以通过下面算法来进行维度编码:1)区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.92324属于右区间[0,90],给标记为12)接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.92324属于左区间 [0,45),给标记为03)递归上述过程39.92324总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.9281674)如果给定的纬度(39.92324)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会 产生一个序列1011 1000 1100 0111 1001,序列的长度跟给定的区间划分次数有关。2、应用场景: 摇一摇 附近位置
7.2.7、BloomFilter
底层是取n个hash,做位运算
7.2.8、HyperLogLog(基数统计):统计用户访问量
7.3、持久化
RDB:快照,bgsave异步创建dump.rdb文件,底层是fork+cow实现。AOF:追加,底层是先写入缓存中,然后每隔一段时间会fsync到磁盘,也是fork一个子进程运行:默认加载rdb文件,如果同时启用了RDB 和 AOF 方式,AOF 优先,启动时只加载 AOF 文件恢复数据,若开启混合持久化方式则会创建一个文件,上面是rdb,下面是aof的数据,启动加载这个文件
7.4、Redis持久化数据和缓存怎么做扩容?
1、如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。2、如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样
7.5、内存淘汰策略
a) 针对设置了过期时间的key做处理: 1、volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。 2、volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。 3、volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。 4、volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。b) 针对所有的key做处理: 5、allkeys-random:从所有键值对中随机选择并删除数据。 6、allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。 7、allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。c) 不处理:8、noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error)OOM command not allowed when used memory",此时Redis只响应读操作。
7.6、数据删除策略
1、被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key2、主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key3、当前已用内存超过maxmemory限定时,触发主动清理策略4、LRU 算法(Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一次访问时间作为参考。5、LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的数据,以次数作为参考
7.7、缓存击穿/缓存雪崩/缓存穿透/热点缓存key重建优化/缓存与数据库双写不一致
7.7.1、缓存击穿(失效)
7.7.1.1、原因
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉
7.7.1.2、解决方案
1、在批量增加缓存时将这一批数据的缓存过期时间设置为一个时间段内的不同时间。2、分布式锁
7.7.2、缓存雪崩
7.7.2.1、原因
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
7.7.2.2、解决方案
1) 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。2) 依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
7.7.3、缓存穿透
7.7.3.1、原因
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。造成缓存穿透的基本原因有两个: 第一, 自身业务代码或者数据出现问题。 第二, 一些恶意攻击、 爬虫等造成大量空命中。
7.7.3.2、解决方案
1、缓存空对象2、布隆过滤器(redission里面有个getBloomFilter()方法实现,==布隆过滤器不能删除数据,如果要删除得重新初始化数据==)
7.7.4、热点缓存key重建优化
7.7.4.1、原因
开发人员使用"缓存+过期时间"的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害: 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。 重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。要解决这个问题主要就是要避免大量线程同时重建缓存。
7.7.4.2、解决方案
互斥锁(也就是所谓的分布式锁)
7.7.5、缓存与数据库双写不一致
1、可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。2、可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。3、先删缓存,再写数据库(1)timer异步淘汰(本文没有细讲,本质就是起个线程专门异步二次淘汰缓存)(2)总线异步淘汰(3)读binlog异步淘汰//blog.csdn.net/zhouhengzhe?t=1
到此,关于"JVM+Redis+SpringBoot的面试题有哪些"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!
对象
数据
缓存
方法
标记
代理
处理
消息
线程
函数
属性
动态
内存
时间
生产
算法
分配
接口
阶段
类型
数据库的安全要保护哪些东西
数据库安全各自的含义是什么
生产安全数据库录入
数据库的安全性及管理
数据库安全策略包含哪些
海淀数据库安全审计系统
建立农村房屋安全信息数据库
易用的数据库客户端支持安全管理
连接数据库失败ssl安全错误
数据库的锁怎样保障安全
用数据库管理技术处理的数据
杭州传奇游戏软件开发公司
网关于网络安全的手抄报
大学生网络安全宣传目的
超图软件开发者大会什么时候开
天台直销软件开发价格表
蓝齐儿网络技术招聘
国家网络安全教育意义
收件服务器的主机名指啥
网络安全峰会多久一届
服务器上iis部署网站
北京数据库日志审计功能
谭州教育网络技术有限公司
做手机软件开发要学多久
网络安全招聘官网是哪个
mysql数据库对比
虚拟机连接公司服务器
pgsql创建数据库
对网站服务器进行安全配置
mysql 数据库教程
苏州梯田软件开发有限公司
数据库将成绩为空的选课记录删除
化和网络安全工作要点
网络安全法企业处罚的规定有哪些
河北餐饮软件开发市场前景如何
江苏串口服务器云服务器
东阳市吴宁海宸网络技术服务部
spss自带的数据库
网络安全威胁教案
通信软件开发行业前景