一、JVM基础
1.1JVM内存模型
jdk1.8的内存模型:
JVM由堆 方法区,虚拟机栈,本地方法栈,程序计数器,PC寄存器组成,其中方法区是JVM定义的规范,元空间和永久代都是他的实现方式。
在JDK8以后,元空间取代了永久代,实现了方法区。
在JDK8以前 HotSpot这种主流的虚拟机在JDK8之前使用了永久代来实现方法区,在JDK8以及以后,弃用了永久代这种实现方式,采用了元空间这种实现方式。
在JDK8之前,类的元信息,常量池,静态变量等都储存在永久代这种方法区的实现方式中,在JDK8及以后,常量池、静态变量被移出方法区,转移到了堆中,元信息依然保存在方法区中,只是使用了源空间这种实现方式。
方法区只是一个抽象的概念,而永久代和元空间都是他的实现方式。
1.2 JVM各区详细
1.2.1 程序计数器
(Program Counter Register)
a.程序计数器是一块较小内存,当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。
b.程序计数器是一块“线程私有”的内存,每条线程都有一个独立的程序计数器,能够将切换后的线程恢复到正确的执行位置。
c.如果线程正在执行一个 Java 方法,这个计数器记录的正在执行的虚拟机字节码指令的地址,如果执行的是 Native 方法,这个计数器值为空。
Native 方法: 简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。
“A native method is a Java method whose implementation is provided by non-java code.”
1.2.2 Java虚拟机栈
(Java Virtual Machine Stacks)
1、Java虚拟机栈(Java Virtual Machine Stacks)描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),栈帧中存储着局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,会对应一个栈帧在虚拟机栈中入栈到出栈的过程。与程序计数器一样,Java虚拟机栈也是线程私有的。
a.局部变量表主要存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置),returnAddress类型(指向了一条字节码指令的地址)。
b. 操作栈帧,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是 用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
c.指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时的常量池
d.方法返回地址,当一个方法执行完毕后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法的返回地址。
由于每个线程执行正在执行的方法可能不同,因此每个线程都有一个Java栈,互不干扰。
2、Java虚拟机规范中对这个区域规定了两种异常状况:
- StackOverflowError(栈溢出):若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
public class StackOverFlowErrorTest {
private int count = 0;
// 方法自己调用自己,在虚拟机栈中建立一个又一个栈帧,直到到达栈深度
public void testStack() {
count++;
testStack();
}
public void test() {
try {
testStack();
} catch (Throwable e) {
System.out.println(e);
System.out.println("stack height:" + count);
}
}
public static void main(String[] args) {
new StackOverFlowErrorTest().test();
}
}
OutOfMemoryError(内存溢出):当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。
(这行代码不要轻易尝试,容易内存飚满
注意,特别提示一下,如果要尝试运行上面这段代码,记得要先保存当前的工作。由于在Windows平台的虚拟机,Java的线程是映射到操作系统的内核线程上的,因此上述代码执行时有较大的风险,可能会导致操作系统假死。
public class OutOfMemoryTest {
private void dontStop() {
while (true) {
}
}
//不断创建线程
public void stackLeakByThread() {
try {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
} catch (Throwable e) {
System.out.println(e);
System.out.println("stack height:" + count);
}
}
public static void main(String[] args) throws Throwable {
OutOfMemoryTest oom = new OutOfMemoryTest();
oom.stackLeakByThread();
}
}
、对于虚拟机栈内存形象理解
可以把虚拟机栈内存看做一个弹夹上述testStack()方法相当于子弹,每执行一次该方法就相当于往弹夹里压入一颗子弹(栈帧),而栈深度就相当于弹夹的容量,即栈帧的数量,当方法执行过多时,弹夹容量达到限制没法再压入子弹,就会出现StackOverflowError,若 Java 虚拟机栈的内存大小允许动态扩展,就相当于把原来的弹夹换成了扩容弹夹,可以再压入子弹,当扩容弹夹再次压满时就会出现OutOfMemoryError
1.2.3 Java本地方法栈
(Native Method Stack)
本地方法栈(Native Method Stack)与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
在虚拟机规范中对本地方法栈中使用的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。甚至有的虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。与虚拟机一样,本地方法栈会抛出StackOverflowError和OutOfMemoryError异常。
1.2.4 堆Heap
对于大多数应用而言,Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,它被所有线程共享的,在虚拟机启动时创建。此内存区域唯一的目的是存放对象实例,几乎所有的对象实例都在这里分配内存,且每次分配的空间是不定长的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
Java堆是垃圾收集器管理的主要区域,因此也被称为 “GC堆(Garbage Collected Heap)” 。从内存回收的角度看内存空间可如下划分:
a.新生代(Young):新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。如果把新生代再分的细致一点,新生代又可细分为Eden空间、From Survivor空间、To Survivor空间,默认比例为8:1:1。
b.老年代(Tenured/Old):在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
c.永久代(Perm):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
其中新生代和老年代组成了Java堆的全部内存区域,其中新生代和老年代组成了Java堆的全部内存区域
1.2.5 方法区\元空间
(Method Area)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。Object Class Data(类定义数据)是存储在方法区的,此外,常量、静态变量、JIT编译后的代码也存储在方法区。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 Non-Heap。
JDK 1.8以前的永久代(PermGen)
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集,也就是说,Java虚拟机规范只是规定了方法区的概念和它的作用,并没有规定如何去实现它。对于JDK 1.8之前的版本,HotSpot虚拟机设计团队选择把GC分代收集扩展至方法区,即用永久代来实现方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机(如Oracle JRockit、IBM J9等)来说是不存在永久代的概念的。
如果运行时有大量的类产生,可能会导致方法区被填满,直至溢出。常见的应用场景如:
1.Spring和ORM框架使用CGLib操纵字节码对类进行增强,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
2.大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。
3.基于OSGi的应用(即使是同一个类文件,被不同的类加载器加载也会视为不同的类)。
这些都会导致方法区溢出,报出java.lang.OutOfMemoryError: PermGen space。
JDK 1.8的元空间(Metaspace)
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
1.3Java类加载过程
面向对象SOLID原则:单一功能、开闭、里氏替换、接口隔离、依赖反转
首先,本人觉得上图为一个类完整的生命周期,在最开始,我们可以加上javac编译阶段,而下文所重点研究的类加载,只有加载、连接、和初始化过程
需要区分加载与类加载,加载只是类加载的一个环节
所谓的解析部分是灵活的,他可以发生在初始化环节之后,实现所谓的“后期绑定”也可以发生在初始化环节之前。其他环节顺序不可变
1.加载
通过该类的全限定名称来获取此类的二进制字节流,将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义再内存中生存一个代表这个类的java.lang,Class对象,它将作为程序访问方法去中的这些类型数据的外部接口。
简单概括就是:
读取一个Class文件,将其转化为某种静态的数据结构存储在方法区,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程(这里的class文件,不一定指的是本地文件,泛指各种来源的二进制流,比如网络,数据库,甚至是即时生成的class,著名的动态代理技术就是使用到了即时生成的class文件)
2.验证(文件格式验证(发生在加载阶段)、元数据、字节码、(发生在此阶段)符号引用(发生在解析阶段))
是连接的第一步,其中更细分为格式验证,语义验证,元数据验证和符号引用验证
验证的机制是会不断发生变化的,从最初低版本的虚拟机到现在,验证已经不断加入各种完善的机制,
验证工作是JVM类加载系统中占了很大的工作量的一部分,首先是为了验证一下该字节码文件不会产生危害虚拟机的行为,符合虚拟机的要求,但由于他对程序运行期没有影响,并不一定必要,可以考虑率Xverify:none
元数据的验证:
在元数据和字节码验证过后,虚拟机会姑且认为该class是安全的,这时候会进入准备阶段,
3、准备
准备阶段的工作,为该类型中的静态变量赋0值
4、解析
将符号引用替换为直接引用。
如何理解符号引用和直接引用?
当一个Java类被编译成class文件以后,假如这个类成为A,而在A中引用了B,那么在编译阶段,A并不知道B有没有被编译而且此时B也一定没有被加载,所以A无法知道B在内存中真实的地址,这个时候在A中有一串字符串代替了B的地址,这个字符串就称为符号引用,在运行时,如果A发生了类加载,到了解析阶段发现B还未被加载,那么A将会触发B的类加载机制,这个时候字符串的值将替换为B的实际地址,这就是符号引用替换为直接引用的过程。
除此之外,在JAVA中,还有多态和后期绑定,那么实现这两种的就是上图的动态解析,假如上层代码使用了多态,那么这里的B可能是一个抽象类或者接口,那么可能有两个具体的实现类C和D,那么我们就不知道使用哪个类的具体引用来直接替换,直到运行过程中发生了调用,此时虚拟机调用栈中将会得到具体的类型信息,这时候再进行解析,就可以用明确的直接引用替换符号引用,这也就是上文为什么说解析可以发生在初始化之后这就是动态解析,实现了上层的后期绑定,底层是调用了Invokevirtual这条指令
当解析阶段完成,意味着连接阶段完成,这就意味着一个外部的java类顺利的进入到了虚拟机中,
初始化阶段
此时代码中会主动判断是否存在资源初始化的操作,如果有的话那么执行,比如说成员变量的赋值,静态变量的赋值,执行静态代码块
只有加载步骤中的读取二进制流与初始化部分,能够被上层开发者,也就是大部分的Java程序员控制,而剩下的所有步骤,都是由JVM掌控,其中的细节由JVM的开发人员处理,对上层开发者来说是个黑盒。
1.4内存溢出与内存泄漏
内存溢出:指可动态扩展的虚拟机栈,在扩展时无法申请到足够的空间(一句话说就是内存不够了)
内存泄漏:是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
1.5 类加载器
JVM将类加载内部复杂的实现封装了起来,拒绝上层开发者修改,只提供一个借口,用于修改class二进制流文件的读取。这一点点的空间,也给了很多大神施展拳脚的舞台,比如基于该特性实现的:热部署,动态代理。
类加载器的分类属于JVM的规范,是一种抽象的概念,各个不同的JVM的实现方式不一定一样。
从JVM的角度,可将类加载器分为两种:
1.启动类加载器
由C++语言实现,是虚拟机自身的一部分
负责加载存放在<JAVA_HOME>\lib目录中、或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库,比如java、javax、和sun开头的包。无法被Java程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话可直接用null代替
2.其他类加载器
由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader,可被Java程序直接引用。常见几种:
1.扩展类加载器
A.由sun.misc.Launcher$ExtClassLoader实现
B.负责加载<JAVA_HOME>\lib\ext目录中的、或者被java.ext.dirs系统变量所指定的路径中的所有类库
2.应用程序类加载器
A.是默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类加载器
B.由sun.misc.Launcher$App-ClassLoader实现
C.负责加载用户类路径上所指定的类库
3.自定义类加载器
如果以上类加载起不能满足需求,可自定义,需要注意的是:虽然数组类不通过类加载器创建而是由JVM直接创建的,但仍与类加载器有密切关系,因为数组类的元素类型最终还要靠类加载器去创建。
UserClassLoader允许用户获取来自任意来源的二进制流文件。只需要继承java.lang.ClassLoader之后,单独实现获取二进制流文件的逻辑,其他部分还是由java.lang.ClassLoade进行处理,用户无权干涉。
1.6双亲委派模型
双亲委派模型,约定类加载器的家在机制,当一个类加载器接收到一个类加载任务时,并不会立即开始执行任务,而是将加载任务委托给他的父类加载器去执行,每一层的类都采用相同的额方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行。
双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这请求委托给父类加载器去完成,每一层的类加载器都如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己悟饭完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
好处:能够确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行阶段,始终只会加载其中的某一个类,双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
1.6.2 双亲委派源码
首先检查该类是否已经被加载过,如果没有开启加载流程,如果有,那么直接读取缓存
parent代表当前的父亲加载器,这里就可以看出来 类加载器不是通过继承,而是通过组合来实现上下级关系 当parent==null,约定parent为bootstrapClassLoader,因为上文提及到,bootstrap是C++实现的,无法被程序引用,所以说约定为null。当parent为null,调用findBootStrapClassLoader,让BootStrap尝试加载,如果返回的class为null说明加载不了,那么久调用findclass,findclass表示如何去寻找限定名的class,需要各个类加载器独立去实现
Extclasssloader和appclassloader都使用了这段逻辑,将类限定名转换为path对象,再使用ucp对象去寻找,找到以后调用defineClass去加载后续流程
defineClass是java.lang.classloader中一个被final修饰的方法,这也印证了上文提到的,类加载器只允许用户自定义二进制文件的来源,不能自己去实现加载过程
1.7GC算法
1.标记-清除算法
该算法先标记,再清除,将所有需要回收的内存随便标记,然后清除。缺点是:效率比较低,标记清除后会造成大量不连续的内存碎片,这些碎片太多的时候,在存储大的对象的时候会导致GC回收,浪费内存和时间。
2.复制算法
3.标记整理算法
4.分代收集算法
1.8GC的回收过程
现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。
1.9GC如何判断对象是否回收
1.10Java的四种引用
强引用
指创建一个对象,并把这个对象赋给一个引用变量
强引用有引用变量指向时,永远不会被垃圾回收器回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
软引用
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它。
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。
另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。
弱引用
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
二、线程
1、线程创建的四种方法
集成Thread类创建线程
实现Runnable接口创建线程
使用Callable接口和Future创建线程
使用线程池例如Executor框架
2、 线程的生命周期
当线程被创建以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(new),就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态,尤其是线程启动以后,它不能一直占有CPU资源独自运行,所CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
1、新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
Thread t1=new Thread();
就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源
t1.start();
运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用wait()方法。(调用motify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止
3、wait和sleep的区别
1、首先wait方法是Object类中的,而sleep方法是Thread中的。
2、sleep方法导致了线程暂停执行一段时间,在这段时间里,把CPU的资源让给线程,但是此线程的监控状态依然保持活跃,在时间到达以后,会重新回到运行状态
3、而wait方法,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对象调用notify()方法之后,本线程才进入对象锁定池准备。获取对象锁进入运行状态。
4、对于死锁的理解
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。(百度百科)
三、锁
四、集合
1、ArrayList源码分析
五、线程池
1、线程池七大参数
CorePoolSize:线程池核心线程大小。
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
maximumPoolSize线程池最大线程数量
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
四、unit 空闲线程存活时间单位
keepAliveTime的计量单位
五、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
六、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
六、数据库
1.mysql中innodb和myisam的区别?
innodb支持事务,myisam不支持事务,对于InnoDB,每一条SQL语句会默认封装成事务,自动提交,效率比较低,所以建议,把多条SQL语句写在begin和commit之间组成一个事务,提高效率。
innoDB支持外键,myisam不支持外键,对一个包含外键的innoDB转为myisam表会失败
innoDB是聚集索引,数据文件和索引是在一起的,必须要有主键,通过主键索引效率很高,但是辅助索引会进行两次查询,第一次查询到主键,然后再根据主键进行索引查询到数据。因此,主键不应该过大,因为主键太大,其他索引也会很大。而myisam是非聚集索引,数据库文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
innoDB不保存表的具体行数,执行select count(*)from table时需要扫描全表扫描。而MyISAM用一个变量保存看整个表的行数,执行上述操作只需要读出变量即可,速度很快。
InnoDB不支持全文索引,而MyISAM支持全文索引,查询效率比MyISAM高;
两者如何选择:
1. 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读写也挺频繁,请使用InnoDB。
系统奔溃后,MyISAM恢复起来更困难,能否接受;
MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
[
](https://blog.csdn.net/weixin_43436478/article/details/103163614)
2.SQL语句分类?
SQL的英文全称为Structured Query Language,SQL用来和数据库打交道,完成和数据库的通信,SQL是一套标准,但是每一个数据库都有自己的特性,当使用这个数据库特性相关的功能,这时SQL语句可能就不是标准了,但是百分之九十以上的SQL都是通用的。
SQL语句的五种分类分别是DQL、DML、DDL、TCL和DCL,下面对SQL语句的五种分类进行列举:
1、数据库查询语言(DQL)
数据查询语言DQL基本结构是由SELECT子句,FROM子句,WHERE 子句组成的查询块,简称DQL,Data Query Language。代表关键字为select。
2、数据库操作语言(DML)
用户通过它可以实现对数据库的基本操作。简称DML,Data Manipulation Language。代表关键字为insert、delete 、update。
3、数据库定义语言(DDL)
数据定义语言DDL用来创建数据库中的各种对象,创建、删除、修改表的结构,比如表、视图、索引、同义词、聚簇等,简称DDL,Data Denifition Language。代表关键字为create、drop、alter。和DML相比,DML是修改数据库表中的数据,而 DDL 是修改数据中表的结构。
4、事务控制语言(TCL)
TCL经常被用于快速原型开发、脚本编程、GUI和测试等方面,简称:TCL,Trasactional Control Languag。代表关键字为commit、rollback。
5、数据控制语言(DCL)
数据控制语言DCL用来授予或回收访问数据库的某种特权,并控制数据库操纵事务发生的时间及效果,对数据库实行监视等。简称:DCL,Data Control Language。代表关键字为grant、revoke。
3.事务的ACID特性?
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败。
一致性(Consistency)
官网上事务一致性的概念是:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
换一种方式理解就是:事务按照预期生效,数据的状态是预期的状态。
举例说明:张三向李四转100元,转账前和转账后的数据是正确的状态,这就叫一致性,如果出现张三转出100元,李四账号没有增加100元这就出现了数据错误,就没有达到一致性。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。
4.脏读、幻读、不可重复读?
A事务执行过程中,B事务读取了A事务的修改。但是由于某些原因,A事务可能没有完成提交,发生RollBack了操作,则B事务所读取的数据就会是不正确的。这个未提交数据就是脏读(Dirty Read)。脏读产生的流程如下:
不可重复读(Nonrepeatable Read)
B事务读取了两次数据,在这两次的读取过程中A事务修改了数据,B事务的这两次读取出来的数据不一样。B事务这种读取的结果,即为不可重复读(Nonrepeatable Read)。不可重复读的产生的流程如下:
不可重复读有一种特殊情况,两个事务更新同一条数据资源,后完成的事务会造成先完成的事务更新丢失。这种情况就是大名鼎鼎的第二类丢失更新。主流的数据库已经默认屏蔽了第一类丢失更新问题(即:后做的事务撤销,发生回滚造成已完成事务的更新丢失),但我们编程的时候仍需要特别注意第二类丢失更新。它产生的流程如下:
5.内连接、左外、右外、子查询的意义和差异?
内连接就是查出多张表的交集部分,而左外连接就是查询左表所有数据以及交集部分。显然,右外连接就是查询右边所有数据以及其交集部分。
七、JDBC和mybatis
1.JDBC使用步骤?
- 加载 jdbc 驱动程序
- 拼接 jdbc 需要连接的 url
- 创建数据库的连接
- 创建一个Statement
- 执行SQL语句
- 处理执行完SQL之后的结果
- 关闭使用的JDBC对象
2.Mybatis多参数处理方案?
1. 利用参数出现的顺序
2.使用注解
3.使用map:
使用map集合的方式传入多个参数值,在SQL中我们使用map中的key来映射值
4、利用对象
3、动态SQL和批处理
if标签
if标签在开发中用的最多,因为要做一些字段的非空校验,保证SQL的正确性。
这个用法很简单,test属性就相当于java代码中if括号内的内容。有两个细节可以注意一下。<select id = "selectUser" parameterType="com.zdydoit.User" resultType="com.zdydoit.User">
select id,user_name,age,phone,height,position_id,created_time,updated_time
from t_user
where 1=1
<if test="userName != null and userName !=''">
and user_name = #{userName}
</if>
<if test="phone != null and phone !=''">
and phone = #{phone}
</if>
<if test="age != null">
and age = #{age}
</if>
</select>
- 第一点,在判断字符串的时候,是双重条件,同时判断null和空串,而数值、日期只要判断非空即可。
- 另外还有一点就是这里的判断是遵循短路与的,test里面的and连接符就符合这个规则,当前一个条件判断false后,and后面的就不会再判断,这样在对象判断的时候就会避免出现空指针问题。
where标签
上面if标签的实例可以看的出来在where条件中代码是很不优雅的,有一个1=1,就是为了防止where后面紧接着的是and,导致代码报错。这里就用到where标签来优化。如下:
<select id = "selectUser" parameterType="com.zdydoit.User" resultType="com.zdydoit.User">
select id,user_name,age,phone,height,position_id,created_time,updated_time
from t_user
<where>
<if test="userName != null and userName !=''">
and user_name = #{userName}
</if>
<if test="phone != null and phone !=''">
and phone = #{phone}
</if>
<if test="age != null">
and age = #{age}
</if>
</where>
</select>
这样的好处就是可以去除开头可能出现and或者or的问题,同时如果if条件都不满足,那么就不会有where条件内容。另外还有一个很大的好处,就是使代码得到了优化。
set标签
set标签和where标签是类似的,只是使用的位置不同,set标签是用在update语句下。如下:
<update id="updateUser" paramterType="com.zdydoit.User">
update t_user
set
<if test="userName != null and userName != ''">
user_name = #{userName},
</if>
<if test="phone != null and phone != ''">
phone = #{phone},
</if>
<if test="positionId != null">
position_id = #{positionId},
</if>
id = #{id}
where id = #{id}
</update>
这是优化前的代码,感觉是无懈可击,但是set里面的内容还是感觉有点别扭,多写了一个id=#{id},完全就是为了防止最后一个条件后面有逗号出现导致SQL报错而存在。和where里面加上1=1是同一个意思,where可以优化,set当然也可以。
优化后的代码:
<update id="updateUser" paramterType="com.zdydoit.User">
update t_user
<set>
<if test="userName != null and userName != ''">
user_name = #{userName},
</if>
<if test="phone != null and phone != ''">
phone = #{phone},
</if>
<if test="positionId != null">
position_id = #{positionId},
</if>
</set>
where id = #{id}
</update>
trim标签
trim标签适用的范围比较广,可以用来替代where、set标签,换句话说,where、set标签是优化后的trim标签。where是用在where条件的时候,set是用在update语句中,现在还差insert语句没有得到优化。
<insert id="insertUser" parameterType="com.zdydoit.User">
insert into t_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userName != null and userName !=''">
user_name,
</if>
<if test="phone != null and phone !=''">
phone,
</if>
<if test="positionId != null">
position_id,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userName != null and userName !=''">
user_name,
</if>
<if test="phone != null and phone !=''">
phone,
</if>
<if test="positionId != null">
position_id,
</if>
</trim>
</insert>
完美的对insert语句进行了优化。这里trim标签有下面四个属性:
- prefix表示前缀内容;
- suffix表示后缀内容;
- prefixOverrides标签内内容开头需要覆盖的部分;
- suffixOverrides标签内内容结尾需要覆盖的部分。
优化后 :
<!-- 替换where -->
<trim prefix="where " prefixOverrides="and | or">
<!-- 忽略内容 -->
</trim>
<!-- 替换set -->
<trim prefix="set " suffixOverrides=",">
<!-- 忽略内容 -->
</trim>
虽然trim可以用来替代where和set,但是实际使用上还是用where和set更好,不需要设置属性值,直接用就好,简洁不易出错,而且辨识度很高,用在对应的位置一看就知道。
foreach标签
这个标签可用于批处理,也可用于一些条件的组合。用法如下:
<!-- in条件查询 -->
<select id = "selectByIds" parameterType="java.util.List">
select id,user_name,phone,age,position_id
from t_user
<where>
<if test="userIds != null and userIds.size()>0">
id in
<foreach collection="userIds" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</if>
</where>
</select>
<!-- 批量插入 -->
<insert id = "batchInsert" parameterType="java.util.List">
<if test="users != null and users.size()>0">
insert into t_user
(user_name,phone,age,position_id)
values
<foreach collection="users" item="user" separator="," open="(" close=")">
#{userName},
#{phone},
#{age},
#{positionId}
</foreach>
</if>
</insert>
第一种用的是in多条件查询,第二种用的是批量插入数据。其中的collection字段表示当前参数中的集合,item表示的是collection中遍历出来的元素,open表示遍历单条内容的开始符号,close表示遍历单条内容结束符号,separator表示的是每次循环结束后使用的分隔符。
批处理的另一种方式
上面说了可以使用foreach实现批处理,但是这里还有一种方式就是通过配置defaultExecutorType属性为BATCH来实现,配置位置是mybatis的mybatis-config.xml配置文件中的settings标签下(具体可以参考ORM框架之Mybatis:基础配置)。当配置后,一个事务中执行的多个SQL不会单独的自动提交,而是通过最后commit才会一次性提交所有SQL。
defaultExecutorType有三个可选值,分别是SIMPLE、REUSE、BATCH,默认的是SIMPLE没有特殊的说明,REUSE表示重用预处理语句,而BATCH表示重用语句和批量处理。但是实际上使用BATCH的场景并不多,为了项目中仅有的几个用的上批处理的业务,做一系列的批处理配置并不划算,另外一般的批处理foreach已经可以应付,无需配置BATCH。
choose、when、otherwise组合标签
这三个标签是组合使用的,同时这些使用场景也不多,所以这里放到最后来写。
<select id = "selectUser" parameterType="java.lang.Integer">
select id,user_name,phone,age,position_id
from t_user
<where>
<choose>
<when test="userName != null and userName !=''">
user_name = #{userName}
</when>
<when test="phone != null and phone !=''">
phone = #{phone}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</where>
</select>
这三个标签是组合使用的,otherwise标签官方说明是必须存在的,但是实际使用中是可以不要的,需要看实际的需求和where条件的书写格式。
这连个标签一起使用类似于if…else if…else实现方式。如果其中有一个条件成立,其他判断条件内的内容就被略过。一般用于几个筛选条件,条件有一定的优先级。
4、mybatis中#和$的区别
{}:占位符号,好处防止sql注入
${}:sql拼接符号
优先使用 #{}。因为 ${} 会导致 sql 注入的问题
区别:
1、#{ }是预编译处理,MyBatis在处理#{ }时,它会将sql中的#{ }替换为?,然后调用PreparedStatement的set方法来赋值,传入字符串后,会在值两边加上单引号,如上面的值 “4,44,514”就会变成“ ‘4,44,514’ ”;
2、${ }是字符串替换, MyBatis在处理${ }时,它会将sql中的${ }替换为变量的值,传入的数据不会加两边加上单引号。
注意:使用${ }会导致sql注入,不利于系统的安全性!
SQL注入:就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。常见的有匿名登录(在登录框输入恶意的字符串)、借助异常获取数据库信息等
应用场合:
1、#{ }:主要用户获取DAO中的参数数据,在映射文件的SQL语句中出现#{}表达式,底层会创建预编译的SQL;
2、${ }:主要用于获取配置文件数据,DAO接口中的参数信息,当$出现在映射文件的SQL语句中时创建的不是预编译的SQL,而是字符串的拼接,有可能会导致SQL注入问题.所以一般使用$接收dao参数时,这些参数一般是字段名,表名等,例如order by {column}。
注:
${}获取DAO参数数据时,参数必须使用@param注解进行修饰或者使用下标或者参数#{param1}形式;
{}获取DAO参数数据时,假如参数个数多于一个可有选择的使用@param。
[
](https://blog.csdn.net/qq_32265203/article/details/89965764)
5.Mybatis的缓存策略和懒加载?
1、首先懒加载又叫延迟加载
在需要用到副表的数据时再加载,如果用不到则不进行加载
好处:先查询主表数据,后期用到副表数据时,再从关联中去查询数据,减少了一次性查询大量数据,大大提高了数据库性能,因为查询单表要比查询多表速度快。
坏处:因为只有在 用到副表数据时才会进行数据库查询,如果有大批量数据查询时,因为查询工作也要消耗时间,所以可能会造成用户等待时间变长,影响用户体验 。
数据库中有四种表关系:一对多,多对一,一对一,多对多
一对多,多对多:通常情况下我们采用延迟加载
一对多,多对一:通常情况下我们采用立即加载(Mybatis没用多对一的概念,当一对一看)
2、Mybatis缓存
什么是缓存:
缓存的英文名称叫cache,数据存在内存之中,用于零时存储,但是项目停止或者断电将失去缓存数据。使用缓存可以减少与数据库的交互次数,提高执行效率,但是经常发送改变的数据不推荐缓存,Mybatis也为我们提供了一级缓存和二级缓存
2:一级缓存
一级缓存是存在与SqlSession对象里面的,只要SqlSession没用使用flush或者close,它就会存在缓存
//查询全部学生
@Test
public void findAllStudent() {
//被初始化了一个SqlSession的一级缓存对象
sqlSession = factory.openSession();
//第一个代理对象
StudentDao mapper1 = sqlSession.getMapper(StudentDao.class);
List<Student> students1 = mapper1.findAll();
System.out.println("打印第一个查询出来的hashcode"+students1.hashCode());
//第二个代理对象
StudentDao mapper2 = sqlSession.getMapper(StudentDao.class);
List<Student> students2 = mapper2.findAll();
System.out.println("打印第二个查询出来的hashcode"+students2.hashCode());
System.out.println("比较2个对象是不是同一个");
System.out.println(students1==students2);
}
清除一级缓存:
当SqlSession调用修改update、删除delete、添加insert、commint()、close()、clearCache()等会清除一级缓存
clearCache是清除缓存的方法
//查询全部学生
@Test
public void findAllStudent() {
//被初始化了一个SqlSession的一级缓存对象
sqlSession = factory.openSession();
//第一个代理对象
StudentDao mapper1 = sqlSession.getMapper(StudentDao.class);
List<Student> students1 = mapper1.findAll();
System.out.println("打印第一个查询出来的hashcode"+students1.hashCode());
//清除缓存
sqlSession.clearCache();
//第二个代理对象
StudentDao mapper2 = sqlSession.getMapper(StudentDao.class);
List<Student> students2 = mapper2.findAll();
System.out.println("打印第二个查询出来的hashcode"+students2.hashCode());
System.out.println("比较2个对象是不是同一个");
System.out.println(students1==students2);
}
/**
* 注意 一级缓存SqlSession对象在当前的实例对象SqlSession对象内部起作用
* 什么意思呢?就是说通过factory工厂创建了多个SqlSession对象,它们之间的一级缓存
* 互不干扰,在SqlSessionA的一级缓存里面操作数据增删改及清除缓存都不会影响到
* SqlSessionB的一级缓存
*/
复制代码
二级缓存
二级缓存是存在与mapper映射级别的缓存(如:xxDao.xml),多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的
首先要实现二级缓存是要手都配置属性和标签,否则不会出现二级缓存
注:首先要再SqlMapConfig.xml文件里配置stettings标签
<!--在properties 下面标签配置settings 保证顺序不会错-->
<!--配置settings标签属性 cacheEnabled默认是true 可以不配置-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
++++++++++++++++++++++++++++++++++++++++++
在Teacher实体类序列化 实现Serializable接口
public class Teacher implements Serializable {
.......
}
++++++++++++++++++++++++++++++++++++++++++
在TeacherDao.xml文件中配置相应标签及属性
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.xw.dao.TeacherDao">
<!--一定要配置此属性 代表这个Mapper开启二级缓存-->
<cache></cache>
<!--辅导员关系映射-->
<resultMap id="teacherMapper" type="teacher">
<id column="tid" property="id"></id>
<result column="tname" property="name"></result>
<result column="tsex" property="sex"></result>
<result column="tage" property="age"></result>
<result column="tsalary" property="salary"></result>
<result column="taddress" property="address"></result>
</resultMap>
<!--查询全部辅导员信息--><!--一定要设置useCade="true"-->
<select id="findAll" resultMap="teacherMapper" useCache="true">
select * from teacher;
</select>
<!--查询单个辅导员信息 根据id-->
<select id="findById" parameterType="Integer" resultMap="teacherMapper">
select * from teacher where tid=#{id};
</select>
</mapper>
问题一:为什么要在指定的select语句标签里设置useCache=“true”?
因为在Dao.xml设置
问题二:为什么当前的mapper的数据有缓存后,而查询的hashcode和对象比较都不一样呢?
因为二级缓存和一级缓存不一样,一级缓存是缓存其Map结果,而Map里面包含查询的对象,如{ “key1” , new Student(“小王”,”男”,25)};而二级缓存结果也是Map,但是缓存的是散装数据,如{ “key1” , “小王” },{ “key2” , “小张” }