跳到主要内容 Java 面试核心知识点汇总 | 极客日志
Java java
Java 面试核心知识点汇总 Java 面试涵盖基础语法、JVM 原理、多线程并发、Spring 框架及常用组件等内容。涉及数据类型与封装类、面向对象特性、集合框架差异、字符串处理机制、异常体系分类、引用类型管理、重载重写区别、equals 与 hashCode 规范、IO 流分类、反射原理、类加载机制、垃圾回收算法、线程状态与锁同步、线程池构建策略、Spring IOC 与 AOP 原理、Bean 生命周期、MyBatis 映射配置、分页插件实现及 Spring Boot 自动装配流程。通过问答形式梳理高频考点,辅助开发者掌握核心技术细节与最佳实践。
林间仙子 发布于 2026/3/30 更新于 2026/4/23 0 浏览
1.1. Java 语言有哪些特点
1、简单易学、有丰富的类库
2、面向对象(Java 最重要的特性,让程序耦合度更低,内聚性更高)
3、与平台无关性(JVM 是 Java 跨平台使用的根本)
4、可靠安全
5、支持多线程
1.2. 面向对象和面向过程的区别
面向过程 :是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发,以函数为单位,一步一步完成,后期出现问题可能会牵一发而动全身。
面向对象 :以对象为最小单位是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。但是性能上来说,比面向过程要低。
1.3. 八种基本数据类型的大小,以及他们的封装类 基本类型 大小(字节) 默认值 封装类 byte 1 (byte)0 Byte short 2 (short)0 Short int 4 0 Integer long 8 0L Long float 4 0.0f Float double 8 0.0d Double boolean - false Boolean char 2 \u0000(null) Character
1.int 是基本数据类型,Integer 是 int 的封装类,是引用类型。int 默认值是 0,而 Integer 默认值是 null,所以 Integer 能区分出 0 和 null 的情况。一旦 java 看到 null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
2.基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。虽然定义了 boolean 这种数据类型,但是只对它提供了非常有限的支持。在 Java 虚拟机中没有任何供 boolean 值专用的字节码指令,Java 语言表达式所操作的 boolean 值,在编译之后都使用 Java 虚拟机中的 int 数据类型来代替,而 boolean 数组将会被编码成 Java 虚拟机的 byte 数组,每个元素 boolean 元素占 8 位。这样我们可以得出 boolean 类型占了单独使用是 4 个字节,在数组中又是 1 个字节。使用 int 的原因是,对于当下 32 位的处理器(CPU)来说,一次处理数据是 32 位(这里不是指的是 32/64 位系统,而是指 CPU 硬件层面),具有高效存取的特点。
1.4. 标识符的命名规则。
**标识符的含义:**是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。
**命名规则:(硬性要求)**标识符可以包含英文字母,0-9 的数字,$以及_标识符不能以数字开头标识符不是关键字
**命名规范:(非硬性要求)**类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。方法名规范:同变量名。
1.5. instanceof 关键字的作用 instanceof 严格来说是 Java 中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
boolean result = obj instanceof Class
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果 result 都返回 true,否则返回 false。
注意:编译器会检查 obj 是否能转换成右边的 class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
int i = 0 ;
System.out.println(i instanceof Integer);
System.out.println(i instanceof Object);
Integer integer = new Integer (1 );
System.out.println(integer instanceof Integer);
System.out.println(null instanceof Object);
1.6. Java 自动装箱与拆箱
装箱 就是自动将基本数据类型转换为包装器类型(int–>Integer);调用方法:Integer的valueOf(int) 方法
拆箱 就是自动将包装器类型转换为基本数据类型(Integer–>int)。调用方法:Integer的intValue方法
在 Java SE5 之前,如果要生成一个数值象,必须这样进行:
Integer i = new Integer (10 );
而在从 Java SE5 开始就提供了自动装箱的特性,如果要生成一个数值为 10 的 Integer 对象,只需要这样就可以了:
public class Main {
public static void main (String[] args) {
Integer i1 = 100 ;
Integer i2 = 100 ;
Integer i3 = 200 ;
Integer i4 = 200 ;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
为什么会出现这样的结果?输出结果表明 i1 和 i2 指向的是同一个对象,而 i3 和 i4 指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是 Integer 的 valueOf 方法的具体实现:
public static Integer valueOf (int i) {
if (i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128 ];
else
return new Integer (i);
}
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128 ;
int h = 127 ;
if (integerCacheHighPropValue != null ) {
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127 );
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer [(high - low) + 1 ];
int j = low;
for (int k = 0 ; k < cache.length; k++)
cache[k] = new Integer (j++);
}
private IntegerCache () {}
}
从这 2 段代码可以看出,在通过 valueOf 方法创建 Integer 对象的时候,如果数值在 [-128,127] 之间,便返回指向 IntegerCache.cache 中已经存在的对象的引用;否则创建一个新的 Integer 对象。上面的代码中 i1 和 i2 的数值为 100,因此会直接从 cache 中取已经存在的对象,所以 i1 和 i2 指向的是同一个对象,而 i3 和 i4 则是分别指向不同的对象。
public class Main {
public static void main (String[] args) {
Double i1 = 100.0 ;
Double i2 = 100.0 ;
Double i3 = 200.0 ;
Double i4 = 200.0 ;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
原因:在某个范围内的整型数值的个数是有限的,而浮点数却不是。
1.7. 重载和重写的区别
从字面上看,重写就是重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型 (除过子类中方法的返回值是父类中方法返回值的子类时) 都相同的情况下,对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
public class Father {
public static void main (String[] args) {
Son s = new Son ();
s.sayHello();
}
public void sayHello () {
System.out.println("Hello" );
}
}
class Son extends Father {
@Override
public void sayHello () {
System.out.println("hello by " );
}
}
1.发生在父类与子类之间
2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3.访问修饰符的限制一定要大于被重写方法的访问修饰符 (public>protected>default>private)
4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
public class Father {
public static void main (String[] args) {
Father s = new Father ();
s.sayHello();
s.sayHello("wintershii" );
}
public void sayHello () {
System.out.println("Hello" );
}
public void sayHello (String name) {
System.out.println("Hello" + " " + name);
}
}
1.重载 Overload是一个类中多态性的一种表现
2.重载要求同名方法的参数列表不同 (参数类型,参数个数甚至是参数顺序)
3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
1.8. equals 与==的区别
==比较的是变量 (栈) 内存中存放的对象的 (堆) 内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
1、比较的是操作符两端的操作数是否是同一个对象 。
2、两边的操作数必须是同一类型 的(可以是父子类之间)才能编译通过。
3、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为 true,如:int a=10 与 long b=10L 与 double c=10.0 都是相同的(为 true),因为他们都指向地址为 10 的堆。
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是 Object类中的方法,而 Object中的equals方法返回的却是==的判断。
所有比较是否相等时,都是用 equals并且在对常量相比较时,把常量写在前面,因为使用 object的equals object可能为 null 则空指针在阿里的代码规范中只使用 equals,阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来排查老代码使用 '==',替换成 equals
1.9. Hashcode 的作用
java 的集合有两类,一类是 List,还有一类是 Set。前者有序可重复,后者无序不重复 。当我们在 set 中插入的时候怎么判断是否已经存在该元素呢,可以通过 equals 方法。但是如果元素太多,用这样的方法就会比较满。于是有人发明了哈希算法来提高集合中查找元素的效率。这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。hashCode 方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值 。这样一来,当集合要添加新的元素时,先调用这个元素的 hashCode 方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的 equals 方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次。
1.10. String、String StringBuffer 和 StringBuilder 的区别是什么?
String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个 final 类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对 String的操作都会生成新的 String对象。
private final char value[];
每次 + 操作:隐式在堆上 new 了一个跟原字符串相同的 StringBuilder对象,再调用 append方法拼接 + 后面的字符。StringBuffer和 StringBuilder他们两都继承了 AbstractStringBuilder抽象类,从 AbstractStringBuilder抽象类中我们可以看到
他们的底层都是可变的字符数组 ,所以在进行频繁的字符串操作时,建议使用 StringBuffer和 StringBuilder来进行操作。另外 StringBuffer对方法加了同步锁或者对调用的方法加了同步锁 ,所以是线程安全 的。StringBuilder并没有对方法进行加同步锁 ,所以是非线程安全的 。
1.11. ArrayList 和 linkedList 的区别
Array(数组)是基于索引 (index) 的数据结构,它使用索引在数组中搜索和读取数据是很快的。Array 获取数据的时间复杂度是 O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据,(因为删除数据以后,需要把后面所有的数据前移)
缺点 : 数组初始化必须指定初始化的长度,否则报错
int [] a = new int [4 ];
int c[] = {23 ,43 ,56 ,78 };
List—是一个有序的集合 ,可以包含重复 的元素,提供了按索引访问的方式,它继承 Collection。List有两个重要的实现类:ArrayList和 LinkedList。ArrayList: 可以看作是能够自动增长容量的数组。ArrayList的 toArray方法返回一个数组。ArrayList的 asList方法返回一个列表。ArrayList底层的实现是 Array, 数组扩容实现。LinkList是一个双链表,在添加和删除元素时具有比 ArrayList更好的性能。但在 get与 set方面弱于 ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。
1.12. HashMap 和 HashTable 的区别
1、两者父类不同 HashMap是继承自 AbstractMap类,而 Hashtable是继承自 Dictionary类。不过它们都实现了同时实现了 map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2、对外提供的接口不同 Hashtable比 HashMap多提供了 elements() 和 contains()两个方法。elements()方法继承自 Hashtable的父类 Dictionnary。elements()方法用于返回此 Hashtable中的 value 的枚举。contains()方法判断该 Hashtable是否包含传入的 value。它的作用与 containsValue()一致。事实上,contansValue()就只是调用了一下 contains()方法。
3、对 null 的支持不同 Hashtable:key和 value都不能为 null。HashMap:key可以为 null,但是这样的 key 只能有一个,因为必须保证 key 的唯一性;可以有多个 key 值对应的 value为 null。
4、安全性不同 HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己处理多线程的安全问题。Hashtable是线程安全的,它的每个方法上都有 synchronized关键字,因此可直接用于多线程中。虽然 HashMap是线程不安全的,但是它的效率远远高于 Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的 ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比 Hashtable要高好多倍。因为 ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5、初始容量大小和每次扩充容量大小不同 16 11 6、计算 hash 值的方法不同 hashmap 通过 hashcode 计算出它的 hash 值然后向右移动 16 位之后在进行异或操作 hashtable 直接通过 hashcode 计算 7.是否包含 contains,hashmap 不包含,hashtable 包含
1.13. Collection 包结构,与 Collections 的区别 Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set;Collections 是集合类的一个帮助类,它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于 Java 的 Collection框架。
1.14. Java 的四种引用,强弱软虚 强引用: 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
String str = new String ("str" );
软引用 软引用在程序内存不足时,会被回收,使用方式:
SoftReference<String> wrf = new SoftReference <String>(new String ("str" ));
**可用场景:**创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM 就会回收早先创建的对象。
弱引用 弱引用就是只要 JVM 垃圾回收器发现了它,就会将之回收,使用方式:
WeakReference<String> wrf = new WeakReference <String>(str);
可用场景 Java 源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM 会自动帮我处理它,这样我就不需要做其它操作。
虚引用 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue中。注意哦,其它引用是被 JVM 回收后才被传入 ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue,使用
PhantomReference<String> prf = new PhantomReference <String>(new String ("str" ), new ReferenceQueue <>());
可用场景:对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference的四个子类的引用 (SoftReference等)。
1.15. 泛型常用特点 泛型是 Java SE 1.5 之后的特性,《Java 核心技术》中对泛型的定义是:
'泛型'意味着编写的代码可以被不同类型的对象所重用。
'泛型',顾名思义,'泛指的类型'。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的 ArrayList 就是个泛型类,ArrayList 作为集合可以存放各种元素,如 Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放 Integer 类型的元素,如
List<Integer> iniData = new ArrayList <>();
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了 Object 即可,添加的数据全部都可向上转型为 Object。更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
1.16. Java 创建对象有几种方式?
new 创建新对象
通过反射机制
采用 clone 机制
通过序列化机制
1.17. 有没有可能两个不相等的对象有相同的 hashcode 有可能。在产生 hash冲突时,两个不相等的对象就会有相同的 hashcode值。当 hash冲突产生时,一般有以下几种方式来处理:
拉链法 :每个哈希表节点都有一个 next 指针,多个哈希表节点可以用 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储。
开放定址法 :一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
再哈希 :又叫双哈希法 ,有多个不同的 Hash 函数。当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突。
1.18. 深拷贝和浅拷贝的区别是什么?
浅拷贝 :被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝 :被复制对象的所有变量都含有与原来的对象相同的值。而那些引用其他对象的变量将指向被复制过的新对象。而不再是原有的那些被引用的对象。换言之。深拷贝把要复制的对象所引用的对象都复制了一遍。
1.19. final 有哪些用法? final 也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下 5 点就不错了:
1.被 final 修饰的类不可以被继承
2.被 final 修饰的方法不可以被重写
3.被 final 修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。
4.被 final 修饰的方法,JVM 会尝试将其内联,以提高运行效率
5.被 final 修饰的常量,在编译阶段会存入常量池中。
除此之外,编译器对 final 域要遵守的两个重排序规则更好:
在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
1.20. static 都有哪些用法? 所有的人都知道 static 关键字这两个基本的用法:静态变量和静态方法。也就是被 static 所修饰的变量/方法都属于类的静态资源,类实例所共享。
除了静态变量和静态方法之外,static 也用于静态块,多用于初始化操作:
public class PreCache {
static {
}
}
此外 static 也多用于修饰内部类,此时称之为静态内部类。最后一种用法就是静态导包,即 import static .import static 是在 JDK 1.5 之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如:
import static java.lang.Math.*;
public class Test {
public static void main (String[] args) {
System.out.println(sin(20 ));
}
}
1.21. 3*0.1==0.3 返回值是什么 false,因为有些浮点数不能完全精确的表示出来。
1.22. a=a+b 与 a+=b 有什么区别吗? += 操作符会进行隐式自动类型转换,此处 a+=b 隐式的将加操作的结果类型强制转换为持有结果的类型,而 a=a+b 则不会自动进行类型转换。如:
byte a = 127 ;
byte b = 127 ;
b = a + b;
b += a;
short s1= 1 ;
s1 = s1 + 1 ;
有错误。short类型在进行运算时会自动提升为 int 类型,也就是说 s1+1 的运算结果是 int 类型,而 s1 是 short类型,此时编译器会报错。正确写法:
+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错。
1.23. try catch finally,try 里有 return,finally 还执行么? 执行,并且 finally的执行早于 try里面的 return结论:
1、不管有木有出现异常,finally块中代码都会执行;
2、当 try和 catch中有 return时,finally仍然会执行;
3、finally是在 return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管 finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在 finally执行前确定的;
4、finally中最好不要包含 return,否则程序会提前退出,返回值不是 try或 catch中保存的返回值
1.24. Excption 与 Error 包结构 Java 可抛出 (Throwable) 的结构分为三种类型:被检查的异常 (CheckedException),运行时异常 (RuntimeException),错误 (Error)。
1、运行时异常 定义:RuntimeException及其子类都被称为运行时异常。特点:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过 throws 声明抛出它",也"没有用 try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的 ArithmeticException异常,数组越界时产生的 IndexOutOfBoundsException异常,fail-fast机制产生的 ConcurrentModificationException异常(java.util包下面的所有的集合类都是快速失败的,'快速失败'也就是 fail-fast,它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException异常,从而产生 fail-fast 机制,这个错叫并发修改异常。Fail-safe,java.util.concurrent包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出 ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是 ConcurrentHashMap迭代器弱一致的表现。ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与 Hashtable和同步的 HashMap一样了。)等,都属于运行时异常。
ClassCastException(类转换异常)
IndexOutOfBoundsException(数组越界)
NullPointerException(空指针异常)
ArrayStoreException(数据存储异常,操作数组是类型不一致)
BufferOverflowException
2、被检查异常 定义:Exception类本身,以及 Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。特点 : Java 编译器会检查它。此类异常,要么通过 throws进行声明抛出,要么通过 try-catch 进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过 clone() 接口去克隆一个对象,而该对象对应的类没有实现 Cloneable接口,就会抛出 CloneNotSupportedException异常。被检查异常通常都是可以恢复的。如:IOException FileNotFoundException SQLException 被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的 FileNotFoundException。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的 NullPointerException。
3、错误 定义 : Error 类及其子类。特点 :**和运行时异常一样,编译器也不会对错误进行检查。**当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。OutOfMemoryError、ThreadDeath。Java 虚拟机规范规定 JVM 的内存分为了好几块,比如堆,栈,程序计数器,方法区等
1.25. OOM 你遇到过哪些情况,SOF 你遇到过哪些情况
1,OutOfMemoryError 异常 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError(OOM)异常的可能。Java Heap 溢出:一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。java 堆用于存储对象实例,我们只要不断的创建对象,并且保证 GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。出现这种异常,一般手段是先通过内存映像分析工具 (如 Eclipse Memory Analyzer)对 dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏 (Memory Leak)还是内存溢出 (Memory Overflow)。如果是内存泄漏,可进一步通过工具查看泄漏对象到 GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与 GC Roots相关联并导致垃圾收集器无法自动回收。如果不存在泄漏,那就应该检查虚拟机的参数 (-Xmx 与-Xms) 的设置是否适当 。
2,虚拟机栈和本地方法栈溢出 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError异常这里需要注意当栈的大小越大可分配的线程数就越少 。
3,运行时常量池溢出 异常信息 :java.lang.OutOfMemoryError:PermGenspace如果要向运行时常量池中添加内容,最简单的做法就是使用 String.intern()这个 Native 方法。该方法的作用是:如果池中已经包含一个等于此 String 的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。由于常量池分配在方法区内,我们可以通过 -XX:PermSize 和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
4,方法区溢出 方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的 class 对象没有被及时回收掉或者 class 信息占用的内存超过了我们配置。
异常信息: java.lang.OutOfMemoryError:PermGenspace方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量 Class 的应用中,要特别注意这点。
SOF(堆栈溢出 StackOverflow):StackOverflowError的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。因为栈一般默认为 1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过 1m 而导致溢出。栈溢出的原因 :递归调用,大量循环或死循环,全局变量是否过多,数组、List、map 数据过大。
1.26. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输 入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
1.27. 线程有哪些基本状态?这些状态是如何定义的?
新建 (new) :新创建了一个线程对象。
可运行 (runnable) :线程对象创建后,其他线程 (比如 main 线程)调用了该对象的 start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。 运行 (running) :可运行状态 (runnable) 的线程获得了 cpu 时间片(timeslice),执行程序代码。 阻塞 (block) :阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行 (runnable) 状态,才有机会再次获得 cpu timeslice 转到运行 (running) 状态。
阻塞的情况分三种:
(一). 等待阻塞:运行 (running) 的线程执行 o.wait()方法,JVM 会把该线程放 入等待队列 (waitting queue) 中。
(二). 同步阻塞:运行 (running) 的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池 (lock pool) 中。
(三). 其他阻塞:运行 (running) 的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时 join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行 (runnable) 状态。
死亡 (dead) :线程 run()、main()方法执行结束,或者因异常退出了 run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
1.28. Java 中 IO 流
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系,Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。按操作方式分类结构图:
1.29. java 反射的作用于原理 1、定义:反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在 java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
Class.forName('com.mysql.jdbc.Driver.class' );
这就是反射。如 hibernate,struts 等框架使用反射实现的。3、反射的实现方式:第一步:获取 Class 对象,有 4 中方法:
1)Class.forName('类的路径');
2)类名.class
3)对象名.getClass()
4)基本类型的包装类,可以调用包装类的 Type 属性来获得该包装类的 Class 对象
1)Class :表示正在运行的 Java 应用程序中的类和接口 注意:所有获取对象的信息都需要 Class 类来实现。
2)Field :提供有关类和接口的属性信息,以及对它的动态访问权限。
3)Constructor :提供关于类的单个构造方法的信息以及它的访问权限
4)Method :提供类或接口中某个方法的信息
1)能够运行时动态获取类的实例,提高灵活性;
2)与动态编译结合
1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。解决方案:1、通过 setAccessible(true)关闭 JDK 的安全检查来提升反射速度;
2、多次创建一个类的实例时,有缓存会快很多
3、ReflectASM工具类,通过字节码生成的方式加快反射速度
2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
1.30. 说说 List,Set,Map 三者的区别?
List(对付顺序的好帮手) :List 接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 Set(注重独一无二的性质) : 不允许重复的集合。不会有多个元素引用相同的对象。 Map(用 Key 来搜索的专家) : 使用键值对存储。Map 会维护与 Key 有关联的值。两个 Key 可以引用相同的对象,但 Key 不能重复,典型的 Key 是 String 类型,但也可以是任何对象。
二、JVM 篇
2.1. 知识点汇总 JVM 是 Java 运行基础,面试时一定会遇到 JVM 的有关问题,内容相对集中,但对只是深度要求较高
2.2. 知识点详解:
1、**JVM 内存模型:**线程独占:栈,本地方法栈,程序计数器 线程共享:堆,方法区
2、**栈:**又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法出口等信息。调用方法时执行入栈,方法返回式执行出栈。
3、本地方法栈 与栈类似,也是用来保存执行方法的信息。执行 Java 方法是使用栈,执行 Native 方法时使用本地方法栈。
4、程序计数器 保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行 Java 方法服务,执行 Native 方法时,程序计数器为空。
5、堆 JVM 内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆没有可用空间时,会抛出 OOM 异常。根据对象的存活周期不同,JVM 把对象进行分代管理,由垃圾回收器进行垃圾的回收管理
6、**方法区:**又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据。1.7 的永久代和 1.8 的元空间都是方法区的一种实现
7、JVM 内存可见性 JMM 是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作。由于指令重排序,读写的顺序会被打乱,因此 JMM 需要提供原子性,可见性,有序性保证
2.3. 类加载与卸载
其中验证,准备,解析 合称链接
加载 通过类的完全限定名,查找此类字节码文件,利用字节码文件创建 Class 对象。
验证 确保 Class 文件符合当前虚拟机的要求,不会危害到虚拟机自身安全。
准备 进行内存分配,为 static 修饰的类变量分配内存,并设置初始值 (0 或 null)。不包含 final 修饰的静态变量,因为 final 变量在编译时分配。
解析 将常量池中的符号引用替换为直接引用的过程。直接引用为直接指向目标的指针或者相对偏移量等。
初始化 主要完成静态块执行以及静态变量的赋值。先初始化父类,再初始化当前类。只有对类主动使用时才会初始化。
触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用 Class.forName反射类的时候,或者某个子类初始化的时候。Java 自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸。
1、加载机制 - 双亲委派模式 双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器。父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载。优点:
1.避免类的重复加载
2.避免 Java 的核心 API 被篡改
2、分代回收 分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间。年轻代->标记 - 复制 老年代->标记 - 清除
a、G1 算法 1.9 后默认的垃圾回收算法,特点保持高回收率的同时减少停顿。采用每次只清理一部分,而不是清理全部的增量式清理,以保证停顿时间不会过长。其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域 (region),一部分用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区。同 CMS 相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间。
**年轻代回收:**并行复制采用复制算法,并行收集,会 StopTheWorld.
**老年代回收:**会对年轻代一并回收 初始标记完成堆 root 对象的标记,会 StopTheWorld. 并发标记 GC 线程和应用线程并发执行。最终标记完成三色标记周期,会 StopTheWorld. 复制/清楚会优先对可回收空间加大的区域进行回收
b、ZGC 算法 前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理 TB 级别的堆,可以做到 10ms 以下的回收停顿时间
着色指针 读屏障 并发处理 基于 region 内存压缩 (整理)
roots 标记 :标记 root 对象,会 StopTheWorld.
并发标记 :利用读屏障与应用线程一起运行标记,可能会发生 StopTheWorld. 清除会清理标记为不可用的对象。roots 重定位:是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生。重定位最开始会 StopTheWorld,却决于重定位集与对象总活动集的比例。并发重定位与并发标记类似。
2.4. 简述一下 JVM 的内存模型 1.JVM 内存模型简介 JVM 定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着 JVM 启动及销毁,另外一些区域的数据是线程性独立的,随着线程创建和销毁
线程私有区
1、程序计数器 当同时进行的线程数超过 CPU 数或其内核数时,就要通过时间片轮询分派 CPU 的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是 JAVA 方法,计数器记录正在执行的 java 字节码地址,如果执行的是 native方法,则计数器为空。
2、虚拟机栈 线程私有的,与线程在同一时间创建。管理 JAVA 方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss 参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出 stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出 OutofMemoryError。
3、本地方法栈 与虚拟机栈作用相似。但它不是为 Java 方法服务的,而是本地方法(C 语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。
1、方法区 线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代码。若要分代,算是永久代(老年代),以前类大多"static"的,很少被卸载或收集,现回收废弃常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果 hotspot 虚拟机确定一个类的定义信息 不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的 ClassLoader 被回收)
2、堆 存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的 Eden 区中,经过 GC 后进入新生代的 S0 区中,再经过 GC 进入新生代的 S1 区中,15 次 GC 后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则 OutOfMemoryError。
2.5. 堆和栈的区别 栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。
1、功能不同 栈内存用来存储局部变量和方法调用,而堆内存用来存储 Java 中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同 栈内存是线程私有的。堆内存是所有线程共有的。
3、异常错误不同 如果栈内存或者堆内存不足都会抛出异常。栈空间不足:java.lang.StackOverFlowError。堆空间不足:java.lang.OutOfMemoryError。
4、空间大小 栈的空间大小远远小于堆的
2.6. 什么时候会触发 FullGC 除直接调用 System.gc外,触发 Full GC 执行的情况有如下四种。
1.旧生代空间不足 旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行 Full GC 后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space为避免以上两种状况引起的 FullGC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2.Permanet Generation 空间满 PermanetGeneration中存放的为一些 class 的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space为避免 Perm Gen 占满造成 Full GC 现象,可采用的方法为增大 Perm Gen 空间或转为使用 CMS GC。
3.CMS GC 时出现 promotion failed 和 concurrent mode failure 对于采用 CMS 进行旧生代 GC 的程序而言,尤其要注意 GC 日志中是否有 promotion failed 和 concurrent mode failure两种状况,当这两种状况出现时可能会触发 Full GC。promotionfailed 是在进行 Minor GC 时,survivor space 放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。应对措施为 :增大 survivorspace、旧生代空间或调低触发并发 GC 的比率,但在 JDK 5.0+、6.0+的版本中有可能会由于 JDK 的 bug29 导致 CMS 在 remark 完毕后很久才触发 sweeping 动作 。对于这种状况,可通过设置 -XX:CMSMaxAbortablePrecleanTime=5(单位为 ms)来避免。
4.统计得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间 这是一个较为复杂的触发情况,Hotspot 为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行 Minor GC 时,做了一个判断,如果之前统计所得到的 Minor GC 晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发 Full GC。例如程序第一次触发 MinorGC 后,有 6MB 的对象晋升到旧生代,那么当下一次 Minor GC 发生时,首先检查旧生代的剩余空间是否大于 6MB,如果小于 6MB,则执行 Full GC。当新生代采用 PSGC 时,方式稍有不同,PS GC 是在 Minor GC 后也会检查,例如上面的例子中第一次 Minor GC 后,PS GC 会检查此时旧生代的剩余空间是否大于 6MB,如小于,则触发对旧生代的回收。除了以上 4 种状况外,对于使用 RMI 来进行 RPC 或管理的 Sun JDK 应用而言,默认情况下会一小时执行一次 Full GC。可通过在启动时通过 -java Dsun.rmi.dgc.client.gcInterval=3600000来设置 Full GC 执行的间隔时间或通过 -XX:+ DisableExplicitGC来禁止 RMI 调用 System.gc。
2.7. 什么是 Java 虚拟机?为什么 Java 被称作是'平台无关的编程语言'? Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
2.8. Java 内存结构 方法区和对是所有线程共享的内存区域;而 java 栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
Java 堆(Heap) ,是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区(Method Area) ,方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register) ,程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
JVM 栈(JVM Stacks) ,与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(Native Method Stacks) ,本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。
2.9. 对象分配规则
对象优先分配在 Eden 区,如果 Eden 区没有足够的空间时,虚拟机执行一次 Minor GC。大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次 Minor GC 那么对象会进入 Survivor 区,之后每经过一次 Minor GC 那么对象的年龄加 1,知道达到阀值对象进入老年区。动态判断对象的年龄。如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。空间分配担保。每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC,如果小于检查 HandlePromotionFailure 设置,如果 true 则只进行 Monitor GC,如果 false 则进行 Full GC。
2.10. Java 对象创建过程
1.JVM 遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)
2.为对象分配内存。一种办法'指针碰撞'、一种办法'空闲列表',最终常用的办法'本地线程缓冲分配 (TLAB)'
3.将除对象头外的对象内存空间初始化为 0
4.对对象头进行必要设置
2.11. 类的生命周期 类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载 ,其中前三部是类的加载的过程加载,查找并加载类的二进制数据,在 Java 堆中也创建一个 java.lang.Class类的对象连接,连接又包含三块内容:验证、准备、初始化。
1)验证,文件格式、元数据、字节码、符号 引用验证;
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3)解析,把类中的符号引用转换为直接引用初始化,为类的静态变量赋予正确的初始值使用,new 出对象程序中使用卸载,执行垃圾回收
2.12. 简述 Java 的对象结构 Java 对象由三个部分组成:对象头、实例数据、对齐填充。对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC 分代年龄、锁标识状态、线程持有的锁、偏向线程 ID(一般占 32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)对齐填充:JVM 要求对象起始地址必须是 8 字节的整数倍(8 字节对齐)
2.13. 如何判断对象可以被回收?
引用计数 :每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis) :从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
2.14. JVM 的永久代中会发生垃圾回收么? 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收 (Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。请参考下 Java8:从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区)
2.15. 垃圾收集算法 GC 最基础的算法有三种:标记 - 清除算法、复制算法、标记 - 压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
标记 - 清除算法 ,'标记 - 清除'(Mark-Sweep)算法,如它的名字一样,算法分为'标记'和'清除'两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
复制算法 ,'复制'(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
标记 - 压缩算法 ,标记过程仍然与'标记 - 清除'算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法 ,'分代收集'(Generational Collection)算法,把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
2.16. 调优命令有哪些? Sun JDK 监控和故障处理命令有 jps jstat jmap jhat jstack jinfo
jps,JVM Process Status Tool ,显示指定系统内所有的 HotSpot 虚拟机进程。 jstat,JVM statistics Monitoring 是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。 jmap ,JVM Memory Map 命令用于生成 heap dump 文件 jhat ,JVM Heap Analysis Tool 命令是与 jmap 搭配使用,用来分析 jmap 生成的 dump,jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 的分析结果后,可以在浏览器中查看 jstack ,用于生成 java 虚拟机当前时刻的线程快照。 jinfo ,JVM Configuration info这个命令作用是实时查看和调整虚拟机运行参数。
2.17. 调优工具 常用调优工具分为两类,jdk 自带监控工具:jconsole 和 jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console 是从 java5 开始,在 JDK 中自带的 java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控 jvisualvm,jdk 自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC 变化等。 MAT,Memory Analyzer Tool,一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗 GChisto,一款专业分析 gc 日志的工具
2.18. Minor GC 与 Full GC 分别在什么时候发生? 新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC
2.19. 你知道哪些 JVM 性能调优 设定新生代大小 。新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小 -XX:NewRatio 新生代和老生代占比 -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用 -XX:+UseConcMarkSweepGC
2.20. 简述 Java 垃圾回收机制? 在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
2.21. 什么是类加载器,类加载器有哪些? 实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。主要有一下四种类加载器:
1.启动类加载器 (Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。
2.扩展类加载器 (extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3.系统类加载器 (system class loader):它根据 Java 应用的类路径 (CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
4.用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
2.22. 你有没有遇到过 OutOfMemory 问题?你是怎么来处理这个问题的?处理过程中有哪些收获?
内存加载的数据量太大:一次性从数据库取太多数据;集合类中有对对象的引用,使用后未清空,GC 不能进行回收;代码中存在循环产生过多的重复对象;启动参数堆内存值小。
2.23. JDK 1.8 之后 Perm Space 有哪些变动?MetaSpace⼤⼩默认是⽆限的么?还是你们会通过什么⽅式来指定⼤⼩? JDK 1.8 后用元空间替代了 Perm Space;字符串常量存放到堆内存中。MetaSpace 大小默认没有限制,一般根据系统内存的大小。JVM 会动态改变此值。 -XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小(Oracle 逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize的值设置的过大会延长垃圾回收时 间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。 -XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发 Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM 会动态地改变此值。
三、多线程&并发篇
3.1. Java 中实现多线程有几种方法
继承 Thread类;实现 Runnable接口;实现 Callable接口通过 FutureTask包装器来创建 Thread 线程;使用 ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了 ExecutorService来管理前面的三种方式)
3.2. 如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当 run方法完成后线程终止。
2、使用 stop方法强行终止,但是不推荐这个方法,因为 stop和suspend及 resume一样都是过期作废的方法。
3、使用 interrupt方法中断线程。
class MyThread extends Thread {
volatile boolean stop = false ;
public void run () {
while (!stop) {
System.out.println(getName() + " is running" );
try {
sleep(1000 );
} catch (InterruptedException e) {
System.out.println("week up from blcok..." );
stop = true ;
}
}
System.out.println(getName() + " is exiting..." );
}
}
class InterruptThreadDemo3 {
public static void main (String[] args) throws InterruptedException {
MyThread m1 = new MyThread ();
System.out.println("Starting thread..." );
m1.start();
Thread.sleep(3000 );
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true ;
m1.interrupt();
Thread.sleep(3000 );
System.out.println("Stopping application..." );
}
}
3.3. notify() 和 notifyAll() 有什么区别?
notify可能会导致死锁,而 notifyAll则不会任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized中的代码使用 notifyall,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify只能唤醒一个。 wait()应配合 while循环使用,不应使用 if,务必在 wait()调用前后都检查条件,如果不满足,必须调用 notify()唤醒另外的线程来处理,自己继续 wait()直至条件满足再往下执行 notify()是对 notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续 notify()下一个线程,并且自身需要重新回到 WaitSet中
3.4. sleep() 和 wait() 有什么区别? 对于 sleep()方法,我们首先要知道该方法是属于 Thread类中的。而 wait()方法,则是属于 Object类中的。 sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用 sleep()方法的过程中,线程不会释放对象锁。当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
3.5. volatile 是什么?可以保证有序性吗? 一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile 关键字会强制将修改的值立即写入主存。
2)禁止进行指令重排序。
volatile不是原子性操作什么叫保证部分有序性?当程序执行到 volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
x = 2 ;
y = 0 ;
flag = true ;
x = 4 ;
y = -1 ;
由于 flag变量为 volatile变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1、语句 2 前面,也不会讲语句 3 放到语句 4、语句 5 后面。但是要注意语句 1 和语句 2 的顺序、语句 4 和语句 5 的顺序是不作任何保证的。使用 Volatile一般用于状态标记量和单例模式的双检锁
3.6. Thread 类中的 start() 和 run() 方法有什么区别? start()方法被用来启动新创建的线程,而且 start()内部调用了 run()方法,这和直接调用 run()方法的效果不一样。当你调用 run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
3.7. 为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面?
明显的原因是 JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的 wait()方法就有意义了。如果 wait()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
3.8. 为什么 wait 和 notify 方法要在同步块中调用?
1.只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的 wait(),notify()和 notifyAll()方法。
2.如果你不这么做,你的代码会抛出 IllegalMonitorStateException异常。
3.还有一个原因是为了避免 wait 和 notify之间产生竞态条件。
wait()方法强制当前线程释放对象锁。这意味着在调用某对象的 wait() 方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的 wait()方法。在调用对象的 notify()和 notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的 notify()或 notifyAll()方法。调用 wait()方法的原因通常是,调用线程希望某个特殊的状态 (或变量) 被设置之后再继续执行。调用 notify()或 notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:'特殊状态已经被设置'。这个状态作为线程间通信的通道,它必须是一个可变的共享状态 (或变量)。
3.9. Java 中 interrupted 和 isInterruptedd 方法的区别? interrupted()和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会 。Java 多线程的中断机制是用内部标识来实现的,调用 Thread.interrupt()来中断一个线程就会设置中断标识为 true。当中断线程调用静态方法 Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法 isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出 InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。
3.10. Java 中 synchronized 和 ReentrantLock 有什么不同?
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
这两种方式最大区别就是对于 Synchronized来说,它是 java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现。而 ReentrantLock它是 JDK 1.5 之后提供的 API 层面的互斥锁,需要 lock()和 unlock()方法配合 try/finally语句块来完成。 Synchronized进过编译,会在同步块的前后分别形成 monitorenter和 monitorexit这个两个字节码指令。在执行 monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加 1,相应的,在执行 monitorexit指令时会将锁计算器就减 1,当计算器为 0 时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。由于 ReentrantLock是 java.util.concurrent包下提供的一套互斥锁,相比 Synchronized,ReentrantLock类提供了一些高级功能,主要有以下 3 项:
1.等待可中断 ,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避免出现死锁的情况。
2.公平锁 ,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件 ,一个 ReentrantLock对象可以同时绑定对个对象。
3.11. 有三个线程 T1,T2,T3,如何保证顺序执行? 在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的 join() 方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个 (T3 调用 T2,T2 调用 T1),这样 T1 就会先完成而 T3 最后完成。实际上先启动三个线程中哪一个都行,因为在每个线程的 run 方法中用 join 方法限定了三个线程的执行顺序
public class JoinTest2 {
public static void main (String[] args) {
final Thread t1 = new Thread (new Runnable () {
@Override
public void run () {
System.out.println("t1" );
}
});
final Thread t2 = new Thread (new Runnable () {
@Override
public void run () {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2" );
}
});
Thread t3 = new Thread (new Runnable () {
@Override
public void run () {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3" );
}
});
t3.start();
t2.start();
t1.start();
}
}
3.12. SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap()和 Hashtable一样,实现上在调用 map 所有方法时,都对整个 map 进行同步。而 ConcurrentHashMap的实现却更加精细,它对 map 中的所有桶加了锁。所以,只要有一个线程访问 map,其他线程就无法进入 map,而如果一个线程在访问 ConcurrentHashMap某个桶时,其他线程,仍然可以对 map 执行某些操作。所以,ConcurrentHashMap在性能以及安全性方面,明显比 Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历 map 时,如果其他线程试图对 map进行数据修改,也不会抛出 ConcurrentModificationException。
3.13. 什么是线程安全
线程安全就是说多线程访问同一代码,不会产生不确定的结果。在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。线程安全一般都涉及到 synchronized,就是一段代码同时只能有一个线程来操作不然中间过程可能会产生不可预制的结果。如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
3.14. Thread 类中的 yield 方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行 yield()的线程有可能在进入到暂停状态后马上又被执行
3.15. Java 线程池中 submit() 和 execute() 方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在 Executor接口中,而 submit()方法可以返回持有计算结果的 Future对象,它定义在 ExecutorService接口中,它扩展了 Executor接口,其它线程池类像 ThreadPoolExecutor和 ScheduledThreadPoolExecutor都有这些方法。
3.16. 说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性 ,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在 Java 早期版本中,synchronized属于 重量级锁,效率低下 ,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized较大优化,所以现在的 synchronized锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁 等技术来减少锁操作的开销。
3.17. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 synchronized 关键字最主要的三种使用方式:
修饰实例方法 : 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法 : 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以如果一个线程 A 调用一个实例对象的非静态 synchronized方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块 : 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结 : synchronized关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a)因为 JVM 中,字符串常量池具有缓存功能
3.18. 什么是线程安全?Vector 是一个线程安全类吗? 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可 以将集合类分 成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的,而和它相似的 ArrayList 不是线程安全的。
3.19. volatile 关键字的作用? 一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2.禁止进行指令重排序。
3.volatile本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
4.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
5.volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
6.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
3.20. 常用的线程池有哪些?
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
3.21. 简述一下你对线程池的理解 (如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理利用线程池能够带来三个好处。
第一:降低资源消耗 。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度 。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性 。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3.22. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 synchronized 关键字最主要的三种使用方式:
修饰实例方法 ,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 修饰静态方法 ,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
**修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。**和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized 关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓冲功能!
下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。面试中面试官经常会说:'单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单利模式的原理呗!'
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton () {}
public static Singleton getUniqueInstance () {
if (uniqueInstance == null ) {
synchronized (Singleton.class) {
if (uniqueInstance == null ) {
uniqueInstance = new Singleton ();
}
}
}
return uniqueInstance;
}
}
另外,需要注意 uniqueInstance采用 volatile关键字修饰也是很有必要。 uniqueInstance采用 volatile关键字修饰也是很有必要的, uniqueInstance = new Singleton();这段代码其实是分为三步执行:
1.为 uniqueInstance 分配内存空间
2.初始化 uniqueInstance
3.将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance()后发现 uniqueInstance不为空,因此返回 uniqueInstance,但此时 uniqueInstance还未被初始化。使用 volatile可以禁止 JVM 的指令重排 ,保证在多线程环境下也能正常运行。
3.23. 讲一下 synchronized 关键字的底层原理 synchronized 关键字底层原理属于 JVM 层面。① synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method () {
synchronized (this ) {
System.out.println("synchronized 代码块" );
}
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java命令生成编译后的 .class文件,然后执行 javap -c -s -v -l SynchronizedDemo.class。
synchronized同步语句块的实现使用的是 monitorenter和 monitorexit指令,其中 monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因) 的持有权。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
public class SynchronizedDemo2 {
public synchronized void method () {
System.out.println("synchronized 方法" );
}
}
synchronized 修饰的方法并没有 monitorenter指令和 monitorexit指令,取得代之的确实是 ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
3.24. 为什么要用线程池? 线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
降低资源消耗 。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度 。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性 。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3.25. 实现 Runnable 接口和 Callable 接口的区别 如果想让线程池执行任务的话需要实现的 Runnable接口或 Callable接口。Runnable 接口或 Callable 接口实现类都可以被 ThreadPoolExecutor或 ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable接口不会返回结果但是 Callable接口可以返回结果。备注:工具类 Executors可以实现 Runnable对象和 Callable对象之间的相互转换。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object resule))。
3.26. 执行 execute() 方法和 submit() 方法的区别是什么呢?
(1) execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
(2)submit()方法用于提交需要返回值的任务。线程池会返回一个 future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过 future的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
3.27. 如何创建线程池 方式二:通过 Executor 框架的工具类 Executors 来实现 我们可以创建三种类型的 ThreadPoolExecutor:
FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
对应 Executors 工具类中的方法如图所示:
四、Spring 篇
4.1. Spring 的 IOC 和 AOP 机制? 我们是在使用 Spring 框架的过程中,其实就是为了使用 IOC,依赖注入,和 AOP,面向切面编程,这两个是 Spring 的灵魂
主要用到的设计模式有工厂模式和代理模式。IOC 就是典型的工厂模式,通过 sessionfactory 去注入实例。AOP 就是典型的代理模式的体现。代理模式是常用的 java 设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。
spring 的 IoC 容器是 spring 的核心,spring AOP 是 spring 框架的重要组成部分。在传统的程序设计中,当调用者需要被调用者的协助时,通常由调用者来创建被调用者的实例。但在 spring 里创建被调用者的工作不再由调用者来完成,因此控制反转(IoC);创建被调用者实例的工作通常由 spring 容器来完成,然后注入调用者,因此也被称为依赖注入(DI),依赖注入和控制反转是同一个概念的不同角度的描述。面向方面编程(AOP)是以另一个角度来考虑程序结构,通过分析程序结构的关注点来完善面向对象编程(OOP)。OOP 将应用程序分解成各个层次的对象,而 AOP 将程序分解成多个切面。spring AOP 只实现了方法级别的连接点,在 J2EE 应用中,AOP 拦截到方法级别的操作就已经足够。在 spring 中,未来使 IoC 方便地使用健壮、灵活的企业服务,需要利用 spring AOP 实现为 IoC 和企业服务之间建立联系。
IOC:控制反转也叫依赖注入。利用了工厂模式将对象交给容器管理,你只需要在 spring 配置文件总配置相应的 bean,以及设置相关的属性,让 spring 容器来生成类的实例对象以及管理对象。在 spring 容器启动的时候,spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类(假设这个类名是 A),分配的方法就是调用 A 的 setter 方法来注入,而不需要你在 A 里面 new 这些 bean 了。
AOP:面向切面编程。(Aspect-Oriented Programming)AOP 可以说是对 OOP 的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP 则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。实现 AOP 的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建'方面',从而使得编译器可以在编译期间织入有关'方面'的代码.
简单点解释,比方说你想在你的 biz 层所有类中都加上一个打印'你好'的功能,这时就可以用 aop 思想来做。你先写个类写个类方法,方法经实现打印'你好',然后 Ioc 这个类 ref='biz. *'让每个类都注入即可实现。
4.2. Spring 中 Autowired 和 Resource 关键字的区别? @Resource和 @Autowired都是做 bean 的注入时使用,其实 @Resource并不是 Spring 的注解,它的包是 javax.annotation.Resource,需要导入,但是 Spring 支持该注解的注入。
1、共同点 两者都可以写在字段和 setter方法上。两者如果都写在字段上,那么就不需要再写 setter方法。
2、不同点
(1)@Autowired @Autowired为 Spring 提供的注解,需要导入包 org.springframework.beans.factory.annotation.Autowired;只按照 byType 注入。
public class TestServiceImpl {
@Autowired
private UserDao userDao;
@Autowired
public void setUserDao (UserDao userDao) {
this .userDao = userDao;
}
}
@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许 null 值,可以设置它的 required属性为 false。如果我们想使用按照名称(byName)来装配,可以结合 @Qualifier注解一起使用。如下:
public class TestServiceImpl {
@Autowired
@Qualifier("userDao")
private UserDao userDao;
}
(2)@Resource @Resource默认按照 ByName自动注入,由 J2EE 提供,需要导入包 javax.annotation.Resource。 @Resource有两个重要的属性:name和 type,而 Spring 将 @Resource注解的 name 属性解析为 bean 的名字,而 type 属性则解析为 bean 的类型。所以,如果使用 name 属性,则使用 byName 的自动注入策略,而使用 type 属性时则使用 byType自动注入策略。如果既不制定 name 也不制定 type 属性,这时将通过反射机制使用 byName自动注入策略。
public class TestServiceImpl {
@Resource(name="userDao")
private UserDao userDao;
@Resource(name="userDao")
public void setUserDao (UserDao userDao) {
this .userDao = userDao;
}
}
注:最好是将 @Resource放在 setter方法上,因为这样更符合面向对象的思想,通过 set、get去操作属性,而不是直接去操作属性。
①如果同时指定了 name 和 type,则从 Spring 上下文中找到唯一匹配的 bean 进行装配,找不到则抛出异常。
②如果指定了 name,则从上下文中查找名称(id)匹配的 bean 进行装配,找不到则抛出异常。
③如果指定了 type,则从上下文中找到类似匹配的唯一 bean 进行装配,找不到或是找到多个,都会抛出异常。
④如果既没有指定 name,又没有指定 type,则自动按照 byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。 @Resource的作用相当于 @Autowired,只不过 @Autowired按照 byType自动注入
4.3. 依赖注入的方式有几种,各是什么?
一、构造器注入 将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
**优点:**对象初始化完成后便可获得可使用的对象。
**缺点:**当需要注入的对象很多时,构造器参数列表将会很长;不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。
二、setter 方法注入 IoC Service Provider 通过调用成员变量提供的 setter 函数将被依赖对象注入给依赖类。
优点 :灵活。可以选择性地注入需要的对象。
**缺点:**依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。
三、接口注入 依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象。
优点 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
缺点 :侵入行太强,不建议使用。 PS:**什么是侵入行?**如果类 A 要使用别人提供的一个功能,若为了使用这功能,需要在自己的类中增加额外的代码,这就是侵入性。
4.4. 讲一下什么是 Spring Spring 是一个轻量级的 IoC 和 AOP 容器框架 。是为 Java 应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。
常见的配置方式有三种:基于 XML 的配置、基于注解的配置、基于 Java 的配置。
Spring Core:核心类库,提供 IOC 服务;
Spring Context:提供框架式的 Bean 访问方式,以及企业级功能(JNDI、定时任务等); Spring AOP:AOP 服务;
Spring DAO:对 JDBC 的抽象,简化了数据访问异常的处理;
Spring ORM:对现有的 ORM 框架的支持;
Spring Web:提供了基本的面向 Web 的综合特性,例如多方文件上传;
Spring MVC:提供面向 Web 应用的 Model-View-Controller实现。
4.5. Spring MVC 流程
1、用户发送请求至前端控制器 DispatcherServlet。
2、 DispatcherServlet收到请求调用 HandlerMapping处理器映射器。
3、处理器映射器找到具体的处理器 (可以根据 xml 配置、注解进行查找),生成处理器对象及处理器拦截器 (如果有则生成) 一并返回给 DispatcherServlet。
4、 DispatcherServlet调用 HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器 (Controller,也叫后端控制器)。
6、 Controller执行完成返回 ModelAndView。
7、 HandlerAdapter将 controller执行结果 ModelAndView返回给 DispatcherServlet。
8、 DispatcherServlet将 ModelAndView传给 ViewReslover视图解析器。
9、 ViewReslover解析后返回具体 View。
10、 DispatcherServlet根据 View进行渲染视图(即将模型数据填充至视图中)
11、 DispatcherServlet响应用户
DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。 HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。 ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jsp、freemarker、pdf、excel 等。
在讲 SpringMVC 之前我们先来看一下什么是 MVC 模式 MVC:MVC 是一种设计模式 MVC 的原理图:
M-Model 模型(完成业务逻辑:有 javaBean 构成,service+dao+entity) V-View 视图(做界面的展示 jsp,html……) C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)
springMVC 是一个 MVC 的开源框架,springMVC=struts2+spring,springMVC 就相当于是 Struts2 加上 sring 的整合,但是这里有一个疑惑就是,springMVC 和 spring 是什么样的关系呢?这个在百度百科上有一个很好的解释:意思是说,springMVC 是 spring 的一个后续产品,其实就是 spring 在原有基础上,又提供了 web 应用的 MVC 模块,可以简单的把 springMVC 理解为是 spring 的一个模块(类似 AOP,IOC 这样的模块),网络上经常会说 springMVC 和 spring 无缝集成,其实 springMVC 就是 spring 的一个子模块,所以根本不需要同 spring 进行整合。
看到这个图大家可能会有很多的疑惑,现在我们来看一下这个图的步骤:(可以对比 MVC 的原理图进行理解)
第一步:用户发起请求到前端控制器(DispatcherServlet)
第二步:前端控制器请求处理器映射器(HandlerMappering)去查找处理器(Handle):通过 xml 配置或者注解进行查找
第三步:找到以后处理器映射器(HandlerMappering)像前端控制器返回执行链(HandlerExecutionChain)
第四步:前端控制器(DispatcherServlet)调用处理器适配器 (HandlerAdapter)去执行处理器(Handler)
第五步:处理器适配器去执行 Handler
第六步:Handler 执行完给处理器适配器返回 ModelAndView
第七步:处理器适配器向前端控制器返回 ModelAndView
第八步:前端控制器请求视图解析器(ViewResolver)去进行视图解析
第九步:视图解析器像前端控制器返回 View
第十步:前端控制器对视图进行渲染
第十一步:前端控制器向用户响应结果
看到这些步骤我相信大家很感觉非常的乱,这是正常的,但是这里主要是要大家理解 springMVC 中的几个组件:
前端控制器 (DispatcherServlet):接收请求,响应结果,相当于电脑的 CPU。
处理器映射器 (HandlerMapping):根据 URL 去查找处理器 处理器(Handler):(需要程序员去写代码处理逻辑的)
处理器适配器 (HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理器,类比笔记本的适配器(适配器模式的应用)
视图解析器 (ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页面
4.6. SpringMVC 怎么样设定重定向和转发的? (1)转发:在返回值前面加 "forward:",譬如 "forward:user.do?name=method4"
(2)重定向:在返回值前面加 "redirect:",如 "redirect:http://www.baidu.com"
4.7. SpringMVC 常用的注解有哪些?
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象。 @ResponseBody:注解实现将 conreoller 方法返回对象转化为 json 对象响应给客户
4.8. Spring 的 AOP 理解: OOP 面向对象,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为'切面'(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。AOP 实现的关键在于代理模式,AOP 代理主要分为静态代理和动态代理。静态代理的代表为 AspectJ;动态代理则以 Spring AOP 为代表。
(1)AspectJ 是静态代理的增强,所谓静态代理,就是 AOP 框架会在编译阶段生成 AOP 代理类,因此也称为编译时增强,他会在编译阶段将 AspectJ(切面) 织入到 Java 字节码中,运行的时候就是增强之后的 AOP 对象。
(2)Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理:
①JDK 动态代理只提供接口的代理,不支持类的代理。核心 InvocationHandler接口和 Proxy类,InvocationHandler通过 invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例,生成目标类的代理对象。
②如果代理类没有实现 InvocationHandler接口,那么 Spring AOP 会选择使用 CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现 AOP。CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB做动态代理的
(3)静态代理与动态代理区别在于生成 AOP 代理对象的时机不同,相对来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译器进行处理,而 Spring AOP 则无需特定的编译器处理
4.9. Spring 的 IOC 理解
(1)IOC 就是控制反转,是指创建对象的控制权的转移,以前创建对象的主动权和时机是由自己把控的,而现在这种权力转移到 Spring 容器中,并由容器根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI 依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖 IoC 容器来动态注入对象需要的外部资源。
(2)最直观的表达就是,IOC 让对象的创建不用去 new 了,可以由 spring 自动生产,使用 java 的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
(3)Spring 的 IOC 有三种注入方式:**构造器注入、setter 方法注入、根据注解注入。**IoC 让相互协作的组件保持松散的耦合,而 AOP 编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
4.10. 解释一下 spring bean 的生命周期 首先说一下 Servlet 的生命周期:实例化,初始 init,接收请求 service,销毁 destroy;Spring 上下文中的 Bean 生命周期也类似,如下:
(1)实例化 Bean:对于 BeanFactory容器,当客户向容器请求一个尚未初始化的 bean 时,或初始化 bean 的时候需要注入另一个尚未初始化的依赖时,容器就会调用 createBean 进行实例化。对于 ApplicationContext容器,当容器启动结束后,通过获取 BeanDefinition对象中的信息,实例化所有的 bean。
(2)设置对象属性(依赖注入):实例化后的对象被封装在 BeanWrapper对象中,紧接着,Spring 根据 BeanDefinition 中的信息以及通过 BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理 Aware 接口:接着,Spring 会检测该对象是否实现了 xxxAware接口,并将相关的 xxxAware实例注入给 Bean:①如果这个 Bean 已经实现了 BeanNameAware 接口,会调用它实现的 setBeanName(String beanId)方法,此处传递的就是 Spring 配置文件中 Bean 的 id 值;②如果这个 Bean 已经实现了 BeanFactoryAware接口,会调用它实现的 setBeanFactory()方法,传递的是 Spring 工厂自身。③如果这个 Bean 已经实现了 ApplicationContextAware接口,会调用 setApplicationContext(ApplicationContext)方法,传入 Spring 上下文;
(4)BeanPostProcessor:如果想对 Bean 进行一些自定义的处理,那么可以让 Bean 实现了 BeanPostProcessor接口,那将会调用 postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:如果 Bean 在 Spring 配置文件中配置了 init-method属性,则会自动调用其配置的初始化方法。
(6)如果这个 Bean 实现了 BeanPostProcessor 接口,将会调用 postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在 Bean 初始化结束时调用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean 就已经被正确创建了,之后就可以使用这个 Bean 了。
(7)DisposableBean:当 Bean 不再需要时,会经过清理阶段,如果 Bean 实现了 DisposableBean 这个接口,会调用其实现的 destroy()方法;
(8)destroy-method:最后,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会自动调用其配置的销毁方法
4.11. 解释 Spring 支持的几种 bean 的作用域。 Spring 容器中的 bean 可以分为 5 个范围:
(1)singleton:默认,每个容器中只有一个 bean的实例,单例的模式由 BeanFactory自身来维护。
(2)prototype:为每一个 bean 请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean 会失效并被垃圾回收器回收。
(4)session:与 request范围类似,确保每个 session中有一个 bean 的实例,在 session 过期后,bean 会随之失效。
(5)global-session:全局作用域,global-session和 Portlet 应用相关。当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。如果你想要声明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在 global-session中。全局作用域与 Servlet 中的 session 作用域效果相同。
4.12. Spring 基于 xml 注入 bean 的几种方式:
(1)Set 方法注入;
(2)构造器注入:①通过 index 设置参数的位置;②通过 type 设置参数类型;
(3)静态工厂注入;
(4)实例工厂;
4.13. Spring 框架中都用到了哪些设计模式?
(1)工厂模式 :BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
(2)单例模式 :Bean 默认为单例模式。
(3)代理模式 :Spring 的 AOP功能用到了 JDK的动态代理和 CGLIB字节码生成技术;
(4)模板方法 :用来解决代码重复的问题。比如 RestTemplate,JmsTemplate, JpaTemplate。
(5)观察者模式 :定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如 Spring 中 listener 的实现–ApplicationListener
五、MyBatis 篇
5.1. 什么是 MyBatis
(1)Mybatis 是一个半 ORM(对象关系映射)框架 ,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高 。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息 ,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执行 sql 到返回 result 的过程)。
5.2. MyBatis 的优点和缺点
(1)基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
(2)与 JDBC 相比,减少了 50% 以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
(3)很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持)。
(4)能够与 Spring 很好的集成;
(5)提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
(1)SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。
(2)SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
5.3. #{}和${}的区别是什么?
#{}是预编译处理,$ {}是字符串替换。Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement的 set 方法来赋值;Mybatis 在处理 $ {}时,就是把$ {}替换成变量的值。使用#{}可以有效的防止 SQL 注入,提高系统安全性。
5.4. 当实体类中的属性名和表中的字段名不一样,怎么办? 第 1 种:通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
<select id ="selectorder" parametertype ="int" resultetype ="me.gacl.domain.order" >
select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select >
第 2 种:通过来映射字段名和实体类属性名的一一对应的关系。
<select parameterType ="int" resultMap ="orderresultmap" >
select * from orders where order_id=#{id}
</select >
<resultMap type ="me.gacl.domain.order" id ="orderresultmap" >
<id property ="id" column ="order_id" >
<result property ="orderno" column ="order_no" />
<result property ="price" column ="order_price" />
</resultMap >
5.5. Mybatis 是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds对象进行分页,它是针对 ResultSet结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect方言,添加对应的物理分页语句和物理分页参数。
5.6. Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用 sql 列的别名功能,将列的别名书写为对象属性名。有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
5.7. 如何执行批量插入? <insert id ="insertname" >
insert into names (name) values (#{value})
</insert >
然后在 java 代码中像下面这样执行批处理插入:
list<string> names = new arraylist ();
names.add("fred" );
names.add("barney" );
names.add("betty" );
names.add("wilma" );
sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch);
try {
namemapper mapper = sqlsession.getmapper(namemapper.class);
for (string name : names) {
mapper.insertname(name);
}
sqlsession.commit();
}catch (Exception e){
e.printStackTrace();
sqlSession.rollback();
throw e;
} finally {
sqlsession.close();
}
5.8. MyBatis 实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次,通过在 resultMap里面配置 association节点配置一对一的类就可以完成;嵌套查询是先查一个表,根据这个表里面的结果的 外键 id,去再另外一个表里面查询数据,也是通过 association配置,但另外一个表的查询通过 select 属性配置。
5.9. Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么? Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的
5.10. Mybatis 的一级、二级缓存:
1)一级缓存:基于 PerpetualCache的 HashMap本地缓存,其存储作用域为 Session,当 Session flush或 close之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable序列化接口 (可用来保存对象的状态),可在它的映射文件中配置;
3)对于缓存数据更新机制,当某一个作用域 (一级缓存 Session/二级缓存 Namespaces) 的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新
六、SpringBoot 篇
6.1. 什么是 SpringBoot?为什么要用 SpringBoot 用来简化 spring 应用的初始搭建以及开发过程 使用特定的方式来进行配置(properties 或 yml 文件)创建独立的 spring 引用程序 main 方法运行嵌入的 Tomcat 无需部署 war 文件简化 maven 配置自动配置 spring 添加对应功能 starter 自动化配置 spring boot 来简化 spring 应用开发,约定大于配置,去繁从简,just run 就能创建一个独立的,产品级别的应用 Spring Boot 优点非常多,如:
一、独立运行 Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成 war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。
二、简化配置 spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。
三、自动配置 Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 spring-boot-starter-web 启动器就能拥有 web 的功能,无需其他配置。
四、无代码生成和 XML 配置 Spring Boot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。
五、应用监控 Spring Boot 提供一系列端点可以监控服务及应用,做健康检测。
6.2. Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? 启动类上面的注解是 @SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: @SpringBootConfiguration:组合了 @Configuration注解,实现配置文件的功能。 @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 @ComponentScan:Spring 组件扫描。
6.3. 运行 Spring Boot 有哪几种方式?
1)打包用命令或者放到容器中运行
2)用 Maven/Gradle 插件运行
3)直接执行 main 方法运行
6.4. 如何理解 Spring Boot 中的 Starters?
Starters 可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。Starters 包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。
Starters 命名:Spring Boot 官方的启动器都是以 spring-boot-starter-命名的,代表了一个特定的应用类型。第三方的启动器不能以 spring-boot 开头命名,它们都被 Spring Boot 官方保留。一般一个第三方的应该这样命名,像 mybatis 的 mybatis-spring-boot-starter。
Starters 分类:1.Spring Boot 应用类启动器
6.5. 如何在 Spring Boot 启动的时候运行一些特定的代码? 如果你想在 Spring Boot 启动的时候运行一些特定的代码,你可以实现接口 ApplicationRunner或者 CommandLineRunner,这两个接口实现方式一样,它们都只提供了一个 run 方法。 CommandLineRunner:启动获取命令行参数
6.6. Spring Boot 需要独立的容器运行吗? 可以不需要,内置了 Tomcat/ Jetty等容器。
6.7. Spring Boot 中的监视器是什么?
Spring boot actuator 是 spring 启动框架中的重要功能之一。Spring boot 监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为 HTTP URL 访问的 REST 端点来检查状态
6.8. 如何使用 Spring Boot 实现异常处理? Spring 提供了一种使用 ControllerAdvice处理异常的非常有用的方法。我们通过实现一个 ControlerAdvice类,来处理控制器类抛出的所有异常
6.9. 你如何理解 Spring Boot 中的 Starters? Starters 可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA访问数据库,只要加入 spring-boot-starter-data-jpa启动器依赖就能使用了
6.10. springboot 常用的 starter 有哪些
spring-boot-starter-web嵌入 tomcat 和 web 开发需要 servlet 与 jsp 支持 spring-boot-starter-data-jpa数据库支持 spring-boot-starter-data-redisredis 数据库支持 spring-boot-starter-data-solrsolr 支持 mybatis-spring-boot-starter第三方的 mybatis 集成 starter
6.11. SpringBoot 实现热部署有哪几种方式?
Spring Loaded Spring-boot-devtools
6.12. 如何理解 Spring Boot 配置加载顺序? 在 Spring Boot 里面,可以使用以下几种方式来加载配置。
1)properties 文件;2)YAML 文件;3)系统环境变量;4)命令行参数;等等……
6.13. Spring Boot 的核心配置文件有哪几个?它们的区别是什么? pring Boot 的核心配置文件是 application 和 bootstrap 配置文件。application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。bootstrap 配置文件有以下几个应用场景。
1.使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
2.一些固定的不能被覆盖的属性;
3.一些加密/解密的场景;
6.14. 如何集成 Spring Boot 和 ActiveMQ? 对于集成 Spring Boot 和 ActiveMQ,我们使用 spring-boot-starter-activemq依赖关系。它只需要很少的配置,并且不需要样板代码。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online