一、HashMap的那些事
1.1、HashMap的实现原理
1.1.1、结构
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。如下图所示:
当新建一个HashMap的时候,就会初始化一个数组。哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。这些元素一般情况是通过hash(key)%len的规则存储到数组中,也就是元素的key的哈希值对数组长度取模得到。
1.1.2、核心变量
1.1.3、put存储逻辑
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标), 如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
这里有一个特殊的地方。在JDK1.6中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
红黑树
红黑树和平衡二叉树(AVL树)类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
红黑树和AVL树的区别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡。
1.1.4、get读取逻辑
从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
如果第一个节点是TreeNode,说明采用的是数组+红黑树结构处理冲突,遍历红黑树,得到节点值。
1.1.5、归纳
简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Node 对象。HashMap 底层采用一个 Node<K,V>[] 数组来保存所有的 key-value 对,当需要存储一个 Node 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。
1.1.6、HashMap的resize(rehash)
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,在对HashMap数组进行扩容时,就会出现性能问题:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
1.2、HashMap在并发场景下的问题和解决方案
1.2.1、多线程put后可能导致get死循环
问题原因就是HashMap是非线程安全的,多个线程put的时候造成了某个key值Entry key List的死循环,问题就这么产生了。
当另外一个线程get 这个Entry List 死循环的key的时候,这个get也会一直执行。最后结果是越来越多的线程死循环,最后导致服务器dang掉。我们一般认为HashMap重复插入某个值的时候,会覆盖之前的值,这个没错。但是对于多线程访问的时候,由于其内部实现机制(在多线程环境且未作同步的情况下,对同一个HashMap做put操作可能导致两个或以上线程同时做rehash动作,就可能导致循环键表出现,一旦出现线程将无法终止,持续占用CPU,导致CPU使用率居高不下),就可能出现安全问题了。
为HashMap以链表组形式存在,初始数组16位(为何16位,又是一堆移位算法,下一篇文章再写吧),如果长度超过75%,长度增加一倍,多线程操作的时候,恰巧两个线程插入的时候都需要扩容,形成了两个链表,这时候读取,size不一样,报错了。其实这时候报错都是好事,至少不会陷入死循环让cpu死了,有种情况,假如两个线程在读,还有个线程在写,恰巧扩容了,这时候你死都不知道咋死的,直接就是死循环,假如你是双核cpu,cpu占用率就是50%,两个线程恰巧都进入死循环了,得!中奖了。
1.2.2、多线程put的时候可能导致元素丢失
主要问题出在addEntry方法的new Entry (hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失。
1.2.3、解决方案
ConcurrentHashMap替换HashMap
Collections.synchronizedMap将HashMap包装起来
1.3、ConcurrentHashMap PK HashTable
ConcurrentHashMap具体是怎么实现线程安全的呢,肯定不可能是每个方法加synchronized,那样就变成了HashTable。
从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。
以空间换时间的结构,跟分布式缓存结构有点像,创建的时候,内存直接分为了16个segment,每个segment实际上还是存储的哈希表(Segment其实就是一个HashMap ),写入的时候,先找到对应的segment,然后锁这个segment,写完,解锁,锁segment的时候,其他segment还可以继续工作。
ConcurrentHashMap如此的设计,优势主要在于: 每个segment的读写是高度自治的,segment之间互不影响。这称之为“锁分段技术”;
二、线程,多线程,线程池的那些事
2.1、线程的各个状态及切换
Java中的线程的生命周期大体可分为5种状态:新建、可运行、运行、阻塞、死亡。
1、新建(NEW):新创建了一个线程对象。
2、可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3、运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4、阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
阻塞的情况分三种:
1)等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
2)同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
3)其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5、死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
几个方法的比较:
1)Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。
2)Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
3)t.join()/t.join(long millis),当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。
4)obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
5)obj.notify(),唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
2.2、多线程的实现方式Thread、Runnable、Callable
继承Thread类,实现Runnable接口,实现Callable接口。
这三种方法的介绍和比较:
1、实现Runnable接口相比继承Thread类有如下优势:
1)可以避免由于Java的单继承特性而带来的局限
2)增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
3)适合多个相同程序代码的线程去处理同一资源的情况
2、实现Runnable接口和实现Callable接口的区别
1)Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
2)实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回结果
3)Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
4)加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法
注:Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞
2.3、线程池原理和运行机制
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。
在ThreadPoolExecutor类中提供了四个构造方法,主要参数包括下面的参数:
1、int corePoolSize:核心池的大小。
线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。这里需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是corePoolSize。
2、int maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程,注意与corePoolSize区分。
线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。
3、long keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。
4、TimeUnit unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性。
5、BlockingQueue<Runnable> workQueue:一个阻塞队列,用来存储等待执行的任务。
6、ThreadFactory threadFactory:线程工厂,主要用来创建线程。
7、RejectedExecutionHandler handler:表示当拒绝处理任务时的策略。
还有一个成员变量比较重要:poolSize
线程池中当前线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止。同一时刻,poolSize不会超过maximumPoolSize。
2.4、线程池对任务的处理
1、如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
2、如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务(maximumPoolSize);
3、如果当前线程池中的线程数目达到maximumPoolSize(此时线程池的任务缓存队列已满),则会采取任务拒绝策略进行处理;
任务拒绝策略,通常有以下四种策略:
1)ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
2)ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
4、如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
2.5、线程池的状态
1、线程池的状态说明:
RUNNING(运行):接受新任务并处理排队任务。
SHUTDOWN(关闭):不接受新任务,会处理队列任务。
STOP(停止):不接受新任务,不处理队列任务,并且中断进程中的任务。
TIDYING(整理):所有任务都已终止,工作计数为零,线程将执行terminated()方法终止线程。
TERMINATED(已终止):terminated()方法已完成。
2、各个状态之间的转换
RUNNING -> SHUTDOWN:调用shutdown()方法。
RUNNING or SHUTDOWN-> STOP:调用shutdownNow()方法。
SHUTDOWN -> TIDYING:当队列和池均都为空时。
STOP -> TIDYING:当池为空时。
TIDYING -> TERMINATED:当terminated()方法已完成。
三、JVM的那些事
3.1、JVM的结构
每个JVM都包含:方法区、Java堆、Java栈、本地方法栈、程序计数器等。
1、方法区:共享
各个线程共享的区域,存放类信息、常量、静态变量。
2、Java堆:共享
也是线程共享的区域,我们的类的实例就放在这个区域,可以想象你的一个系统会产生很多实例,因此Java堆的空间也是最大的。如果Java堆空间不足了,程序会抛出OutOfMemoryError异常。
3、Java栈:私有
每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个Java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈。如果Java栈空间不足了,程序会抛出StackOverflowError异常,想一想什么情况下会容易产生这个错误,对,递归,递归如果深度很深,就会执行大量的方法,方法越多Java栈的占用空间越大。
4、本地方法栈:私有
角色和Java栈类似只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。
5、程序计数器:私有
说到这里我们的类已经加载了,实例对象、方法、静态变量都去了自己该去的地方,那么问题来了,程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是程序计数器在管,它的作用就是控制程序指令的执行顺序。
6、执行引擎当然就是根据PC寄存器调配的指令顺序,依次执行程序指令。
3.2、Java堆的介绍及典型的垃圾回收算法介绍
3.2.1、Java堆的介绍
Java堆是虚拟机管理的最大的一块内存,堆上的所有线程共享一块内存区域,在启动虚拟机时创建。此内存唯一目的就是存放对象实例,几乎所有对象实例都在这里分配,这一点Java虚拟机规范中的描述是:所有对象实例及数组都要在堆上分配。
Java堆是垃圾收集器管理的主要区域,也被称为“GC堆”,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以分为:新生代和老年代。
堆的内存模型大致为:
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小。
其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
新生代是 GC 收集垃圾的频繁区域。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳 ( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
From Survivor区域与To Survivor区域是交替切换空间,在同一时间内两者中只有一个不为空。
3.2.2、如何确定某个对象是可回收的(垃圾)
1、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
这种方式的问题是无法解决循环引用的问题,当两个对象循环引用时,就算把两个对象都设置为null,因为他们的引用计数都不为0,这就会使他们永远不会被清除。
2、根搜索算法(可达性分析)
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
比较常见的将对象视为可回收对象的原因:
显式地将对象的唯一强引用指向新的对象。
显式地将对象的唯一强引用赋值为Null。
局部引用所指向的对象(如,方法内对象)。
只有弱引用与其关联的对象。
1)强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。
2)软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
3)弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4)虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
3.2.3、典型的垃圾回收算法介绍
1、标记-清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为“标注”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
标记过程:为了能够区分对象是live的,可以为每个对象添加一个marked字段,该字段在对象创建的时候,默认值是false。
清除过程:去遍历堆中所有对象,并找出未被mark的对象,进行回收。与此同时,那些被mark过的对象的marked字段的值会被重新设置为false,以便下次的垃圾回收。
缺点:效率低,空间问题(产生大量不连续的内存碎片),后续可能发生大对象不能找到可利用空间的问题。
2、复制算法(Copying)——新生代的收集算法就是这种,但是比例不是1:1,而是(8+1):1
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为大小相等的两块,每次只使用其中一块。当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存空间一次清理掉。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。
3、标记-整理算法(Mark-Compact)——老年代的收集算法
结合了以上两个算法,标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将所有存活对象移向内存的一端,然后清除端边界外的对象。如图:
4、分代收集算法(Generational Collection)
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。
老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
目前大部分JVM的GC对于新生代都采取复制算法(Copying),因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
老年代中的对象存活率高,没有额外空间对它进行分配,使用“标记-清理”或“标记-整理”算法来进行回收。
3.3、JVM处理FULLGC经验
3.3.1、内存泄漏
1、产生原因
1)JVM内存过小。
2)程序不严密,产生了过多的垃圾。
2、一般情况下,在程序上的体现为:
1)内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
2)集合类中有对对象的引用,使用完后未清空,使得JVM不能回收。
3)代码中存在死循环或循环产生过多重复的对象实体。
4)使用的第三方软件中的BUG。
5)启动参数内存值设定的过小。
3.3.2、Java内存泄漏的排查案例
1、确定频繁Full GC现象,找出进程唯一ID
使用jps(jps -l)或ps(ps aux | grep tomat)找出这个进程在本地虚拟机的唯一ID(LVMID,Local Virtual Machine Identifier)
2、再使用“虚拟机统计信息监视工具:jstat”(jstat -gcutil 20954 1000)查看已使用空间站总空间的百分比,可以看到FGC的频次。
3、找出导致频繁Full GC的原因,找出出现问题的对象
分析方法通常有两种:
1)把堆dump下来再用MAT等工具进行分析,但dump堆要花较长的时间,并且文件巨大,再从服务器上拖回本地导入工具,这个过程有些折腾,不到万不得已最好别这么干。
2)更轻量级的在线分析,使用“Java内存影像工具:jmap”生成堆转储快照(一般称为headdump或dump文件)。
jmap命令格式:jmap -histo:live 20954
4、一个工具:BTrace,没有使用过
四、MySQL的那些事
4.1、找出慢SQL的方法
4.1.1、开启慢查询日志
4.1.2、MySQL基准测试方法
4.2、索引的优缺点及实现原理
4.2.1、索引的优缺点
4.2.2、MySQL索引的实现原理
4.3、对于一个SQL的性能优化过程
4.4、MySQL数据库的分库分表方式以及带来的问题处理
4.5、MySQL主从复制数据一致性问题处理
五、HTTP、HTTPS、协议相关
5.1、HTTP请求报文和响应报文
5.2、HTTPS为什么是安全的?HTTPS的加密方式有哪些?
5.2.1、HTTPS的工作原理说明HTTPS是安全的
客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
1、客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
2、Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
3、客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
4、客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
5、Web服务器利用自己的私钥解密出会话密钥。
6、Web服务器利用会话密钥加密与客户端之间的通信。
5.2.2、HTTPS的加密方式有哪些?
1、对称加密
对称加密是指加密和解密使用相同密钥的加密算法。它要求发送方和接收方在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信至关重要。
2、更多的加密方式的了解
5.3、TCP三次握手协议,四次挥手
第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;
第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;
第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
完成三次握手,主机A与主机B开始传送数据。
5.4、OAuth协议介绍
5.5、防盗链Referer
Referer请求头: 代表当前访问时从哪个网页连接过来的。
当Referer未存在或者是从其他站点访问我们资源的时候就直接重定向到我们的主页,这样既可以防止我们的资源被窃取。
六、Spring
6.1、AOP的实现原理
spring框架对于这种编程思想的实现基于两种动态代理模式,分别是JDK动态代理 及 CGLIB的动态代理,这两种动态代理的区别是 JDK动态代理需要目标对象实现接口,而 CGLIB的动态代理则不需要。下面我们通过一个实例来实现动态代理,进而帮助我们理解面向切面编程。
JDK的动态代理要使用到一个类 Proxy 用于创建动态代理的对象,一个接口 InvocationHandler用于监听代理对象的行为,其实动态代理的本质就是对代理对象行为的监听。
6.2、Spring MVC工作原理
Spring的MVC框架主要由DispatcherServlet、处理器映射、处理器(控制器)、视图解析器、视图组成。
6.2.1、SpringMVC原理图
6.2.2、SpringMVC运行原理
1、客户端请求提交到DispatcherServlet
2、由DispatcherServlet控制器查询一个或多个HandlerMapping,找到处理请求的Controller
3、DispatcherServlet将请求提交到Controller
4、Controller调用业务逻辑处理后,返回ModelAndView
5、DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图
6、视图负责将结果显示到客户端
6.2.3、SpringMVC核心组件
1、DispatcherServlet:中央控制器,把请求给转发到具体的控制类
2、Controller:具体处理请求的控制器
3、HandlerMapping:映射处理器,负责映射中央处理器转发给controller时的映射策略
4、ModelAndView:服务层返回的数据和视图层的封装类
5、ViewResolver:视图解析器,解析具体的视图
6、Interceptors :拦截器,负责拦截我们定义的请求然后做处理工作
6.2.4、Servlet 生命周期
Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:
1、Servlet 通过调用 init () 方法进行初始化。
2、Servlet 调用 service() 方法来处理客户端的请求。
3、Servlet 通过调用 destroy() 方法终止(结束)。
4、最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
6.2.5、Spring容器初始化过程
Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配号Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。
七、分布式
7.1、分布式下如何保证事务一致性
分布式事务,常见的两个处理办法就是两段式提交和补偿。
7.1.1、两段式提交
分布式事务将提交分成两个阶段:
prepare;
commit/rollback
在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(参与者)的操作结果并最终指示这些节点是否需要把操作结果进行真正的提交。算法步骤如下:
第一阶段:
1、协调者会问所有的参与者,是否可以执行提交操作。
2、各个参与者开始事务执行的准备工作,如:为资源上锁,预留资源。
3、参与者响应协调者,如果事务的准备工作成功,则回应“可以提交”,否则回应“拒绝提交”。
第二阶段:
1、如果所有的参与者都回应“可以提交”。那么协调者向所有的参与者发送“正式提交”的命令。参与者完成正式提交并释放所有资源,然后回应“完成”,协调者收集各节点的“完成”回应后结束这个Global Transaction
2、如果有一个参与者回应“拒绝提交”,那么协调者向所有的参与者发送“回滚操作”,并释放所有资源,然后回应“回滚完成”,协调者收集各节点的“回滚”回应后,取消这个Global Transaction。
7.1.2、三段式提交
三段提交的核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源。他把二段提交的第一个段break成了两段:询问,然后再锁资源。最后真正提交。
7.1.2、事务补偿,最终一致性
补偿比较好理解,先处理业务,然后定时或者回调里,检查状态是不是一致的,如果不一致采用某个策略,强制状态到某个结束状态(一般是失败状态)。
八、常用的排查问题方法
8.1、Linux日志分析常用命令
8.1.1、查看文件内容
cat
-n 显示行号
8.1.2、分页显示
more
Enter 显示下一行
空格 显示下一页
F 显示下一屏
B 显示上一屏
less
/get 查询"get"字符串并高亮显示
8.1.3、显示文件尾
tail
-f 不退出持续显示
-n 显示文件最后n行
8.1.4、显示头文件
head
-n 显示文件开始n行
8.1.5、内容排序
sort
-n 按照数字排序
-r 按照逆序排序
-k 表示排序列
-t 指定分隔符
8.1.6、字符统计
wc
-l 统计文件中行数
-c 统计文件字节数
-L 查看最长行长度
-w 查看文件包含多少个单词
8.1.7、查看重复出现的行
uniq
-c 查看该行内容出现的次数
-u 只显示出现一次的行
-d 只显示重复出现的行
8.1.8、字符串查找
grep
8.1.9、文件查找
find
which
whereis
8.1.10、表达式求值
expr
8.1.11、归档文件
tar
zip
unzip
8.1.12、URL访问工具
curl
wget
8.1.13、查看请求访问量
页面访问排名前十的IP
cat access.log | cut -f1 -d " " | sort | uniq -c | sort -k 1 -r | head -10
页面访问排名前十的URL
cat access.log | cut -f4 -d " " | sort | uniq -c | sort -k 1 -r | head -10
查看最耗时的页面
cat access.log | sort -k 2 -n -r | head 10
九、中间件和架构
9.1、kafka消息队列
1、避免数据丢失
producer:
加大重试次数
同步发送
对于单条数据过大,要设置可接收的单条数据的大小
对于异步发送,通过回调函数来感知丢消息
block.on.buffer.full = true
consumer:
enable.auto.commit=false 关闭自动提交位移
2、避免消息乱序
假设a,b两条消息,a先发送后由于发送失败重试,这时顺序就会在b的消息后面,可以设置max.in.flight.requests.per.connection=1来避免。
max.in.flight.requests.per.connection:限制客户端在单个连接上能够发送的未响应请求的个数,设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求,但吞吐量会下降。
3、避免消息重复
使用第三方redis的set
9.2、ZooKeeper的原理
9.3、SOA相关,RPC两种实现方式:基于HTTP和基于TCP
9.4、Netty
9.5、Dubbo
十、其他
10.1、系统做了哪些安全防护
1、XSS(跨站脚本攻击)
全称是跨站脚本攻击(Cross Site Scripting),指攻击者在网页中嵌入恶意脚本程序。
XSS防范:
XSS之所以会发生,是因为用户输入的数据变成了代码。因此,我们需要对用户输入的数据进行HTML转义处理,将其中的“尖括号”、“单引号”、“引号”之类的特殊字符进行转义编码。
2、CSRF(跨站请求伪造)
攻击者盗用了你的身份,以你的名义向第三方网站发送恶意请求。
CSRF的防御:
1)尽量使用POST,限制GET
2)将cookie设置为HttpOnly
3)增加token
4)通过Referer识别
3、SQL注入
使用预编译语句(PreparedStatement),这样的话即使我们使用sql语句伪造成参数,到了服务端的时候,这个伪造sql语句的参数也只是简单的字符,并不能起到攻击的作用。
做最坏的打算,即使被’拖库‘('脱裤,数据库泄露')。数据库中密码不应明文存储的,可以对密码使用md5进行加密,为了加大破解成本,所以可以采用加盐的(数据库存储用户名,盐(随机字符长),md5后的密文)方式。
4、DDOS
最直接的方法增加带宽。但是攻击者用各地的电脑进行攻击,他的带宽不会耗费很多钱,但对于服务器来说,带宽非常昂贵。
云服务提供商有自己的一套完整DDoS解决方案,并且能提供丰富的带宽资源