①,一面的面试题总结
1,HashMap实现原理,ConcurrentHashMap实现原理。
<1>HashMap,底层hash表,在jdk 1.7以前是数组与链表,jdk1.8以后是链表长度达到8时会演变成红黑树(维持数据的插入和查找的效率平衡)。 <2>ConcurrentHashMap,是hashMap的演变,在jdk 1.7以前是segement分段加锁,为了减少锁竞争。 每次的数据查找经历两次hash,第一次hash先找到segement,第二次hash是查找segement段内的链表位置,在写入数据时对segement加锁. 每个版本查找数据的特殊地方:1.6以前是找到节点数据才加锁(外层调用不加锁),1.7以后都是使用Unsafe.getObjectVolatile。 ConcurrentHashMap 1.8以后是数组和链表,不再使用segement,直接对每个链表进行加锁,更进一步降低锁竞争,put操作是使用了synchronize对当前链表加锁,get是使用Unsafe.getObjectVolatile获取最新的共享内存值(利用cas不加锁),为了保证获取的数据是最新的(可见性)
Segement的modCount变更条件:调用put或者remove操作,并且导致元素新增或者被删除,才能引起变化,如果仅仅覆盖或者删除不成功,不会导致变化。 Size()方法:其实就是在每个segement modcount相同情况下(否则马上再次发起重试),再校验count的总数(查两次),如果不同,重试两次,如果还是不同,就对每个segement加锁。
补充:LinkedList、ArrayList,LinkedHashMap <1>LinkedList:底层是基于双向链表(非环状)实现,为什么使用双向链表?为了提高数据的查找效率(包括指定index位置插入),因此内部会根据index和size做比较,决定从头部向后或者尾部向前开始遍历,如当我们调用get(int location)时,首先会比较“location”和“双向链表长度的1/2”;若前者大,则从链表头开始往后查找,直到location位置;否则,从链表末尾开始先前查找,直到location位置。
特点:LinkedList查找效率低,增删效率更高,可以利用零碎的内存空间。
<2>ArrayList:是基于动态数组实现的,如果空间不够的时候,增加新元素时要动态扩容数组(期间还需要拷贝数据到新数组),删除元素时也需要对后面的数据进行向前移动整合(因为后面还需要使用index搜索数据)。 特点:ArrayList的查找效率高,而增删操作的效率低(只能使用连续的内存空间),使用建议:空间可以申请大一些,尽量不要删除数据。
ArrayList中的modCount,继承于AbstractList,这个成员变量记录着集合的修改次数,也就每次add或者remove它的值都会加1,在使用迭代器遍历集合的时候同时修改集合元素。因为ArrayList被设计成非同步的,因此会抛出异常,实现原理:获取迭代器的时候,会记录一个expectedModCount(不会被改变),在每次迭代过程中会校验expectedModCount和modCount是否相等,否则会抛出异常ConcurrentModificationException
<3>LinkedHashMap:继承了HashMap,因此底层还是哈希表结构(数组+链表),但是另外多维护了一个双向环状链表(只有一个头结点),提供了插入有序和访问有序(lru)两种模式,默认为插入有序(打印的结果就是之前的插入顺序),LinkedHashMap重写了的内部addEntry(put调用)方法,重写了Entry,包含before, after,为了将来能构建一个双向的链表,当向map里面添加数据时,在createEntry时,把新创建的Entry加入到双向链表中,之所以使用双向环状链表就是为了实现访问有序,每次访问,都把节点调整到头结点前面(实际上变成了尾节点)。 简单总结:当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中,所以可以看出LinkedHashMap就是HashMap+双向环状链表
参考:www.jianshu.com/p/8f4f58b4b…
补充:为什么Hashtable ConcurrentHashmap不支持key或者value为null ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断(除非全程在插入、查询时加锁判断,此时性能就下降了),它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。 备注:实际存储null,这种场景已经不大,如果真的要存储,在查询和插入都需要牺牲了在线程安全条件的并发性能。 参考:blog.csdn.net/gagewang1/a…
2,红黑树,为什么允许局部不平衡 完全平衡二叉树的左右两个子树的高度差的绝对值不超过1,因此每次的节点变更都需要保证树的严格平衡。 红黑树只需要保证从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点,因此红黑树是一颗接近完全平衡的二叉树,减少了旋转次数,提高数据的写入效率。
红黑树特点: (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
补充:插入节点默认是红色(第五点限制),红黑树具体的旋转:www.cnblogs.com/skywang1234…
平衡二叉搜索树:又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
普通的二叉查找树:容易失去”平衡“,极端情况下,二叉查找树会退化成线性的链表,导致插入和查找的复杂度下降到 O(n) ,所以,这也是平衡二叉树设计的初衷。那么平衡二叉树如何保持”平衡“呢?根据定义,有两个重点,一是左右两子树的高度差的绝对值不能超过1,二是左右两子树也是一颗平衡二叉树
树的补充:blog.csdn.net/qq_25940921…
3,tcp三次握手,四次挥手,为什么不是2次握手?
<1>三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包 TCP是一个双向通信协议,客户端向服务端建立连接需要得到服务端ack响应,意味着客户端向服务端通信正常,此时客户端再给服务端回复一个ack,意味着服务端向客户端通信正常,这样才能正确建立起来双工通信通道,因此两次握手不能保证服务端向客户端通信是否正常。
<2>TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。 客户端或服务器可以单独关闭向另外一端的通信通道,即为半关闭状态,因此才进行四次挥手。 为什么需要半关闭?比如只需要向一方传输资源,传输完毕以EOF标记即可。
4,tcp和udp的区别,为什么是可靠和不可靠的? <1>TCP协议是有连接的,有连接的意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。而UDP是无连接的 <2>TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性,但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列,也不保证按序送到。 <3>TCP有流量控制和拥塞控制,UDP没有,网络拥堵不会影响发送端的发送速率 <4>TCP是一对一的连接,而UDP则可以支持一对一,多对多,一对多的通信。 <5>TCP面向的是字节流的服务,UDP面向的是报文的服务
TCP的可靠性含义: 接收方收到的数据是完整, 有序, 无差错的。 UDP不可靠性含义: 接收方接收到的数据可能存在部分丢失, 顺序也不一定能保证。
备注:GET和POST还有一个重大区别,简单的说:GET产生一个TCP数据包;POST产生两个TCP数据包。
长的说: 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
post请求的过程: (1)浏览器请求tcp连接(第一次握手) (2)服务器答应进行tcp连接(第二次握手) (3)浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送) (4)服务器返回100 Continue响应 (5)浏览器发送数据 (6)服务器返回200 OK响应 get请求的过程: (1)浏览器请求tcp连接(第一次握手) (2)服务器答应进行tcp连接(第二次握手) (3)浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送) (4)服务器返回200 OK响应 也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。
参考:blog.csdn.net/zzk220106/a…
5,TCP滑动窗口和socket缓冲区之间的关系 TCP的滑动窗口大小实际上就是socket的接收缓冲区大小的字节数,“窗口”对应的是一段可以被发送者发送的字节序列,其连续的范围称之为“窗口”;每次成功发送数据之后,发送窗口就会在发送缓冲区中按顺序移动,将新的数据包含到窗口中准备发送。(数据都往窗口写入,因此窗口大小控制数据传输速率,起到一个缓冲的作用)
6,tcp拥塞控制 网络中的链路容量和交换结点中的缓存和处理机都有着工作的极限,当网络的需求超过它们的工作极限时,就出现了拥塞。拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。常用的方法就是:
- 慢开始、拥塞控制
- 快重传、快恢复
补充说明:慢开始是指发送方先设置cwnd=1,一次发送一个报文段,随后每经过一个传输轮次,拥塞串口cwnd就加倍,其实增长并不慢,以指数形式增长。还要设定一个慢开始门限,当cwnd>门限值,改用拥塞避免算法。拥塞避免算法使cwnd按线性规律缓慢增长。当网络发生延时,门限值减半,拥塞窗口执行慢开始算法。(先指数级别增加,再线性增加,延迟再衰减)
快重传的机制是: 接收方建立这样的机制,如果一个包丢失,则对后续的包继续发送针对该包的重传请求,一旦发送方接收到三个一样的确认,就知道该包之后出现了错误,立刻重传该包;
此时发送方开始执行“快恢复”算法: 慢开始门限减半; cwnd设为慢开始门限减半后的数值; 执行拥塞避免算法(高起点,线性增长) 参考:www.cnblogs.com/woaiyy/p/35…
7,mysql事务是什么?四大特性,四大隔离级别 事务:事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行,是最小的不可再分的工作单元。 四大特性,原子性,隔离性,持久性,一致性。 四大隔离级别: 事务隔离级别 脏读 不可重复读 幻读 读未提交(read-uncommitted) 是 是 是 读已提交(read-committed) 否 是 是 可重复读(repeatable-read) 否 否 是 串行化(serializable) 否 否 否 serializable级别是最高的 mysql默认的事务隔离级别为repeatable-read
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。 ② Repeatable read (可重复读):可避免脏读、不可重复读的发生。 ③ Read committed (读已提交):可避免脏读的发生。 ④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。
8,spring ioc和aop,各自有什么优点?
ioc,依赖注入,在以前的软件工程编码过程中,类的属性需要硬编码生成对象数据,耦合性较高,如果使用ioc,是在容器启动过程中,在bean对象实例化过程中需要检查其依赖数据,并且进行数据注入(setter,构造器注入),完成一个对象的实例化并实现了解耦合,并且能够对这些对象进行复用。
aop,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。它利用一种称为"横切"的技术,并将那些影响了多个类的公共行为封装到一个可重用模块,简单理解是抽象出与业务逻辑无关的公共行为逻辑。
9,java有哪几种线程池 //线程池大小固定为1 Executors.newSingleThreadExecutor();
//固定大小线程池由自己设定,即自己控制资源的固定分配
Executors.newFixedThreadPool(10);
//动态调整线程池的大小,最小为0,最大为int最大值,,newCachedThreadPool会大幅度提高大量短暂异步任务的性能,
//如果执行业务逻辑比较慢,会导致持续创建线程,导致cpu资源消耗殆尽
//为什么使用SynchronousQueue?最多只能持有一个任务数据,当任务数据插入队列失败,会驱动创建新线程
Executors.newCachedThreadPool();
//基于延迟队列实现的延时任务线程池,周期性的执行所提交的任务
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run");
}
}, 1000,2000, TimeUnit.MILLISECONDS);
复制代码
10,什么情况下使用Thread和Runnable创建线程,Runnable和Callable区别 两个都可以实现多线程编程,但是基于java是单继承和多实现,所以实现Runable更灵活,并且Runable可以简单的实现变量的线程间共享。 Runnable和Callable区别: 实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果; Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
11,线程方法中的异常如果处理?父线程可以捕获到吗? 在run方法体里面必须主动捕获check exception,如果是unchecked exception(RunntimeException)可以给线程注册一个UncaughtExceptionHandler,发生异常时执行回调。 通过Callable创建的线程,可以在futureTask.get()获取结果时捕获异常。
12,synchronized和lock区别,什么情况下使用synchronized和Reentrantlock ? 1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类; 2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁; 3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁; 4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了; 5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可判断、可公平(两者皆可) 6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
备注:synchronized适合低并发的场景,锁竞争发生的概率很小,此时锁处于偏向锁或者轻量级锁状态,因此性能更加好,比如jdk1.8concurrentHashMap为什么使用synchronized的原因就是,每个hash槽上的锁竞争很少,用synchronized比lock更好
13,jvm的对象分配在哪个区,Class对象分配在哪个区? 线程共享的变量分配在堆(实例)和方法区(静态变量等)里面,非线程共享的变量在栈区 Class对象分配在堆里面,Class的元数据是在方法区里面
14,一次http请求的全过程,包括域名解析、主机定位等。 <1>域名解析,分为如下几个步骤(找不到逐步递归) A,查找浏览器的dns缓存(域名与ip的映射表) B,查找自身操作系统自身的DNS缓存, C,查找自身的host文件 D,请求打到运营商的dns服务器,访问运营商的dns缓存。 E,访问全球顶级域名解析器,获得将要寻址域名的权限域名服务器地址(用来保存该区中的所有主机域名到IP地址的映射,如baidu.com的域名ip管理机器) F,运营商dns访问权限域名服务器获取目标域名的ip。
<2>tcp三次握手 <3>建立TCP连接后发起http请求(get,post由发起者决定) HTTP请求报文由三部分组成:请求行,请求头和请求正文 请求行:用于描述客户端的请求方式,请求的资源名称以及使用的HTTP协议的版本号(例:GET/books/java.html HTTP/1.1) 请求头:用于描述客户端请求哪台主机,以及客户端的一些环境信息等 注:这里提一个请求头 Connection,Connection设置为 keep-alive用于说明 客户端这边设置的是,本次HTTP请求之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP建立连接的时间 请求正文:当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中(GET方式是保存在url地址后面,不会放到这里) <4>服务器端响应http请求,浏览器得到html代码 HTTP响应也由三部分组成:状态码,响应头和实体内容 状态码:状态码用于表示服务器对请求的处理结果 列举几种常见的:200(没有问题) 302(要你去找别人) 304(要你去拿缓存) 307(要你去拿缓存) 403(有这个资源,但是没有访问权限) 404(服务器没有这个资源) 500(服务器这边有问题) 若干响应头:响应头用于描述服务器的基本信息,以及客户端如何处理数据 实体内容:服务器返回给客户端的数据 注:html资源文件应该不是通过 HTTP响应直接返回去的,应该是通过nginx通过io操作去拿到的吧
<5>浏览器解析html代码,并请求html代码中的资源 <6>断开TCP连接(四次挥手) <7>页面渲染给用户
总结:域名解析 --> 发起TCP的3次握手 --> 建立TCP连接-->发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等)-->tcp四次挥手 --> 浏览器对页面进行渲染呈现给用户
②,二面的面试题总结 1,常用设计模式的介绍,单例模式,装饰模式,及使用场景
单例模式: public class SingletonTest { private static volatile SingletonTest singletonTest;
public SingletonTest getInstance() {
if (singletonTest == null) {
synchronized (SingletonTest.class) {
if (singletonTest == null) {
singletonTest = new SingletonTest();
}
}
}
return singletonTest;
}
复制代码
} 使用场景:全局只需要初始化一个实例对象,节省内存开销(比如spring bean的单例配置)
装饰模式 public class DecoratorTest { public interface Greeting { void sayHello(); }
public static class GreetingImpl implements Greeting {
@Override
public void sayHello() {
System.out.println("GreetingImpl run");
}
}
public static abstract class GreetingDecorator implements Greeting {
private Greeting greeting;
public GreetingDecorator(Greeting greeting) {
this.greeting = greeting;
}
@Override
public void sayHello() {
greeting.sayHello();
}
}
public static class GreetingBefore extends GreetingDecorator {
public GreetingBefore(Greeting greeting) {
super(greeting);
}
@Override
public void sayHello() {
before();
super.sayHello();
}
private void before() {
System.out.println("Before");
}
}
public static void main(String[] args) throws FileNotFoundException {
Greeting greeting = new GreetingBefore(new GreetingImpl());
greeting.sayHello();
}
复制代码
}
代理模式重在于对方法的控制,添加行为对于用户是被动的;装饰模式重在于装饰方法,增加方法的功能(功能增强),添加装饰对于用户是主动的(代理模式和装饰模式看起来很相像)
输入输出流使用装饰模式的地方:BufferedInputStream就是一个装饰者,它能为一个原本没有缓冲功能的InputStream添加上缓冲的功能。
2,Java会出现内存溢出吗?什么情况下会出现? 内存溢出(Out Of Memory,OOM),就是内存不够用了,内存泄漏(Memory Leak),指的是申请的内存空间,自己没有去主动释放,gc也无法释放(如强引用),多次内存泄漏,就会导致内存溢。
3,双亲委派模型,为什么这样做? 当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作Abc.class,首先加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。Bootstrap类加载器会先看看rt.jar中有没有这个类,因为并没有这个类,所以这个请求由回到Extension类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被Extension类加载器找到了,那么它将被加载,而Application类加载器不会加载这个类;而如果这个类没有被Extension类加载器找到,那么再由Application类加载器从classpath中寻找。
双亲委托可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次(单一性)。
4,对象什么情况下进入老年代? <1>申请大对象,young区无法存放这么大的对象,此时需要在老年代申请空间。 <2>young区中的eden区对象经过多次Minor GC晋升到Survivor区,如果还存活,就会移动到老年代。
补充: <1>full gc发生条件:System.gc()调用,老年代空间不足,perm(方法区,即永生代)空间不足。 <2>young区为什么要有Survivor区?只要进行一次MinorGC,存活的对象就进入了old区,导致old区很快写满,就会频繁出发full gc,进而引发99线波动很大。 <3>young区为什么要有2个Survivor区?如果使用单个Survivor,eden区和Survivor经过Minor GC之后存活的对象存放在单个Survivor,可能出现碎片化,使用两个就不会(每次gc后s0和s1之间交换数据,保证有一个无碎片,一个为空) <4>young和old区比例一般是1:2,eden和s0,s1是8:1:1 参考:blog.csdn.net/towads/arti…
5,快速排序说一下(todo)
6,Spring AOP原理说下 AOP是切面编程,所谓切面编程是把与业务无关的公共逻辑抽象成一个公共方法执行。AOP的实现是通过动态代理,动态代理有两种实现方式,jdk自带的动态代理(通过类加载器,运行时生成一个新的代理类,代理类通过反射机制获取被代理类的方法,完成被代理方法的执行),cglib在类加载时通过探针技术(javaAgent和ASM,均是字节码修改工具)修改被代理类的字节码,生成新的class,完成被代理类的方法调用。 两者区别:Jdk动态代理针对接口,cglib针对类。 补充:Jdk动态代理针对接口为什么只能代理接口?考虑到被代理对象的通用性,由于一个接口可能有多个实现类,不需要针对每个实现类写一个代理类,这样提高了通用性。
7,BIO、NIO(如何实现的)、AIO 先简介一下同步、异步、阻塞、非阻塞的概念 同步:调用者(用户线程,如selector线程)调用一个服务,必须自己主动等待结果,中间可以做别的事情,但是得要轮询结果。 异步:调用者(用户线程)调用一个服务,不需要等待业务逻辑执行完,就可以去别的事情,之前业务逻辑处理完,会通知调用者。 关注点:执行结果获取结果方式(主动、被动(回调))
阻塞:调用结果没有返回之前,会持续等待结果(调用者线程被阻塞,cpu没有交出去) 非阻塞:调用结果没有返回之前,调用者线程不会被阻塞。 关注点:用户线程是否被阻塞
BIO:同步阻塞io NIO:同步非阻塞io,需要多路复用器Selector进行调度,执行结果需要多路复用器询问。 AIO:异步非阻塞io,执行结果由os进行回调通知
使用场景: BIO:使用连接数较少,服务器压力很低的架构。 NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。 AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
AIO在性能上相对于NIO没有本质的提升。 AIO只是帮助你从内核中将数据复制到用户空间中,并调用你传入的回调方法(内核完成回调)。 NIO是需要程序自己从内核中将数据复制到用户空间中,并需要程序自己调用相应的处理逻辑。
补充:在微服务架构里面,服务响应都比较快,aio并不比nio响应快多少,因此netty实现上也就不使用aio。
8,消息中间件有哪些?它们之间的优劣势? ActiveMQ:单机吞吐量在万级,比RocketMQ和Kafka低一个数量级,传统中小企业用的多一些,没有大规模吞吐场景验证,没有活跃社区,有较低的概率丢失数据(不是百分百可靠),支持事务。 RabbitMQ:单机吞吐量在万级,比RocketMQ和Kafka低一个数量级,基于erlang开发,不利于java开发者深入研究,其可靠性非常高,有活跃社区,不支持事务。 RocketMQ:吞吐量10万级(高吞吐,低延迟),单机支持topic可达到好几百,基于java开发(客户端只支持java),有活跃社区,支持事务。(高吞吐量实现上参考了很多kafka的原理,比如内存映射,多个broker) Kafka:吞吐量10万级(高吞吐,低延迟),客户端支持多种语言,单机topic过多时,会导致性能下降,此时需要增加机器,有活跃社区,不支持事务。
参考:www.jianshu.com/p/eaafb1581…
9,redis的持久化方式 RDB:在指定的时间间隔能对数据进行快照存储。 优点:使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能 缺点:RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失,数据的准确性不高。 AOF:AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。 优点:可以保持更高的数据完整性,因此已成为主流的持久化方案 缺点:AOF文件比RDB文件大,且恢复速度慢。
补充:redis如何压缩AOF文件,具体过程如下: redis调用fork ,现在有父子两个进程 <1>,子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令 <2>,父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。 <3>,当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。 <4>,现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。 <5>,需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
简单总结:如何缩小AOF文件大小:文件重写是指定期重写AOF文件(产生新的AOF文件),减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件(为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件)
10,栈和队列(todo)
参考:www.cnblogs.com/smyhvae/p/4…
11,jvm垃圾回收算法 <1>标记-清除算法 这种垃圾回收一次回收分为两个阶段:标记、清除。首先标记所有需要回收的对象,在标记完成后回收所有被标记的对象。这种回收算法会产生大量不连续的内存碎片,当要频繁分配一个大对象时,jvm在新生代中找不到足够大的连续的内存块,会导致jvm频繁进行内存回收(目前有机制,对大对象,直接分配到老年代中) 场景:如果没有Survivor,eden区gc后会产生很多碎片。 <2>复制算法 这种算法会将内存划分为两个相等的块,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。这样做的效率比较高,也避免了内存碎片。但是这样内存的可使用空间减半,是个不小的损失。 场景:在发生young gc的时候,Survivor0和Survivor1之间的数据相互复制迁移。
<3>标记-整理算法(标记压缩法) 这是标记-清除算法和复制算法的综合版,在完成标记阶段后,不是直接对可回收对象进行清理,而是让存活对象向着一端移动,然后清理掉边界以外的内存,避免产生内存碎片。
<4>分代收集算法 这是对上面三种算法的综合应用,并且采取分代处理,当前商业虚拟机都采用这种算法。首先根据对象存活周期的不同将内存分为几块即新生代、老年代,然后根据不同年代的特点,采用不同的收集算法。
补充:
1,Java GC如何判断对象是否为垃圾(www.cnblogs.com/hzzjj/p/626… <1>引用计数法,引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。 <2>根搜索算法,主流的虚拟机都是采用GC Roots Tracing算法,比如Sun的Hotspot虚拟机便是采用该算法。 该算法的核心算法是从GC Roots对象作为起始点,利用数学中图论知识,图中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。 那么可以作为GC Roots的对象: A,虚拟机栈的栈帧的局部变量表所引用的对象; B,本地方法栈的JNI所引用的对象; C,方法区的静态变量和常量所引用的对象;
垃圾回收器
Cms垃圾回收器:CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,老年代的回收采用CMS,使用算法:标记清除法(参考:blog.csdn.net/hutongling/…
工作过程: <1>初始标记:在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word),所以这个过程虽然暂停了整个JVM,但是很快就完成了。初始标记也就是标记一下GC roots 关联到的对象。(并不是所有活动对象)。 <2>并发标记:这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。并发标记就需要标记出GC roots关联到的对象的引用对象有哪些。比如说 A -> B (A 引用 B,假设 A是GC Roots关联到的对象),那么这个阶段就是标记出B对象,A对象会在初始标记中标记出来。 <3>重新标记(也会stw),之前在并发标记时,因为是GC和用户程序是并发执行的,可能导致一部分已经标记为 从GC Roots不可达 的对象,因为用户程序的(并发)运行,又可达了(对象被复活了),Remark的作用就是将这部分对象又标记为 可达对象。 <4>并发清理:清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。 初始标记和重新标记都要stop the world
重新标记(Remark) 的作用在于: 之前在并发标记时,因为是 GC 和用户程序是并发执行的,可能导致一部分已经标记为 从 GC Roots 不可达 的对象,因为用户程序的(并发)运行,又可达 了,Remark 的作用就是将这部分对象又标记为 可达对象。 至于 “浮动垃圾”,因为 CMS 在 并发标记 时是并发的,GC 线程和用户线程并发执行,这个过程当然可能会因为线程的交替执行而导致新产生的垃圾(即浮动垃圾)没有被标记到;而 重新标记 的作用只是修改之前 并发标记 所获得的不可达对象,所以是没有办法处理 “浮动垃圾” 的。
Cms的缺点: <1>CMS不会整理和压缩堆空间,导致产生问题:经过CMS收集的堆会产生空间碎片(不压缩空间是为了响应更快),典型的空间换时间(实际可以配置经过多少次old gc之后整理old区,减少内存碎片) <2>需要更多的CPU资源,不直接使用多线程,直接利用多核cpu。 <3>CMS的另一个缺点是它需要更大的堆空间,为了保证在CMS回收完堆之前还有空间分配给正在运行的应用程序,CMS不会在老年代满的时候才开始收集,在空间到了68%就开始回收。 <4>cms会产生浮动垃圾,由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
G1垃圾回收器,回收算法是:标记整理,减少内存碎片化
G1是在JDK 7u4版本之后发布的垃圾收集器,并在jdk9中成为默认垃圾收集器,G1也是利用多CPU来缩短stop the world时间(弱化了分代的概念,g1能同时作用于新生代和老年代),并且是高效的并发垃圾收集器。但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年轻代的ygc,全堆扫描的fullgc外,还有包含所有年轻代以及部分老年代Region的MixedGC。G1的优势还有可以通过调整参数,指定垃圾收集的最大允许pause time。下面会详细阐述下G1分区以及分代的概念,以及G1 GC的几种收集过程的分类。
G1的优点: 0. 弱化young区和old区概念,把堆区分为若干小的区域region,这些region 可分为eden,surival,old,humergus,默认分为2048个区,每块1~32M 1,同时对多个regoin进行并发标记,每次GC不需要清理全部的堆区域,只需要清理垃圾最多的分区,这样可以减少stw的时间,提升了jvm的响应性能。 2,不同区域region不需要连续。活跃数据在不同region复制和移动。 3,单个大对象可以存放多个humergus之间,可以有效利用空间。 4,G1根据回收时间的可预计性(时间可配置),一次回收一定数量的region,减少stw时间。
参考:www.cnblogs.com/niejunlei/p…
备注:G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩)。
G1的工作流程: 1:初始标记,STW。基于yong GC,标记survivor中可能引用老年代对象的对象,作为Root Region,并扫描 2:并发标记:贯穿整个堆内存,标记活跃对象,并立即清除,同时收集活跃对象统计信息。 3:重新标记:使用snapshot-at-the-beginning(SATB),移除,回收标记的空region。STW 4:清理/复制,G1选择最不活跃的region,以便最快收集。这些区域可以和yong GC同时收集,STW 清理:统计活跃对象,活跃区域(STW)=》清理RSet(STW)=》重置空的region=》归还到free list(并发)。 复制:移动活跃对象到未应用的区域(STW)
发生full gc条件:当收集垃圾,从一个区域复制数据到另一个区域时,找不到可用区域。
12,MySql索引
<1>InnoDB(b+树,聚簇索引):支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)(用于事务处理应用程序,具有众多特性,包括ACID事务支持。(提供行级锁))
<2>MyISAM(b+树,非聚簇索引):插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用(MyISAM类型不支持事务处理等高级处理,因此也不支持数据的回滚修复)。
<3>MEMORY(hash结构):所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。
为什么使用b+树? 在范围查询方面,B+树的优势更加明显,B树的范围查找需要不断进行遍历查找每个数据(查一次,进行一次io)。首先二分查找到范围下限,在不断通过遍历,知道查找到范围的上限即可。整个过程比较耗时,而B+树的范围查找则简单了许多。首先通过二分查找,找到范围下限,然后同过叶子结点的链表顺序遍历,直至找到上限即可,整个过程简单许多,效率也比较高。(b+树的数据都分布在子节点上)。B+树的空间相对比较大,典型的空间换时间。
13,tomcat类加载器 在tomcat7版本下,Tomcat自己定义了两个核心类加载器,JasperLoader负责加载jsp文件经过编译生成的jsp类,该类加载器打破了双亲委托机制(为什么打破双亲委托机制?实现jsp文件的热部署,如果不打破双亲委托,之前的jsp部署一次,就会被持续运行,由于父类加载器一直存在,无法再次热部署了)。webappclassLoader只负责加载web项目下的lib类(分析源码并没有打破双亲委托机制)
14,内存泄漏,什么情况下会发生,如何排查? 指的是申请的内存空间,自己没有去主动释放,gc也无法释放(如强引用),多次内存泄漏,就会导致内存溢出,在生成环境中对某些大对象的不合理使用(没有用的对象没被释放),导致gc回收不了。 如何排查:用jmap生成堆内存信息文件(常说的内存dump), MAT(Memory Analyzer Tool),Eclipse提供的一款用于Heap Dump文件的工具,MAT分析堆信息文件,包括整个堆内存的大小、类(Class)的数量、对象(Object)的数量、类加载器(Class Loader)的数量、以及所占的空间大小。 补充:jstack查看线程堆栈信息,排查死锁用的比较多。
16,spi,service provice interface,其实是一套协议,使用者不用关注其实现,各个功能实现者针对标准进行实现就可以了,比如jdbc的数据库驱动,各个厂商有自己的具体实现