2018/6/15 19:00:02当前位置推荐好文阿里云教程浏览文章

一、HashMap的那些事

1.1、HashMap的实现原理

1.1.1、结构

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。如下图所示:

image.png

当新建一个HashMap的时候,就会初始化一个数组。哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。这些元素一般情况是通过hash(key)%len的规则存储到数组中,也就是元素的key的哈希值对数组长度取模得到。

1.1.2、核心变量

image.png

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栈、本地方法栈、程序计数器等。

image.png

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堆中还能分为:新生代和老年代。

堆的内存模型大致为:

image.png

默认的,新生代 ( 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,以便下次的垃圾回收。

缺点:效率低,空间问题(产生大量不连续的内存碎片),后续可可以发生大对象不可以找到可利使用空间的问题。

image.png

2、复制算法(Copying)——新生代的收集算法就是这种,但是比例不是1:1,而是(8+1):1

为理解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为大小相等的两块,每次只用其中一块。当这一块内存满后将尚存活的对象复制到另一块上去,把已用的内存空间一次清除掉。这种算法尽管实现简单,内存效率高,不易产生碎片,但是最大的问题是可使用内存被压缩到了本来的一半。且存活对象增多的话,Copying算法的效率会大大降低。

image.png

3、标记-整理算法(Mark-Compact)——老年代的收集算法

结合了以上两个算法,标记阶段和Mark-Sweep算法相同,标记后不是清除对象,而是将所有存活对象移向内存的一端,而后清理端边界外的对象。如图:

image.png

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的频次。

image.png

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请求报文和响应报文

image.png

5.2、HTTPS为什么是安全的?HTTPS的加密方式有哪些?

5.2.1、HTTPS的工作原理说明HTTPS是安全的

image.png

用户端在用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
1、用户用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
2、Web服务器收到用户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给用户端。
3、用户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
4、用户端的浏览器根据双方同意的安全等级,建立会话密钥,而后利使用网站的公钥将会话密钥加密,并传送给网站。
5、Web服务器利使用自己的私钥解密出会话密钥。
6、Web服务器利使用会话密钥加密与用户端之间的通信。

image.png

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协议详情

image.png

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原理图

image.png

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之间的依赖关系,为上层应使用提供准备就绪的运行环境。

image.png

七、分布式

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

image.png

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处理方案,并且可以提供丰富的带宽资源

10.2、项目管理工具

10.2.1、OmniPlan

10.2.2、Excel

上一篇 目录 已是最后
网友评论