java和其他语言的区别和优势在哪?
(内存动态分配和垃圾收集技术)
C语言:贴近内存,运行极快,效率极高。但是他要进行指针和内存管理,指针可以直接操作内存,但是却没有做数组越界等检查,容易出错。而且自己申请的空间需要自己去释放。并且这些问题编译期间发现不了,运行时才会暴露。
C++:添加了面向对象的功能,兼容了c语言,加入了静态类型的检测,但是太复杂。
Java:摆脱了硬件平台的束缚,实现了垮平台。
提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界问题。
实现了热点代码的检测和运行时编译及优化。
实现内存动态分配和垃圾收集技术。
内存自动分配和销毁,消除了c中的指针,垃圾自动回收和垮平台。
Java是解释型还是编译型语言。(Java属于解释型语言)
• 编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。
• 解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束!
Jdk和jre的区别
Java程序设计语言,java虚拟机,java API类库统称为JDK,jdk是用于支持java程序开发的最小环境
Java SE API子集和java虚拟机这两部分统称为JRE,JRE是支持程序运行的标准环境。
Thread和runnable的区别,start方法和run方法的区别
Thread类实现了Runnable接口,都需要重写里面Run方法
实现Runnable接口比继承Thread类所具有的的优势:适合多个相同的程序代码的线程去处理同一个资源;在这就是可以避免Java中的单继承的限制,增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。线程池只能放入实现Runnable 类线程,不能直接放入继承Thread的类
通过继承Thread类来创建的线程不共享实例变量,通过实现Runnable接口来创建的线程对象共享线程类的实例变量
调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于就绪状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体,所以如果直接调用run,就和一个普通的方法没什么区别,是不会创建新的线程的
Object自带的两个方法,一个是equals和hashCode,在什么情况下需要重写equals和HashCode方法。
Java中的超类Object类中定义的equals()方法是用来比较两个引用所指向的对象的内存地址是否一致,String中重写了equals()方法和hashcode。
HashSet存放元素时,根据元素的hashCode值快速找到要存储的位置,如果这个位置有元素,两个对象通过equals()比较,如果返回值为true,则不放入;如果返回值为false,则这个时候会以链表的形式在同一个位置上存放两个元素,这会使得HashSet的性能降低,因为不能快速定位了。还有一种情况就是两个对象的hashCode()返回值不同,但是equals()返回true,这个时候HashSet会把这两个对象都存进去,这就和Set集合不重复的规则相悖了;所以,我们重写了equals()方法时,要按照b,c规则重写hashCode()方法!
equals 相等,hashCode 一定要相等。
重写了 hashCode 也要重写 equals。
hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致。
equals 的对称、反射、传递等特性。
什么是线程安全,为什么会出现线程安全,如何解决?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
线程安全主要讲的基本上就是线程之间对共享的资源(比如变量、文件、数据库之类的)的操作不互相妨碍。有时候多个线程可能同时(只是一个概念,不是绝对的同时)操作一个变量,这样这个变量的值就不能确定了。
解决线程安全:
不在线程之间共享该状态变量(实例或者静态域的数据)
将状态变量修改为不可变的变量
在访问状态变量时使用同步
什么是死锁,为什么会产生死锁,解决方法是什么?
死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。
定位死锁最常见的方式就是利用jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测
解决:避免使用多个锁,并且只有需要时才持有锁
Java OOM异常,java内存溢出异常有哪些
不断的创建对象,达到最大堆的容量限制后就会产生内存溢出异常
线程请求的栈深度大于虚拟机所允许的最大深度,抛出stackoverflowError(递归调用)
虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
方法区溢出
不健壮代码的特征及解决办法
尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。
对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;
我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域;
String str = “aaa”;
String str2 = “bbb”;
String str3 = str + str2;//假如执行此次之后str,str2以后再不被调用,那它就会被放在内存中等待Java的gc去回收,程序内过多的出现这样的情况就会报上面的那个错误,建议在使用字符串时能使用StringBuffer就不要用String,这样可以省不少开销;
尽量少用静态变量,因为静态变量是全局的,GC不会回收的;
避免集中创建对象尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;例如显示的声明数组空间,而且申请数量还极大。导致该数组分配了很多内存空间,而且该数组不能及时释放
不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用hashtable,vector
创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃
一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of Memory Error 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
组合和聚合的区别,继承的区别
组合:体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;
聚合:整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;
设计模式中要“少用继承,多用组合”,
组合优点:
- 不破坏封装,整体类与局部类之间松耦合,彼此相对独立
- 具有较好的可扩展性
- 支持动态组合。在运行时,整体对象可以选择不同类型的局部对象
- 整体类可以对局部类进行包装,封装局部类的接口,提供新的接口
缺点: - 整体类不能自动获得和局部类同样的接口
- 创建整体类的对象时,需要创建所有局部类的对象
继承优点: - 子类能自动继承父类的接口
- 创建子类的对象时,无须创建父类的对象
缺点: - 破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
- 支持扩展,但是往往以增加系统结构的复杂度为代价
- 不支持动态继承。在运行时,子类无法选择不同的父类
- 子类不能改变父类的接口
如何进行拷贝(clone),深拷贝和浅拷贝的区别
实现cloneable接口,重写clone方法,调用super.clone()方法,还可以用序列化实现深拷贝,实现Serializable接口,进行序列化和反序列化。
浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针
浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
深复制—-在计算机中开辟了一块新的内存地址用于存放复制的对象。
filter和listener的区别。
Filter过滤器:拦截资源是按照 filter-mapping 配置节出现的顺序来依次调用 doFilter() 方法的
filter使用户可以改变一个request并且修改一个response,filter不是一个Servlet,不能生产一个response,能够在一个request到达Servlet之前预处理request,也可以在离开Servlet时处理response。Filter其实是一个“Servlet chaining”(Servlet链)
一个filter包括:
1) 在Servlet被调用之前截获,并可以检查Servlet request
2) 根据需要修改request头和request数据
3) 根据需要修改response头和response数据
4) 在Servlet被调用之后截获
利用filter进行权限的管理
Listener监听器:主要完成对java内置对象的状态(创建和销毁)及属性的变化
监听ServletContext、HttpSession、ServletRequest
1) 对application(application 是 ServletContext 的实例)内置对象,session和request进行监听
2) 作用:通过监听用户session,监听用户上线与退出,显示在线用户
4、八大基本数据类型有哪些,对应的大小
5、Jdk7和jdk8的区别
JDK7
1) Jdk7中switch操作可以支持String类型(实际上对int类型值进行匹配,通过对case后面的String对象调用hashCode()方法,得到一个int类型的hash值,然后用这个hash值来唯一标识这个case。当进行匹配时,首先调用这个字符串的hashCode方法,获取一个hash值,用这个hash值来匹配所有的case,若没有匹配成功,则不存在,否则接着调用会接着调用字符串的equals方法进行匹配,所以String变量不能为空)
2) 可以在catch代码块中捕获多个异常类型
try {
//可能会抛出Exception1和Exception2异常的代码
}catch(Exception1 | Exception2) {
// 处理异常的代码
}
3) 对数值字面量进行了改进(增加了二进制字面量的表示:整数类型可以用二进制来表示;
在数字中可以添加分隔符,只能被用于数字中间,编译时这些下划线会被编译器去掉,例如123_12,)
4) 使用泛型的时候增加了类型推断机制
Map<String, String> map = new HashMap<>();
5) 增加了try-with-resource语句:可以保证在该语句执行完之后关闭每个资源
6) 增加了fork/join框架用来增强对处理多核并行计算的支持,他的应用场景为:如果一个应用能被分解成多个子任务,并且组合多个子任务的结果就能获得最终答案。
Fork就是把一个大任务切割成若干个子任务并行执行,join就是合并这些子任务的执行结果,最后得到这个大任务的结果。
JDK8
1) 增加了lambda表达式的支持。Lambda表达式是一个匿名函数(没有函数名的函数),lambda表达式允许把函数作为一个方法的参数,lambda表示式是通过函数式接口(只有一个方法的普通接口,用@FunctionalInterface)实现的
2) 接口增加了方法的默认实现和静态方法,jdk1.8通过使用关键字default可以给接口中的方法添加默认方法,此外,接口中还可以定义静态方法。为了接口升级,在原有的设计中,如果想要给接口中添加一个新的方法,会导致所有实现这个接口的类都需要被修改,可以使用默认方法或者静态方法。
public interface Inter {
void f();
default void g() {
System.out.println("default");
}
static void h() {
System.out.println("static");
}}
3) 方法引用,可以直接引用java类或者对象的方法。
Arrays.sort(people, Comparator.comparing( Person :: getAge ));
4) 引入重复注解机制,相同的注解在同一个地方可以声明多次
5) 添加stream类—引入函数式编程
1、 集合
hashMap为什么线程不安全,不安全会发生什么现象,为什么
HashMap 在并发环境可能出现无限循环占用 CPU(扩容)、size 不准确等诡异的问题。
HashMap、TreeMap和HashTable的区别
HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能O(1), HashMap 在并发环境可能出现无限循环占用 CPU(扩容)、size 不准确等诡异的问题。
TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断
Hashtable 是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用
解决哈希冲突的常用方法有:
开放定址法
基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key) (i=1,2,…,k)当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
TreeSet和HashSet、TreeMap的区别?
TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。
相同点:
单列集合,元素不可重复
不同点
- 底层存储的数据结构不同
HashSet底层用的是HashMap哈希表结构存储,而TreeSet底层用的是TreeMap树结构存储 - 存储时保证数据唯一性依据不同
HashSet是通过复写hashCode()方法和equals()方法来保证的,而TreeSet通过Compareable接口的compareTo()方法来保证的 - 有序性不一样
HashSet无序,TreeSet有序
存储原理:
HashSet:底层数据结构是哈希表,本质就是对哈希值的存储,通过判断元素的hashCode方法和equals方法来保证元素的唯一性,当hashCode值不相同,就直接存储了,不用在判断equals了,当hashCode值相同时,会在判断一次euqals方法的返回值是否为true,如果为true则视为用一个元素,不用存储,如果为false,这些相同哈希值不同内容的元素都存放一个桶里(当哈希表中有一个桶结构,每一个桶都有一个哈希值)
TreeSet:底层的数据结构是红黑树(一种自平衡的二叉树,自平衡是指如果有空的左/右子树,元素会先入空的左/右子树,而不会一直往一个方向添加元素出现不平衡现象),可以对Set集合中的元素进行排序,这种结构,可以提高排序性能, 根据比较方法的返回值确定的,只要返回的是0.就代表元素重复
TreeSet的add(E e)方法,底层是根据实现Comparable的方式来实现的唯一性,通过compare(Object o)的返回值是否为0来判断是否为同一元素。
compare() == 0,元素不入集合。
compare() > 0 ,元素入右子树。
compare() < 0,元素入左子树。
而对其数据结构:自平衡二叉树做前(常用)、中、后序遍历即可保证TreeSet的有序性
集合类:java中的collection中整体的架构是怎样的?
集合的长度是可变的,且存储元素类型是可以任意,而数组长度是固定的,且存储元素类型要保持一致。
虽然通常概念上我们也会把 Map 作为集合框架的一部分,但是本身并不是真正的集合(Collection)。
List,有序集合
Set:不允许重复元素的,也就是不存在两个对象 equals 返回 true。适应需要保证元素唯一性的场合。
Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。这里不包括 BlockingQueue,因为通常是并发编程场合,所以被放置在并发包里。
有序的Map:LinkedHashMap和TreeMap
LinkedHashMap 通常提供的是遍历顺序符合插入顺序,它的实现是通过为键值对维护一个双向链表。可实现FIFO和LRU算法,构建一个空间占用敏感的资源池(缓存区),希望可以自动将最不常被访问的对象释放掉,这就可以利用 LinkedHashMap 提供的机制来实现。
TreeMap,它的整体顺序是由键的顺序关系决定的,通过 Comparator 或 Comparable(自然顺序)来决定。
concurrent类有哪些
ConcurentHashMap
ArrayBlockingQueue:用数组实现的有界阻塞队列,其内部按先进先出的原则对元素进行排序,其中put方法和take方法为添加和删除的阻塞方法,阻塞队列是通过一个重入锁ReenterLock和两个Condition条件队列实现的
LinkedBlockingQueue:一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,阻塞队列是通过两个重入锁ReenterLock和两个Condition条件队列实现的
CopyOnWriteArrayList : 使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组,整个add操作都是在锁(ReentrantLock)的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来
所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList的读操作是可以不用加锁的。
2、 java虚拟机
java的内存体系是如何管理的,java的内存模型是什么。
Java内存模型:为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台上都能达到一致的内存访问效果。主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量(包括实例变量、静态字段和构成数组对象的元素,不包括局部变量与方法参数,后者是线程私有的,不会被共享)。
划分:java内存模型规定所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递均需要通过主内存来完成。
主内存中主要对应java堆中的对象实例数据部分。
工作内存中对应虚拟机栈中的部分区域。
Java内存模型主要是定义虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,这里的变量指的是实例字段,静态字段和构成数组对象的元素等全局变量,不包括局部变量和方法参数,因为后者是私有的,不会被共享。(如果局部变量是引用类型,它引用的对象在java堆中可被各个线程共享,但是引用本身在java栈的局部变量表中,他是线程私有的)
Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在主内存中进行,而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程之间变量值的传递均需要通过主内存来完成。
主内存主要对应java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
Jvm启动时将内存分为那几块,分别存放那些东西,有什么区别。Class文件的定义是在哪块区域
程序计数器:java多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,为了使线程切换后能恢复到正确的执行位置,每条线程都需要执行一个独立的线程计数器,各个线程的程序计数器之间互不影响,独立存储,是线程私有的内存,如果执行的是native方法,这个计数器值为空
Java虚拟机栈:线程私有,他的生命周期与线程相同,每个方法在执行的同时会创建一个
本地方法栈帧用于存储局部变量表、操作数栈,动态链接,方法出口等信息。存放了一些局部变量表部分。会抛出StackOverflowError和OOM异常
本地方法栈:虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。会抛出StackOverflowError和OOM异常
Java堆:被所有的线程共享,在虚拟机启动的时候创建,存放对象的实例和数组等,是垃圾收集器管理的主要区域,采用的分代收集,
方法区:线程共享,存储被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据
运行时常量池:属于方法区的一部分,用于存储编译期生成的各种字面量和符号引用,
栈和堆的区别,什么是栈帧
栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,他是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧储存了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
物理地址:
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-整理,现在基本使用分代(即新生代使用复制算法,老年代使用标记-整理)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存区别
堆因为是物理上不连续的(逻辑上连续),所以分配的内存是在运行期确认的,因此大小不固定。
栈分配的内存大小要在编译期就确认,大小是固定的
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储(根据逃逸分析,并不是所有的实例都分配在了堆上,当确定一个对象不会逃逸出方法之外,那就可以让这个对象在栈上分配,并随着栈帧出栈而销毁,还有同步消除(变量不会逃逸出线程,无法被其他线程访问,就可以消除同步)和标量替换)
栈存放:局部变量,操作数栈,动态链接和方法出口。该区更关注的是程序方法的执行。
PS:
静态变量,常量,类信息放在方法区(内存共享)
静态的对象还是放在堆。
程序的可见度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
Jvm1.7用的什么回收机制
G1垃圾收集器,使用标记-整理算法和复制算法结合,分代收集:根据新生代和老年代收集,新生代中选择复制算法,老年代标记-整理算法
垃圾回收算法的工作原理。
通过引用计数算法和可达性分析判断对象是否存活的
用虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象作为GC Root
CMS用的什么回收算法
基于标记-清除算法实现,并发收集、低停顿
是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:
(1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。
(2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
(3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
(4). 并发清除,回收内存空间,时间很长。
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
G1收集器-整体采用标记-整理,局部基于复制算法
(1). 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
(2). 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
(3). 空间整合。基于标记 - 整理算法,无内存碎片产生。
(4). 可预测的停顿。可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region的集合。
调用system.gc会触发什么操作?
System.gc()用于调用垃圾收集器,在调用时,垃圾收集器将运行以回收未使用的内存空间。它将尝试释放被丢弃对象占用的内存。然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用, 只是提醒虚拟机:程序员希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
垃圾回收的minorGC和fullGC的区别
对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次minor GC,指发生在新生代的垃圾收集,新生代使用复制收集算法。
Major/Full GC :老年代的GC,出现了full gc,一般会伴随minor gc,老年代使用标记整理
类加载机制,谈到双亲委派模型后会问到哪些违反了双亲委派模型?为什么?为什么要双亲委派?好处是什么?
类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,从类被加载到虚拟机内存开始,到卸载出内存为止。
双亲委派模型:启动类加载,扩展类加载器、应用程序类加载器。如果一个类加载器收到了类加载的请求,他首先不会自己加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有当父类反馈自己无法加载请求时,子加载器会尝试自己去加载,若找不到回报ClassNotfoundException.
双亲委派模型的好处:java类随着他的类加载器一起具备了一种带有优先级的层次关系。例如Object类,无论哪一个类要加载它,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境真都是一个类。
破坏双亲委派模型:重写loadClass()方法,线程上下文加载器(父类加载器可以请求子类加载器去完成类加载工作,例jdbc),代码热替换、模块热部署(自定义的类加载机制实现
)
Happens-before(先行发生原则)
操作A先行发生于操作B,在发生操作B之前,操作A产生的影响能被操作B观察到,用来判断数据是否存在竞争、线程是否安全的依据。
垃圾回收机制:
• 那些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法)
• 什么时候回收? (堆的新生代、老年代、永久代的垃圾回收时机,MinorGC 和 FullGC)
• 如何回收?(三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法 和 七种垃圾收集器)
HashMap 的原理?当谈到线程不安全时自然引申出 ConcurrentHashMap ,它的实现原理?
Hashmap:底层由数组和链表组成,每个数组里面存的是一个单向链表,不支持并发操作,线程不安全,java8中当链表中的元素超过8个,会自动将链表转换为红黑树,提高效率
ConcurrentHashMap:[kənˈkɜːrənt]支持并发操作,线程安全,利用分段锁实现,是一个segment数组,segment通过继承ReentrantLock来进行加锁每次需要加锁锁住的是一个segment,保证了每个segment都是线程安全的,默认数组大小为16(不可扩容),并且之间互不干扰,并发执行
3、 数据库
InnoDB和MyISAM的区别
InnoDB:mysql的默认存储引擎,具有自动崩溃恢复特性,采用MVCC(多版本并发控制)来支持高并发,并且实现四个标准的隔离级别,默认是可重复读,并通过间隙锁策略来防止幻读。InnoDB表示基于聚簇索引建立的,采用的是行级锁,最大程度的支持并发处理。
MyISAM:MySQL5.1之前默认的存储引擎,提供了全文检索、压缩等,但MyISAM不支持事务和行级锁,数据库崩溃后无法安全恢复,对于只读的数据,或者表比较小可以继续使用MyISAM;MyISAM对整张表加锁,而不是针对行,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。
压缩:如果表在创建并导入数据之后,以后不会进行修改操作,就可以对表进行压缩,可以极大地减少磁盘空间的占用,因此减少磁盘io,从而 提升查询性能。
全文检索:一种特殊类型的索引,查找的是文本中的关键词,而不是直接比较索引中的值,类似于搜索引擎做的事情,而不是简单的where匹配。使用一种特殊的B-Tree索引,共有两层,第一层是所有的关键字,然后对于每一个关键字的第二层,包含的是一组相关的“文档指针”。
Innodb有哪些索引,具体怎么实现的。
前缀索引(blob,text或者很长的varchar类型的列,计算合适的前缀长度的一个方法就是计算完整列的选择性,使得前缀的选择性接近于完整列的选择性,缺点:mysql无法使用前缀索引做order by 和 group by,也无法使用前缀索引做覆盖扫描)
多列索引:索引合并,使用表上的多个单列索引来定位指定的行,查询时能够同时使用这两个单列索引进行扫描,并将结果进行合并。
聚簇索引
覆盖索引:包含所有需要查询的字段的值,
哪些场景会让你不会命中索引
索引列是表达式的一部分,或者是函数的参数,如果要查询的列不是独立的列,就不会使用索引
InnoDB:mysql的默认存储引擎,采用MVCC(多版本并发控制)来支持高并发,并且实现四个标准的隔离级别,默认是可重复读,并通过间隙锁策略来防止幻读。InnoDB表示基于聚簇索引建立的,采用的是行级锁,最大程度的支持并发处理。
聚簇索引:是一种数据存储方式,实际上是在同一个结构中保存了B-Tree索引和数据行,聚簇索引的叶子页包含了行的全部数据,节点页只包含索引列,一个表中只能有一个聚簇索引,并且是通过主键聚集数据。
B-Tree索引:所有的值都是存储在叶子节点并且按照顺序存储的,并且每一个叶子到根的距离相同,B-Tree索引能够加快访问数据的速度,因为存储引擎不需要进行全表查询来获取需要的数据,而是从索引的根节点开始进行搜索,根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。
哈希索引:基于hash表实现,只有精确匹配索引的所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列表计算出一个hash码,哈希索引将所有的哈希码存储在索引中,同时在hash表中保存指向每个数据行的指针。
分库分表如何设计?垂直拆分、水平拆分?
分表:对于访问极为频繁且数据量巨大的单表来说,我们首先要做的就是减少单表的记录条数,以便减少数据查询所需要的时间,提高数据库的吞吐,这就是所谓的分表!
用户id是最常用的分表字段。因为大部分查询都需要带上用户id,这样既不影响查询,又能够使数据较为均衡地
分库:分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库!
与分表策略相似,分库可以采用通过一个关键字取模的方式,来对数据访问进行路由
分库分表:有时数据库可能既面临着高并发访问的压力,又需要面对海量数据的存储问题,这时需要对数据库既采用分表策略,又采用分库策略,以便同时扩展系统的并发处理能力,以及提升单表的查询性能,这就是所谓的分库分表。
数据库做了分区分表就要保证一致性,采用分布式事务或者最终一致性来处理,服务之间的通信和交互依赖定义的良好接口,通常使用restful样式的aip或者透明的RPC调用框架。
in 和exist 的区别和好处(如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in)
select * from A
where id in(select id from B)
以上查询使用了in语句,in()只执行一次,它查出B表中的所有id字段并缓存起来.之后,检查A表的id是否与B表中的id相等,如果相等则将A表的记录加入结果集中,直到遍历完A表的所有记录.
in()适合B表比A表数据小的情况
数据库的事务,自动提交和手动提交,mysql是否开启事务,如果手动提交后不关闭会怎样,回滚机制是怎么工作的
—> mysql采用默认提交的模式,如果不是显示的开始一个事务,则每个查询都被当做一个事务执行提交操作,在当前链接中,可以通过设置autocommit来启动(1)或者禁用(0)自动提交模式。当设置autocommit=0时,表示所有的查询都是在一个事务中,直到显示的执行commit提交或者rollback回滚,该事务结束后,同时又开始了另一个新的事务。默认采用的隔离级别是可重复读。
4、 多线程
平时怎么使用多线程?有哪些好处?线程池的几个核心参数的意义?
并发访问数据库。提高系统并发量,增加吞吐量。
Int corePoolSize:线程池基本大小,如果当线程池的当前大小超过了基本大小时,当前线程将被终止。当工作队列满了的情况下会创建出超过这个数量的线程。
Int maximumPoolSize:最大大小,可同时活动的线程数量的上限
Long keepAliveTime:存活时间,当某线程的空闲时间超过了存活时间则可回收。
TimeUnit unit: keepAliveTime时间单位
BlockingQueue
ThreadFactory threadFactory:线程工厂,线程池创建线程通过线程工厂方法完成
RejectedExecutionHandler handler:饱和策略,当工作队列被填满后,通过拒绝执行或抛弃某项任务尝试执行新的任务。
如何创建一个线程池,当我创建了一个大小为10的线程池时,当我第11个线程到来时会出现什么情况。如何超过了线程队列的大小会怎样?
放到工作队列中,当工作队列被填满后,通过饱和策略拒绝执行或抛弃某项任务尝试执行新的任务。
线程间通信的方式?
通过共享变量进行通信
通过队列进行通信(消费者生产者模式)
Synchronized同步、while轮询、wait/notify机制、管道通信
Java线程启动时我想指定内存大小使用什么参数,和方法。
通过ProcessBuilder的start方法来创建一个新进程
Xmx:指定java程序的最大堆内存, 使用java -Xmx5000M -version判断当前系统能分配的最大堆内存
Xms:指定最小堆内存, 通常设置成跟最大堆内存一样,如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。设置一样可以减轻伸缩堆大小带来的压力
Xmn:年轻代大小,整个堆大小=年轻代大小 + 年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
Xss:指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误(StackOverflowError)
Java中如何创建一个进程
1、Runtime.exec()方法来创建一个进程,由于任何进程只会运行于一个虚拟机实例当中,所以在Runtime中采用了单例模式,即只会产生一个虚拟机实例,最终还是通过ProcessBuilder类的start方法来创建的
2、通过ProcessBuilder的start方法来创建进程,ProcessBuilder是一个final类
线程和进程的区别
进程是系统进行资源分配和调度的一个独立单位.
线程是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,线程共享进程所拥有的全部资源。
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
创建一个线程,常用的方法。
继承Thread类创建线程类
通过Runnable接口创建线程类
通过Callable和FutureTask创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
终止线程 4 种方式
1、 正常结束
2、 使用退出标志退出线程
3、 Interrupt [ˌɪntəˈrʌpt] 方法结束线程
4、 stop 方法终止线程(线程不安全)
sleep 与 wait 区别
1、对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于 Object类中的。
2、 sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然 保持者,当指定的时间到了又会自动恢复运行状态。
3、 在调用sleep()方法的过程中,线程不会释放对象锁。
4、调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此 对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
start 与 run 区别
- start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕, 可以直接继续执行下面的代码。
- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运 行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运 行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
JAVA锁
对 Java 锁的理解?
synchronize,Lock接口的应用
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
锁的具体应用场景
悲观锁(synchronized):比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁(Lock-tryLock):比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入 值是否一样,一样则更新,否则失败。
例如ReentrantReadWriteLock读写锁,缓存用到了
synchronize,Lock的区别及优缺点
synchronized:jdk1.6之前采用的是悲观锁的机制,用的是重量级锁,线程获取的是独占锁。1.6之后做了优化(适应性自旋、锁消除,锁粗化等),其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁切换导致效率很低。
Lock:采用乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)
Synchronized和ReentranLock(riˈɛntrəntlɑːk可重入锁)的区别 - ReentrantLock 通过方法 lock()与unlock()来进行加锁与解锁操作,与synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出 现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- ReentrantLock相比synchronized的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock。
具体解释一下CAS
CAS(比较并交换):包含三个操作数-需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A的值时,CAS才会通过原子方式用新值B的值来更新V的值,否则不会执行任何操作。
CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。当使用引用类型的时候会出现ABA问题,例如对链表进行删除元素操作,JUC提供了带有标记的原子引用类来保证CAS的正确性。
5、 数据结构
排序算法:
6、 计算机网络
http报文头里面含有什么
请求头
返回头
文本域
文本编码
谈谈你所理解的 HTTP 协议?
http协议是无状态的,简化了服务器的设计,支持大量并发请求。
采用TCP作为运输层协议,保证了数据的可靠性。但是http协议本身是无连接的,在通信双方交换http报文之前不需要先建立http连接。
HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。
1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。
2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。
访问一个网址的流程:
a:域名解析
b:tcp3次握手建立连接
c:建立连接后,发起http请求
d:服务器端响应http请求,浏览器得到http请求的内容
e:浏览器解析html代码,并请求html代码中的资源
f:浏览器通过页面渲染,展现在用户面前
对 TCP 的理解?三次握手?滑动窗口?
TCP:一种面向连接的、可靠的、基于字节流的传输层通信协议,全双工模式
三次握手:为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手,第三步是防止了乙的一直等待而浪费自己的时间,而不是为了保证甲能够正确回应乙的信息。
三次握手的目的:为了防止已失效的连接请求报文段突然又传送到了服务端,而产生错误。
四次挥手:当 Client 发出FIN报文段时,只是表示 Client 已经没有数据要发送了,Client 告诉 Server,它的数据已经全部发送完毕了;但是,这个时候 Client 还是可以接受来自 Server 的数据;当 Server 返回ACK报文段时,表示它已经知道 Client 没有数据发送了,但是 Server 还是可以发送数据到 Client 的;当 Server 也发送了FIN报文段时,这个时候就表示 Server 也没有数据要发送了,就会告诉 Client ,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
滑动窗口(以字节为单位):TCP协议作为一个可靠的面向流的传输协议,其可靠性和流量控制由滑动窗口协议保证。
流量控制:发送方的发送窗口不能超过接收方给出的接收窗口的数值。
已发送并收到确认的数据(不再发送窗口和发送缓冲区之内)、已发送但未收到确认的数据(位于发送窗口之中)、允许发送但尚未发送的数据以及发送窗口外发送缓冲区内暂时不允许发送的数据;
TCP/UDP有哪些优缺点?
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
TCP可靠的四大手段:
顺序编号:tcp在传输文件的时候,会将文件拆分为多个tcp数据包,每个装满的数据包大小大约在1k左右,tcp协议为保证可靠传输,会将这些数据包顺序编号
确认机制:当数据包成功的被发送方发送给接收方,接收方会根据tcp协议反馈给发送方一个成功接收的ACK信号,信号中包含了当前包的序号
超时重传:当发送方发送数据包给接收方时,会为每一个数据包设置一个定时器,当在设定的时间内,发送方仍没有收到接收方的ACK信号,会再次发送该数据包,直到收到接收方的ACK信号或者连接已断开
校验信息:tcp首部校验信息较多,udp首部校验信息较少
为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。 现在把三次握手改成仅需要两次握手,死锁是可能发生的。
为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
MSL(最大报文段生存时间)
7、 操作系统
操作系统的组成
1、驱动程序是最底层的、直接控制和监视各类硬件的部分,它们的职责是隐藏硬件的具体细节,并向其他部分提供一个抽象的、通用的接口。
2、内核是操作系统之最内核部分,通常运行在最高特权级,负责提供基础性、结构性的功能。
3、支承库是一系列特殊的程序库,它们职责在于把系统所提供的基本服务包装成应用程序所能够使用的编程接口(API),是最靠近应用程序的部分。例如,GNU C运行期库就属于此类,它把各种操作系统的内部编程接口包装成ANSI C和POSIX编程接口的形式。
4、外围是指操作系统中除以上三类以外的所有其他部分,通常是用于提供特定高级服务的部件。例如,在微内核结构中,大部分系统服务,以及UNIX/Linux中各种守护进程都通常被划归此列。
操作系统中的缓存
缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,可以进行高速数据交换的存储器,它先于内存与CPU交换数据。
操作系统中虚拟内存是什么
虚拟内存允许执行进程不必完全在内存中。每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。
进程和线程
(1)进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现操作系统的并发。
(2)线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发。
(3)一个程序至少有一个进程,一个进程至少有一个线程,线程依赖进程的存在。
(4)进程执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
8、 io
什么叫做BIO、NIO、AIO、Netty
BIO:同步阻塞式IO,服务器端与客户端通过三次握手后建立连接,连接成功,双方通过I/O进行同步阻塞式通信。
NIO:NIO类库是jdk1.4中引入的,它弥补了同步阻塞IO的不足,它在Java提供了高速的,面向块的I/O。同步阻塞IO是以流的方式处理数据,而NIO是以块的方式处理数据。面向流的I/O通常比较慢, 按块处理数据比按(流式的)字节处理数据要快得多。
AIO:异步非阻塞I/O,NIO2的异步套接字通道时真正的异步非阻塞I/O,它对应unix网络驱动中的事件驱动I/O,它不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写,简化了NIO编程模型。
Netty:是业界最流行的NIO框架之一,具有健壮性,性能好,可定制性,可扩展性。
SSM框架
Spring框架中运用到了哪些设计模式,具体体现是什么
工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
Bean在spring中的生命周期是怎样的(*)
1.实例化bean对象
2.注入bean的所有属性
3.设置bean的id
调用BeanNameAware接口的setBeanName(String)方法
4.设置bean工厂
调用BeanFactoryAware接口的setBeanFactory()方法
5.设置实例所在的上下文空间
调用ApplicationContextAware接口的setApplicationContext()方法,传入Spring上下文
6.调用后置处理器的预初始化方法
调用BeanPostProcessor接口的postProcessorBeforeInitialization()方法
7.执行InitializingBean[ɪˈnɪʃəlaɪzɪŋ]的afterPropertiesSet()
8.调用使用init-method配置的自定义初始化方法
9.调用后置处理器的后初始化方法
调用BeanPostProcessor接口的postProcessorAfterInitialization()方法
10.调用DisPosableBean接口的destory()方法
11.调用使用destroy-method配置的自定义销毁由方法
由BeanFactroy创建的bean
没有第五步
Spring 的IOC为什么叫做控制反转或者依赖注入
控制反转是从容器的角度来说的,以前对象都是应用程序new出来的,对象之间的依赖也是应用程序自己创建的,从而导致类与类之间高耦合,难于测试。现在,由Spring管理bean的生命周期以及bean之间的关系,降低了业务对象替换的复杂性,提高了组件之间的解耦。
对资源进行集中管理,实现了资源的可配置和易管理;
隐藏细节,不用自己组装,我们只负责调用。
依赖注入是从应用程序的角度来说的,即,应用程序依赖Spring管理的bean以及bean之间的关系。Spring容器中有很多bean的实例,它会将符合依赖关系的对象通过注入的方式进行关联,建立bean与bean之间的联系。
常见注入方式有:属性注入,构造器注入,数组注入,集合注入(list、map)。
自动装配(只适用于 ref类型,也就是引用类型 )
约定优于配置
自动装配:
<bean … class=”org.entity.Course” autowire=”byName|byType|constructor|no” > byName本质是byId
byName: 自动寻找:其他bean的id值=该Course类的属性名
byType: 其他bean的类型(class) 是否与 该Course类的ref属性类型一致 (注意,此种方式 必须满足:当前Ioc容器中 只能有一个Bean满足条件 )
constructor: 其他bean的类型(class) 是否与 该Course类的构造方法参数 的类型一致;此种方式的本质就是byType
Spring的AOP怎么理解的
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP采取横向抽取机制,支持将公共业务提取出来(例如:安全/事务/日志)进行集中管理,面向核心业务编程,只需要关注业务本身,而不用去关注公共业务。使用AOP可以将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
应用:事务管理、性能监视、安全检查、缓存 、日志等
原理:
Spring中实现AOP的方式有三种,分别为,基于AspectJ注解方式实现、基于Schema的xml配置、基于ProxyFactoryBean代理实现,但是底层都是基于动态代理实现的,动态代理有JDK动态代理和CGLIB动态代理,AOP默认使用的是JDK动态代理,当目标类没有接口时,使用CGLIB动态代理,也可以在配置文件中配置proxy-target-class=true,只使用CGLIB动态代理。
AOP术语
- target目标类:需要被代理的类。例如:UserService
- Joinpoint连接点:所谓连接点是指那些可能被拦截到的方法。例如:所有的方法
- PointCut切入点:已经被增强的连接点。例如:addUser()
- advice通知/增强,增强代码。例如:after、before
- Weaving织入:是指把增强advice应用到目标对象target来创建新的代理对象proxy的过程.
- proxy代理类
- Aspect切面:是切入点pointcut和通知advice的结合
一个线是一个特殊的面。
一个切入点和一个通知,组成成一个特殊的面。
Spring 的常用注解
控制反转
@Component,标注为一个普通的bean
@Service,@Repository依赖注入
@Autowired
@Qulifier
@Resource
@Value全局
@Configuration,代替配置文件,相当于beans
@ComponentScan,配置扫描包
@Scope,配置bean的生命周期
如何解决 get 和 post 乱码问题?get乱码,手动转换
String name = new String(xx.getBytes(“iso-8859-1”),”utf-8”);
不行的话,再:
在server.xml中,修改编码和工程编码一致post乱码,在web.xml中配置字符过滤的filter,采用的类是Spring的CharacterEncodingFilter
Spring 的事务事务的特性ACID
原子性、一致性、隔离性、持久性事务是一系列操作的最小单元,在Spring中,一个session对应一个事务,要么全部成功要么全部失败,如果中间有一条出现异常,那么回滚之前的所有操作
Spring中有自己的事务管理机制,实现方式共有两种:编程式和声明式。
编程式事务:使用TransactionTemplate,重写execute方法实现事务管理
声明式事务:使用AOP面向切面编程实现,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
实现声明式事务管理又有两种方式:
基于XML配置文件;
基于注解,使用@Transactional注解,将事务规则应用到业务逻辑中
- 事务最重要的两个特性是事务的传播级别(7种)和数据隔离级别(4种)
传播级别定义的是事务的控制范围。
我使用过的是REQUIRED和SUPPORTS
EQUIRED(增删改):在事务中执行,如果没有事务存在,则会重新创建一个。
SUPPORTS(查):使用当前的环境执行,如果当前存在事务,则使用这个事务;如果当前没有事务,则不使用事务
事务隔离级别定义的是事务在数据库读写方面的控制范围。
未授权读取,授权读取,可重复读取,序列化(隔离级别最高)
事务隔离的实现是基于悲观锁和乐观锁
Mysql默认的隔离级别是可重复读
谈谈你对SpringMVC的理解
- 是一个基于MVC的web框架
- SpringMVC是Spring的一个模块,是Spring的子容器,子容器可以拿父容器的东西,但是父容器不能拿子容器的东西
- SpringMVC的前端控制器DispatcherServlet,用于分发请求,使开发变得简单
- SpringMVC三大组件
1)HandlerMapping:处理器映射器
用户请求路径到Controller方法的映射
2)HandlerAdapter:处理器适配器
根据handler(controlelr类)的开发方式(注解开发/其他开发) 方式的不同去寻找不同的处理器适配器
3)ViewResolver:视图解析器
可以解析JSP/freemarkerr/pdf等
Spring MVC 原理
Spring的模型-视图-控制器(MVC)框架是围绕一个DispatcherServlet 来设计的,这个Servlet 会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染 等,甚至还能支持文件上传。
流程图:
(1)用户发送请求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;
(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器;
(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。
SpringMVC 常用注解都有哪些? - @Controller,使用它标记的类就是一个SpringMVC Controller 对象
- @RequestMapping,处理请求映射地址
- @PathVariable,用于对应restful风格url中的参数
@RequestMapping(value=”/happy/{dayid}”) findPet(@PathVariable String dayid) - @RequestParam,将请求的参数绑定到方法中的参数上
@RequestParam(value = “name”, required = false)String name - @ResponseBody,将返回类型直接输入到http response body中
- @RequestBody,方法参数直接被绑定到http request body中
- @ModelAttribute和@SessionAttributes,用来传递和保存数据,有很多的坑,不建议使用
Mybatis:mapper.xml中写sql语句,其中#符和$符有什么区别。
输入参数ParameterType,输出参数:ResultType
#{}、${}的区别:
a.#{任意值}
${value} ,其中的标识符只能是value
b.#{}自动给String类型加上’’ (自动类型转换)
${} 原样输出,但是适合于 动态排序(动态字段)
c.#{}可以防止SQL注入
${}不防止
${}、#{}相同之处:都可以 获取对象的值
Mybatis 的使用步骤是什么样的?
1. 读取配置文件
2. 创建SqlSessionFactoty
3. 创建SqlSession
4. 操作数据库
5. 提交事务(增删改)
6. 关闭session
使用 MyBatis 的 mapper 接口调用时有哪些要求
namespace命名空间指向一个特定的dao接口(全路径)
每一个sql中的id,唯一标识接口中的一个方法
parameterType对应接口方法中的输入参数类型
resultType对应接口方法的返回类型
参数多个怎么做
map,索引,注解@Param
mybatis的缓存机制,一级,二级介绍一下一级缓存
默认开启
SqlSession级别的缓存,实现在同一个会话中数据的共享
一级缓存的生命周期和SqlSession一致
当有多个SqlSession或者分布式环境下,数据库写操作会引起脏数据。二级缓存
默认不开启,需手动开启
SqlSessionFactory级别的缓存,实现不同会话中数据的共享,是一个全局变量
可自定义存储源,如Ehcache
当开启缓存后,数据查询的执行的流程是:二级缓存>一级缓存>数据库
不同于一级缓存,二级缓存可设置是否允许刷新和刷新频率
实现:
实体类实现序列化
在mapper文件中开启
在配置文件中设置cacheEnabled为true
数据库
事务的四个基本特性
① Atomic(原子性):事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成 功,要么全部失败。
② Consistency(一致性):事务完成时,数据必须处于一致状态,数据的完整性约束没有被破坏,事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没 有执行过一样。
③ Isolation(隔离性):事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性 和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。
④Durability(持久性):事务结束后,事务处理的结果必须能够得到固化。
并行事务的四大问题:
1.更新丢失:和别的事务读到相同的东西,各自写,自己的写被覆盖了。(谁写的快谁的更新就丢失了)
2.脏读:读到别的事务未提交的数据。(万一回滚,数据就是脏的无效的了)
3.不可重复读:两次读之间有别的事务修改。
4.幻读:两次读之间有别的事务增删。
事务隔离级别
1、 读未提交。最低的隔离级别(是一种危险的隔离级别,会出现脏读),其含义是允许一个事务读取另外一个事务没有提交的数据。
原理:
1,事务对当前被读取的数据不加锁;
2,事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。
2、 读已提交。 指一个事务只能读取到另外一个事务已经提交的数据。
原理:
1,事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
2,事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
3、 可重复读。 克服读写提交中出现的不可复读的现象,因为在读写提交的时候,可能出现一些值的变化,影响当前事务的执行。
原理:
1,事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加 行级共享锁,直到事务结束才释放;
2,事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
4、 串行化 最高的隔离级别,会要求所有的SQL按照顺序执行,这样就可以克服上述隔离级别出现的问题,所以能够保证数据的一致性。
原理:
1,事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放;
2,事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放。
锁可以分为
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
锁根据模式分为
共享锁(S):发生在数据查找之前,多个事务的共享锁之间可以共存
排他锁(X):发生在数据更新之前,排他锁是一个独占锁,与其他锁都不兼容
更新锁(U):发生在更新语句中,更新锁用来查找数据,当查找的数据不是要更新的数据时转化为S锁,当是要更新的数据时转化为X锁
意向锁:发生在较低粒度级别的资源获取之前,表示对该资源下低粒度的资源添加对应的锁,意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
有两个事务,第一个线程事务需要往数据库中插入5条,另外一个事务插入第6条,当第一个事务没有提交前,第二的线程事务可以提交吗?
如果两个线程都插入第五条数据,第一个没有提交,第二个可以提交吗?
都可以
Mysql的存储引擎
1.InnoDB(MySQL默认存储引擎 从版本5.5.5开始)
支持事务,行级锁,以及外键,拥有高并发处理能力。但是在创建索引和加载数据时,比MyISAM慢。默认的隔离级别是Repeatable Read(可重复读)
2.MyISAM
不支持事务和行级锁。所以速度很快,性能优秀。可以对整张表加锁,支持并发插入,支持全文索引。
3.MEMORY
支持Hash索引,内存表,Memory引擎将数据存储在内存中,表结构不是存储在内存中的,查询时不需要执行磁盘I/O操作,所以要比MyISAM和InnoDB快很多倍,但是数据库断电或是重启后,表中的数据将会丢失,表结构不会丢失。
Mysql的分页查询如何写。
使用limit
数据库调优如何做。最佳调优方式。(SQL调优)
1、缓存,应用系统将常被访问的数据,放在缓存里,减少对数据库的访问频率
2、如果确定知道访问几条数据,则用Limit
3、确定选那几列数据的时候,不要用select *
4、主键最好是int,推荐使用unsigned(>=0的int),并将其设置为自动增加auto_increment。
5、对于固定值的数据,用int(enum),不要用varchar,比如性别。增加应用系统的计算量,但是可以大大减少数据库的负载
6、尽可能的使用not null。除非你有一个很特别的原因要去使用null值。
7、存储引擎:MyIsam,适合大应用的查询,是表锁,所以在更新写的时候比较慢;innodb适合事务,是行锁,不适合高效率的查询。MyISAM适合SELECT密集型的表,而InnoDB适合INSERT和UPDATE密集型的表
8、建立合适的索引
9、架构方面,考虑:主从复制;读写分离;分库分表
数据库分库分表如何操作
一般就是垂直切分和水平切分,这是一种结果集描述的切分方式,是物理空间上的切分。如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。 分库分表的顺序应该是先垂直分,后水平分。
消息中间件有了解
具体做过的项目,项目的背景是什么,具体要解决什么问题,主要负责哪方面。
分布式
分布式事务
保持一致性
CAP原理:
一致性:在分布式系统中的所有数据备份,在同一时刻具有同样的值,所有的节点在同一时刻读取的数据都是最新的数据副本。
可用性:在任何故障模型下,服务都会在有限的时间内处理完成并进行响应
分区容忍性:尽管网络上有部分消息丢失,但系统仍然可以继续工作
任何分布式系统只可以同时满足以上两点,分布式系统都需要满足分区容错性。
BASE(碱)
解决了CAP提出的分布式系统的一致性和不可用性不可兼得的问题。通过牺牲强一致性来获得可用性,通过达到最终一致性来尽量满足业务的需求。
包括三个元素:
BA:基本可用
S:软引用,状态可以在一段时间内不同步
E:最终一致性,在一定的时间窗口内,最终达到数据一致即可。
分布式事务一致性协议(两段提交协议,三段提交协议,阿里的TCC)
1、 两阶段提交协议(分为准备阶段和提交阶段,可以保证强一致性,但会存在阻塞,单点故障,脑裂)
准备阶段:事务管理器向资源管理器发起指令,资源管理器评估自己的状态,如果资源管理器评估指令可以完成,则会写redo日志(写前日志),然后锁定资源,执行操作,但是并不提交。
提交阶段:如果每个资源管理器明确返回准备成功,也就是预留资源和执行操作成功,则事务管理器向资源管理器发起提交指令,资源管理器提交资源变更的事务,释放锁定的资源;如果任何一个资源管理器明确返回准备失败,事务管理器发起中止指令,资源管理器取消已经变更的事务,执行日志,释放锁定的资源。
2、 三段提交协议(通过超时机制解决了阻塞的问题,分为询问阶段,准备阶段,提交阶段)
询问阶段:事务管理器询问资源管理器是否可以完成指令,事务管理器只需要回答是或者不是,而不需要做真正的操作,这个阶段超时会导致中止;
准备阶段和提交阶段和两段提交协议相似。
3、 TCC协议,将一个任务拆分成Try,Confirm,Cancel三个步骤,正常的流程会先执行try,如果执行没有问题,则执行Confirm,如果执行过程中出现了问题,则执行操作的逆操作Cancel,达到最终一致性状态。
缓存一致性(消息队列)
使用缓存来抗住读流量
如果性能要求不是很高,尽量使用分布式缓存,而不要使用本地缓存。
写缓存时数据一定要完整,如果缓存数据的一部分有效,另一部分无效,宁可去查询数据库,也不要把部分数据放入缓存中
使用缓存牺牲了一致性,为了提高性能,数据库与缓存只需要保持弱一致性,而不需要保持强一致性。
读的顺序是先读缓存,后读数据库,写的顺序需要先写数据库,后写缓存。
什么是Restful风格,什么是restful样式的API
它纯粹面向资源,面向服务的思想
RPC:远程服务调用
JDK RMI
一个java进程内的服务调用其他java进程内的服务,使用JDK内置的序列化和反序列化协议,但是RMI采用JDK自带的专用序列化协议,不能跨语言,使用了底层的网络协议,不如基于文本的http可读和广泛认可。
Hessian及Buriap(远程调用协议,基于http传输)
Hessian将对象序列化成二进制协议,Buriap将对象序列化成xml数据,
Hessian及Buriap都适合于传输较小的对象,对较大、复杂的对象,无论是在序列化方式上和传输通道上都没有RMI有优势。
由于服务化框架中大量的服务调用都是大规模的、高并发的短小请求,因此Hessian和Buriap协议在服务化架构中得到广泛应用。
Spring Http Invoker
重用了jdk内置的对象序列化技术传输对象,与RMI原理一致,他通过http通道传输数据,在效率上稍微低于RMI,并且使用了JDK内置的序列化机制,不能跨语言。
SOA(服务化)
Dubbo(分布式服务框架)
提供了高性能和透明化的RPC远程服务调用,基本的服务监控、服务治理和服务调度等功能,支持多种序列化协议和通信编码协议,默认使用Dubbo协议传输Hessian序列化的数据(二进制),Dubbo使用ZooKeeper作为注册中心来注册和发现服务,并通过客户端负载均衡来路由请求,负载均衡包括:随机、轮询、最少活跃调用数,一致性哈希。
HSF(High Speed Framework)
淘宝使用的高性能服务框架(分布式服务框架),以高性能的网络通信框架为基础(未开源)
Thrift
Facebook实现的一种高性能且支持多种语言的远程服务调用框架,传输数据时采用二进制序列化格式,相对于JDK本身的序列化、xml和json等需要的内存更小。
Mule ESB
企业服务总线产品,可以把多个复杂的异构系统通过总线模式集成在一起,并且让他们可以互相通信。
微服务
Spring Boot
可以创建独立、自启动的应用程序
不需要构建war包并发布到容器中
通过Maven定制化标签,快速创建Spring Boot应用程序
没有xml配置,不需要代码生成
Netfilt
合并到Spring Cloud项目中,主要提供服务发现、断路器和监控、智能路由、客户端负载均衡、易用的REST客户端等服务化必须的功能。
Hystrix框架提供了微服务架构所需的容错机制的解决方案和设计模式,大大简化了微服务下容错机制的实现,包括服务分组和隔离、熔断和防止级联失败、限流机制、失效转移机制和监控机制等。
Spring Cloud
集成了spring boot对微服务敏捷启动和发布的功能,以及Netflix提供微服务化管理和治理能力。
微服务特点:
将传统单体应用拆分成网络服务,来实现模块化组件
根据微服务架构的服务划分来分组职能团队,减少跨团队的沟通
每个服务对应一个团队,团队成员负责各自的任务。
去中心化,去SOA服务化的中心服务治理和去企业化服务的总线
微服务重视服务的合理拆分、分层和构造,可建设自动化持续发布平台,并进行敏捷开发和部署。
具备兼容性设计、容错性设计和服务的契约设计
微服务中,每一个组负责一项特定的功能,要做到数据统一就得做数据库的拆分(分库分表),把编译好的代码放到一个环境中形成一个镜像,然后把镜像放到服务端的docker(一个应用容器的引擎,采用沙箱模式,相互间不会有任何接口)运行环境中。
消息中间件
主要解决应用耦合,异步消息,流量削锋等问题
实现高性能,高可用,可伸缩和最终一致性架构
使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ
二、消息队列应用场景
以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景
关注于数据的发送和接收,利用高效可靠的异步消息传递机制集成分布式系统。
优点
① 解耦 ② 异步 ③ 横向扩展 ④ 安全可靠 ⑤ 顺序保证(比如kafka)
消息中间件适用于需要可靠的数据传送的分布式环境。采用消息中间件机制的系统中,不同的对象之间通过传递消息来激活对方的事件,完成相应的操作。发送者将消息发送给消息服务器,消息服务器将消息存放在若干队列中,在合适的时候再将消息转发给接收者。消息中间件能在不同平台之间通信,它常被用来屏蔽掉各种平台及协议之间的特性,实现应用程序之间的协同,其优点在于能够在客户和服务器之间提供同步和异步的连接,并且在任何时刻都可以将消息进行传送或者存储转发,这也是它比远程过程调用更进一步的原因。
消息队列应用的场景
消息队列技术是分布式应用间交换信息的一种技术。消息队列可驻留在内存或磁盘上,队列存储消息直到它们被应用程序读走。通过消息队列,应用程序可独立地执行–它们不需要知道彼此的位置、或在继续执行前不需要等待接收程序接收此消息
- 业务解耦:消息队列要解决的最本质问题,实现设计的单一性原则,不耦合其他模块的业务。
- 最终一致性:用来处理延迟不那么敏感的“分布式事务”场景或者不重要的业务。
- 广播:下游有很多系统关心你的系统发出的通知的时候。
- 错峰和流控:上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。
项目难点
- 本文作者: 生活,生活?
- 本文链接: ayjcsgm.github.io/2019/10/18/面试总结/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!