目录
JVM
类加载机制:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
类的生命周期:
类的生命周期包括加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,连接又包含验证、准备、初始化三块内容
三个类加载器:
系统类加载器(system classloader),平台类加载器(platform classloader),启动类加载器(bootstrap classloader)。系统类加载器继承于平台类加载器,平台类加载器继承于启动类加载器。
双亲委派模型:
一个类加载器收到了类加载请求,会先委托给父类加载器去执行,父类加载器还有父类加载器则进一步委托。当委托到顶后,如果父类加载器可以完成类加载任务,则成功返回;如果不能完成,则交给其子类加载器尝试加载。
内存结构:
主要分为堆区、栈区、方法区
方法区和堆区是所有线程共享的内存区域;而java栈、本地方法栈和程序计数器是运行时线程私有的内存区域
堆区:
是java虚拟机所管理内存中最大的一块,被所有线程共享,在虚拟机启动时创建;用于存放对象实例,几乎所有的对象实例都在这里分配内存;堆区是垃圾收集器管理的主要区域,很多时候也被称为GC堆
堆区分为新生代和老年代两部分,默认新生代占1/3,老年代占2/3;其中,新生代被细分为Eden和两个Survivor区域,这两个 Survivor 区域分别被命名为 from和to,默认的Edem : from : to = 8 :1 : 1;通常情况下,对象主要分配在新生代的Eden区上,少数情况下也可能会直接分配在老年代中
新生代:主要用来存放新生的对象。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收;新生代又分为Eden、SurvivorFrom、SurvivorTo三个区
Eden:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收
SurvivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者
SurvivorTo:保留了一次MinorGC过程中的幸存者
在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survivalfrom区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代;MinorGC采用复制算法
老年代:主要存放应用程序中生命周期长的内存对象。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源;老年代的对象比较稳定,所以MajorGC不会频繁执行;MajorGC采用标记—清除算法
方法区:
被所有线程共享;用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
持久代:jdk8之前,持久代在方法区上,指内存的永久保存区域,GC不会在主程序运行期对永久区域进行清理,会随着加载的Class增多,最终抛出OOM(Out of Memory)异常;jdk8中,元数据区(元空间)取代持久代,元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制
栈区:
线程私有,存储基本数据类型、对象的引用
GC算法:
GC 算法基本上是基于根可达算法的 , 在 Java 中采用可达性分析算法来判断对象是否是垃圾,以 GCRoots 为根节点,从这些节点向下搜索,所遍历过的路径称为引用链 ,如果某一个对象到 GC Roots 没有任何引用链(即 GC Roots 到对象不可达)时,则证明此对象是不可用的。没有引用的对象是要回收的
内存泄露:
指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用;内存泄露积累最终将导致内存溢出
内存溢出:
指程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中
常用算法:
Mark-Sweep(标记-清除):标记清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象;缺点:标记和清除过程的效率都不高,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
Copying(复制):复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收,实现简单,运行高效;缺点:浪费了一半的内容
Mark-Compact(标记-整理):标记整理算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存;消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,消除了复制算法当中,内存减半的高额代价;缺点:效率不高,不仅要标记所有的存活对象,还要整理所有存活对象引用的地址,从效率上来说,标记整理算法低于复制算法
Generational Collection(分代收集):根据对象存活周期的不同将内存划分为几块, 如JVM中的新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的GC算法
常见的垃圾收集器:
-
Serial 单线程新生代复制算法的垃圾回收器
-
SerialOld 垃圾回收器,是一种单线程老年代标记整理算法
-
ParNew 垃圾回收器,是 Serial 的多线程实现,采用复制算法实现
-
Parallel Scavenge 垃圾回收器,是一种高效的多线程复制算法
-
ParallelOld 垃圾回收器,是 Parallel Scavenge 的一种老年代的多线程标记整理算法
-
CMS 垃圾回收器,是一种多线程标记清除算法
-
G1 垃圾回收器,是一种高吞吐量的垃圾回收器
串行回收器:Serial,Serial old;并行回收器:ParNew,Parallel scavenge,Parallel old;并发回收器:CMS、G1
新生代收集器:Serial,ParNew,Parallel scavenge;老年代收集器:Serial old,Parallel old.cMS;整堆收集器:G1
JVM调优:
jvm调优主要是针对吞吐量和响应时间进行调整优化,达到一个理想的目标,根据业务确定目标是吞吐量优先(单位时间内STW时间最短)还是响应时间优先(单次STW时间最短),尽量在大吞吐量的情况下降低响应时间
调优原则:
优先原则:优先架构调优和代码调优,JVM优化是不得已的手段,大多数的Java应用不需要进行JVM优化
调优参数设置位置:
war包部署在tomcat中设置
jar包部署在启动参数设置
调优参数:
设置堆空间大小、虚拟机栈大小的设置、年轻代中Eden区和两个Survivor区的大小比例、年轻代晋升老年代阈值、设置垃圾回收收集器
调优工具:
命令:
jps:进程状态信息;jstack:查看java进程内线程的堆栈信息;jmap:查看堆转信息;jhat:堆转储快照分析工具;jstat:JVM统计监测工具
可视化工具:
jconsole:用于对jvm的内存,线程,类的监控;VisualVM:能够监控线程,内存情况
排查思路:
内存泄露:获取堆内存快照dump;VisualVM去分析dump文件;通过查看堆信息的情况,定位内存溢出问题
CPU飙高:使用top命令查看占用cpu的情况;通过top命令查看后,可以查看是哪一个进程占用cpu较高;ps命令查看进程中的线程信息;使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
锁
java中的锁、是在多线程环境下为保证共享资源健康、线程安全的一种手段
太杂了没心情,暂停更新ing QAQ
线程
进程是系统进行资源分配的基本单位,线程是处理器调度的最小单位
线程五种状态:新建、就绪、运行、阻塞、消亡
线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度
面试专攻:
run()和start()区别:
start()方法用于启动线程,run()方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次
调用 start() 方法会启动一个线程并使线程进入就绪态,当分配到时间片后就可以开始运行。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是多线程工作;而直接执行 run() 方法,会把 run()方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这不是多线程工作。
sleep()和wait()区别:
sleep()是线程类(Thread)的方法,wait()是Object类的方法
sleep()是使线程休眠,不会释放锁,wait()是使线程等待,会释放锁,需要notify()或notifyAll()
特别的,wait()、notify()、notifyAll()必须在同步方法或同步代码块被调用
创建线程三种方式:
继承Thread类:重写run()方法,在run()方法中实现运行在线程上的代码,调用start()方法开启线程
实现Runnable接口:Runnable规定的方法是run(),无返回值,无法抛出异常
实现Callable接口:Callable规定的方法是call(),任务执行后有返回值,可以抛出异常
ThreadLocal变量:
ThreadLocal是线程的局部变量,为每一个使用该变量的线程提供一个本地拷贝,其他线程操作这个变量时,实际是在操作自己本地内存的变量副本,起到线程隔离的作用,避免了并发场景下的线程安全问题
线程池的创建方法:
-
Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
-
Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
-
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
-
Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
-
Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程
-
Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
-
ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置
创建出线程池的对象,对象的参数及区别:
对象是ThreadPoolExecutor
参数:
corePoolSize:核心线程数,也就是最少的线程数
maxPoolSize:最大线程数
keepAliveTime:线程最大活跃时间
unit:时间单位
workQueue:阻塞队列,线程用完时,任务放进的队列
threadFactory:线程工厂,用来创建新线程
handler:丢弃策略,当队列也满时,如何处理的策略
线程池执行流程:
提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker() 方法创建一个核心线程去执行任务;
如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;
如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;
如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略
CAS算法:
乐观锁的核心是CAS,CAS是一种能让线程不需要通过阻塞就能避免多线程安全问题的一种算法,它可以不使用锁而保证多线程安全,是一种无锁算法
CAS全称是Compare and Swap,即比较并交换,是实现并发计算时常用到的技术,CAS包括内存值、预期值、新值,当且仅当预期值等于内存值时,才将新值保存到内存中
死锁:
两个或多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
死锁四要素:互斥条件:一个资源每次只能被一个进程使用;请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
避免死锁:锁的目的就是互斥,所以不能阻止,需要通过阻止其他三个条件来避免死锁。最简单的是阻止循环等待条件:将系统中所有资源设置标志位排序,规定所有的线程申请资源必须以一定的顺序来操作;阻止请求与保持条件:一个线程必须一次性申请所有的锁,不能单独持有某一个锁;阻止不剥夺条件:一个线程获取不到锁时,就主动释放持有的所有锁
活锁:
指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。线程没有发生阻塞,但依然执行不下去,例如一个抽水一个注水
解决活锁:增加随机的睡眠时间,将这样的两个线程错开执行,只要一方先运行完了,那么另一方也就能运行完
饥饿:
线程因为某些原因获取不得资源,导致程序无法执行。如在CPU繁忙时,如果一个线程优先级太低,就有可能遇到一直得不到执行,或持有锁的线程,如果执行的时间过长,会导致其他阻塞的线程一直获取不到锁
避免饥饿:保证资源充足,公平地分配资源,避免持有锁的线程长时间执行