Java经典面试题笔记

一,Java基础

1,说说你对面向对象的理解。

什么是面向对象呢?在所其是什么时,不妨我们先来说说以其不同的一个概念面向过程。面向过程是一个更加注重事情的每一个步骤即顺序,即是强调过程的。而面向对象更加注重有哪些参与者(对象)、及各个参与者(对象)的作用(方法)。

举一个我们熟悉的例子:怎么才能把一个大象放进冰箱里冰冻起来?

面向过程的操作是:

        打开冰箱门->把大象塞进冰箱里->关闭冰箱门->把大象冰冻起来

面向对象的操作是:拆分出人,冰箱和大象三个对象

        人:打开冰箱、塞入大象、关闭冰箱

        大象:没有操作,特点是大

        冰箱:把大象冰冻起来

从以上例子可以看出,面向过程比较直接高效,而面向对象更易于复用,拓展和维护。

2,JDK、JRE、JVM之间的区别。

先从它们的名字上来解析:

JDK全称是(Java SE Development Kit),中文意思Java标准开发包,它提供了编译运行java程序所需的各种工具(如 javadoc 和 jdb)和资源。包含Java编译器(javac)java运行环境(JRE),以及常用的java类库等。

JRE全称是(Java Runtime Environment),java运行环境,用于运行Java的字节码文件。JRE包含JVM以及JVM工作所需的类库,普通用户而只需安装JRE来运行Java程序(不需编译),而程序开发者必须按照JDK来编译、调试程序。

JVM全称是(Java Virtual Mechinal),java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心的部分,负责运行字节码文件(转成机器指令)。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在

从包含关系上区分:

JDK包含JRE,JRE包含JVM

从使用上区分:

需要编写java程序来运行的话,必须安装JDK(因为JRE不包含编译器)

只需运行编译好的字节码的话,安装JRE即可(不能是JVM,因为缺少JVM工作所需的类库)

3,泛型中的extends和super的区别

官方解析:

通俗解析就是,这两个关键字都是用来限制泛型类中<>中可以填入的对象类型。extends T表示

只能填入T的子类类型,例如:T为Number时,只能填入int,double,float和Integer等,不能填入String。

值得注意的是:泛型擦除这种现象,因为java中的泛型都是伪泛型

Java经典面试题笔记_第1张图片

泛型擦除概念的理解

4,==和equals方法的区别

== :如果比较的是基本数据类型,则比较值,如果是引用类型,则比较地址

equals:具体看各个类重写equals方法之后的比较逻辑,比如String类型,虽然是引用类型,但是重写了equals方法,方法内部比较的是字符串中的各个字符串是否相等(Object中的equals默认是比较地址)

5,Java和C++的区别

虽然,Java和C++都是面对对象的语言,都是支持封装,、继承和多态。但是,它们还是有挺多不

相等的地方。

  1. Java不提供指针来直接访问内存,程序内存相对而言比C++更安全
  2. Java的类时单继承的,C++支持多重继承;虽然Java的类不支持多继承,但是接口可以多继承
  3. Java有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
  4. C++同时支持方法重载和操作符重载(+-*/等,改变它们的表示的职能),但是Java只支持方法重载(操作符重载增加了复杂性,这和Java最初的设计思想相违背)
  5. ……

6,标识符和关键字的区别什么?

编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字

有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字 。简单来说,关键字是被赋予特殊含义的标识符 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。(private、protected、public)

7,自增自减运算符

口诀:“符号在前就先加/减,符号在后就后加/减”。

8,Java的移位运算符

Java 中有三种移位运算符:

  • << :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
  • >> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
  • >>> :java的无符号右移,忽略符号位,空位都以 0 补齐。(例如:一字节8位数:10110100右移两位的结果为:00101101,不会保留符号位1,高位全补零)

不能进行移位操作的数据类型:

由于 doublefloat (浮点数)在二进制中的表现比较特殊,因此不能来进行移位操作。

移位操作符实际上支持的类型只有intlong,编译器在对shortbytechar类型进行移位前,都会将其转换为int类型再操作。

如果移位的位数操作数值所占有的位数会怎样?

当 int 类型左移/右移位数大于等于 32 位操作时,会先会和类型位数求余(%)后再进行左移/右移操作。

例如:x<<42等同于x<<10x>>42等同于x>>10x >>>42等同于x >>> 10,x<<32等同于不移动

9,静态方法为什么不能调用非静态成员?

这个需要结合 JVM 的相关知识,主要原因如下:

  1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,所以属于非法操作。

10,静态方法和实例方法有何不同?

调用方式:

外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式。而调用实例方法时,只有 对象.方法名 这种方式。也就是说:调用静态方法可以无需创建对象

但是,一般不建议使用 对象.方法名 的方式调用静态方法,种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。

访问类成员是否在限制:

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

 11,重载和重写有什么区别?

重载:

编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))。

Java 允许重载任何方法, 而不只是构造器方法。

综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。

重写:

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类访问修饰符范围大于等于父类
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写

综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。

方法的重写要遵循“两同两小一大”(以下内容摘录自《疯狂 Java 讲义》,issue#892open in new window ):

  • “两同”即方法名相同、形参列表相同;
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

 ⭐️ 关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型

12,List和Set的区别

List:有序,保存元素插入的顺序,即有序。允许多个NULL元素对象存在,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。

Set:无序,不可重复,最多允许一个NULL元素对象存在,取元素只能使用Iterator接口取得所有的元素,再逐一遍历各个元素。

13,ArrayList和LinkedList区别

首先,它们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。

由于底层数据结构不同,它们所适用的场景页不同,ArrayList更适合随机查找,LinkedList更适合删除和添加。查询,添加,删除的时间复杂度不同。

ArrayList和LinkedList都实现了List接口,但是Linked额外实现了Deque接口(双端队列 Double Ended Queue,所以LinkedList还可以当作队列来使用。

14,谈谈ConcurrentHashMap(并发HashMap)的扩容机制

先说说ConcurrentHashMap的定义:

ConcurrentHashMap是Java中的一个线程安全的Map实现,可以在多线程环境下使用。它的作用是提供一个线程安全的Map,可以用来存储键值对。与HashMap不同的是,ConcurrentHashMap的put()、get()等方法可以在多线程环境下安全地并发访问,而不需要使用同步机制。

jdk1.7版本的底层实现原理:

ConcurrentHashMap的底层实现原理是分段锁(Segment)和CAS(Compare-And-Swap)算法。

  1. 分段锁(Segment段/部分):ConcurrentHashMap将Map分为多个Segment,每个Segment都是一个独立的Hash表,其中每个Segment都有一个锁来控制对该Segment的访问。这样,在多线程环境下,每个线程只需要锁定访问的Segment,而不需要锁定整个Map,从而提高了并发访问的效率。

  2. CAS(Compare-And-Swap)算法:ConcurrentHashMap使用CAS算法来实现对Map的并发访问。CAS是一种乐观锁机制,它利用了CPU的原子性指令,将修改操作转换为一个原子性的比较、修改、更新的操作。这样,在并发访问时,每个线程可以独立地进行操作,而不需要加锁,从而提高了并发访问的效率。

总的来说,ConcurrentHashMap的底层实现原理是将Map分为多个Segment,并对每个Segment进行锁定和CAS算法的操作,从而实现了线程安全和高效的并发访问。

jdk1.8版本的底层实现原理:

1. 采用了CAS(Compare-And-Swap)和Synchronized两种锁机制:ConcurrentHashMap在JDK1.8中采用了CAS和Synchronized两种锁机制,其中CAS用于对Map的读操作和更新操作,而Synchronized用于对Segment的扩容操作。

2. 采用了红黑树:在JDK1.8中,当一个Segment中的链表长度超过了一个阈值(默认为8),就会将链表转换为红黑树,从而提高了查找和删除操作的效率。

3. 采用了扩容的新策略:在JDK1.8中,ConcurrentHashMap采用了一种新的扩容策略,即在原有的Segment基础上,增加新的Segment,从而避免了整个Map的扩容,提高了扩容的效率。

JDK1.7版本的ConcurrentHashMap扩容机制:

1. ConcurrentHashMap在JDK1.7版本是基于Sement分段实现,每个Segment就是一个小型的HashMap,其中每个Segment都有一个锁来控制对该Segment的访问。

2. 当某个Segment的元素数量达到了一定的阈值(默认为Segment的容量的0.75),就会触发该Segment的扩容操作。

3. 在扩容操作中,ConcurrentHashMap会将该Segment中的元素重新分配到新的Segment中,从而保证每个Segment的元素数量尽量平均。

4. 在JDK1.7版本中,ConcurrentHashMap的扩容操作是比较耗时的,因为需要对整个Map进行重建。

JDK1.8版本的ConcurrentHashMap扩容机制:

1. 在JDK1.8版本中,ConcurrentHashMap的扩容机制得到了改进,主要是通过增加Segment的方式来避免整个Map的扩容。

2. 1.8版本的ConcurrentHashMap不在基于Segment实现。

3. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容。

4.如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容,且支持多个线程同时扩容

5. 在JDK1.8版本中,ConcurrentHashMap的扩容操作相比于JDK1.7版本更加高效,因为它只需要对需要扩容的Segment进行操作,而不需要对整个Map进行重建。

总的来说,JDK1.8版本的ConcurrentHashMap采用了更加高效的扩容机制,通过增加Segment的方式避免了整个Map的扩容,从而提高了并发性能和扩容效率。

15,jdk1.7到jdk1.8HashMap底层实现发生了什么变化?

1. 数组+链表+红黑树的混合实现方式:在JDK1.7中,HashMap的底层实现采用了数组+链表的方式来处理哈希冲突,而在JDK1.8中,HashMap的底层实现采用了数组+链表+红黑树的混合实现来处理哈希冲突。当链表长度超过一定阈值时,会将链表转换为红黑树,从而提高了插入,查找和删除操作的效率。 

2. 存储方式的改变:在JDK1.7中,HashMap的底层实现采用了Entry数组来存储键值对(头插法),而在JDK1.8中,HashMap的底层实现采用了Node数组来存储键值对(尾插法)。Node数组中的每个元素包含了键、值、哈希值和指向下一个元素的指针,从而避免了在链表中存储键值对时需要多存储一个指向键的指针的问题。

3. 哈希值的计算方式的改变:在JDK1.7中,HashMap的底层实现采用了位运算来计算哈希值(复杂),而在JDK1.8中,HashMap的底层实现采用了位运算和一些其他的运算来计算哈希值(因为采用红黑树,算法得到简化),从而提高了哈希值的分布均匀性。

4. 并发性能的改进:在JDK1.8中,HashMap的底层实现采用了一些并发性能的改进,例如使用了CAS(Compare-And-Swap)操作和Synchronized锁机制来保证线程安全,从而提高了HashMap在高并发场景下的性能表现。

总的来说,在JDK1.8中,HashMap的底层实现得到了一些优化和改进,从而提高了HashMap在性能、并发性和哈希冲突处理方面的表现。

16,说一下HashMap的Put方法

先说HashMap的Put⽅法的⼤体流程:

1. 根据Key通过哈希算法与运算得出数组下标

2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中 是Node对象)并放⼊该位置

3. 如果数组下标位置元素不为空,则要分情况讨论

        a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中

        b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node                 ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个 过程中会判断红⿊树中是否存在当前key,如果存在则更新value ;

                ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8(要判断数组长度是否大于等于64,否则会进行数组扩容,而不是转换成红黑树),那么则会将该链表转成红⿊树;

                ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要 就扩容,如果不需要就结束PUT⽅法;

17,深拷贝和浅拷贝的区别

深拷⻉和浅拷⻉就是指对象的拷,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤(引用对象)

1. 浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象

2. 深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制, 深拷⻉出来的对象,内部的属性指向的不是同⼀个对象

18,谈下HashMap的扩容机制原理

 1.7版本

1. 先⽣成新数组(一般为原来数组的两倍,即数组长度*2)

2. 遍历⽼数组中的每个位置上的链表上的每个元素

3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标

4. 将元素添加到新数组中去

5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本

1. 先⽣成新数组(一般为原来数组的两倍,即数组长度*2);

2. 遍历⽼数组中的每个位置上的链表或红⿊树;

3. 如果是链表,则直接将链表中的每个元素重新计算下标(根据新数组的长度),并添加到新数组中去;

4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置

        a. 统计每个下标位置的元素个数;

        b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置;

        c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置;

5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性;

19,CopyWriteArrayList的底层原理是怎样的?

CopyOnWriteArrayList是一个线程安全的ArrayList,它的主要作用是在多线程环境下提供高效的并发访问,同时保证数据的一致性和可靠性。

1. ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素 时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏(写不会堵塞读,即提高了并发场景的读效率,当数据实时性不好);

2. 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题;

3. 写操作结束之后会把原数组指向新数组;

4. CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所 以不适合实时性要求很⾼的场景;

20,什么是字节码?采用字节码的好处有哪些?

编译器(javac)将Java源⽂件(*.java)⽂件编译成为字节码⽂件(*.class),可以做到⼀次编译到处运⾏, windows上编译好的class⽂件,可以直接在linux上运⾏,通过这种⽅式做到跨平台,不过Java的跨平 台有⼀个前提条件,就是不同的操作系统上安装的JDK或JRE是不⼀样的,虽然字节码是通⽤的,但是 需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各⾃ 的JDK或JRE。

采⽤字节码的好处,⼀⽅⾯实现了Java跨平台的特性,另外⼀⽅⾯也提⾼了代码执⾏的性能,编译器在编译源代码 时可以做⼀些编译期的优化,⽐如锁消除、标量替换、⽅法内联等。

21,Java中的异常体系是怎样的?

Java中的所有异常都来⾃顶级⽗类Throwable

Throwable下有两个⼦类Exception和Error

Error表示⾮常严重的错误,⽐如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError, 通常这些错误出现时,仅仅想靠程序⾃⼰是解决不了的,可能是虚拟机、磁盘、操作系统层⾯出现 的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不⼤,因为程序可能已经根本运⾏不了了。

Exception表示异常,表示程序出现Exception时,是可以靠程序⾃⼰来解决的,⽐如 NullPointerException、IllegalAccessException等,我们可以捕获这些异常来做特殊处理。

Exception的⼦类通常⼜可以分为RuntimeException和⾮RuntimeException两类 RunTimeException表示运⾏期异常,表示这个异常是在代码运⾏过程中抛出的,这些异常是⾮检查 异常,程序中可以选择捕获处理,也可以不处理。这些异常⼀般是由程序逻辑错误引起的,程序应该从逻 辑⻆度尽可能避免这类异常的发⽣,⽐如NullPointerException、IndexOutOfBoundsException等。

⾮RuntimeException表示⾮运⾏期异常,也就是我们常说的检查异常,是必须进⾏处理的异常,如果 不处理,程序就不能检查异常通过。如IOException、SQLException等以及⽤户⾃定义的Exception异 常。

22,在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?

异常相当于⼀种提示,如果我们抛出异常,就相当于告诉上层⽅法,我抛了⼀个异常,我处理不了这个 异常,交给你来处理,⽽对于上层⽅法来说,它也需要决定⾃⼰能不能处理这个异常,是否也需要交给它的上层(责任链模式)

所以我们在写⼀个⽅法时,我们需要考虑的就是,本⽅法能否合理的处理该异常,如果处理不了就继续 向上抛出异常,包括本⽅法中在调⽤另外⼀个⽅法时,发现出现了异常,如果这个异常应该由⾃⼰来处 理,那就捕获该异常并进⾏处理。

23,Java中有哪些类加载器

JDK⾃带有三个类加载器:bootstrap ClassLoader(启动类加载器)、ExtClassLoader(扩展类加载器)、AppClassLoader(应用程序类加载器)。

BootStrapClassLoader是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下的 jar包和class⽂件。(负责加载Java的核心类库,如java.lang包中的类、java.util包中的类等)

ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext⽂件夹下的 jar包和class类。(负责加载Java扩展类库,如javax包中的类、org包中的类等。)

AppClassLoader是⾃定义类加载器的⽗类,负责加载classpath下的类⽂件。(主要负责加载应用程序类,也就是应用程序代码中的类。应用程序类是指应用程序自己定义的类,它们通常存放在CLASSPATH环境变量指定的目录中。)

24,说说类加载器双亲委派模型

JVM中存在三个默认的类加载器:

1. BootstrapClassLoader

2. ExtClassLoader

3. AppClassLoader

AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是 BootstrapClassLoader。 JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤ BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。

所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap(父类加载器)进⾏加载,如果没加载到才由⾃⼰(子类加载器)进⾏加载。

25,JVM中哪些是线程共享区?

堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器(记录某个线程执行的代码行数等)是每个线程独有的。

Java经典面试题笔记_第2张图片

JVM内存结构详解文章

1.1 栈(Stack)

每当一个线程去执行方法时,就会同时在栈里面创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等。

每一个方法从被调用到执行完成的过程,都对应着一个栈帧从入栈到出栈的过程。

栈是线程私有的,每个线程在栈中保有自己的数据,别的线程无法访问。

在栈中我们可能遇到两种异常:StackOverflowError和OutOfMemoryError

StackOverflowError是指线程请求的栈深度大于虚拟机所允许的深度

OutOfMemoryError是指栈扩展时无法申请到足够的内存

这两种异常我们会在后面的文章中详细讲到。

1.2 本地方法栈(Native Method Stack)

本地方法栈和栈差不多,区别只在于本地方法栈为Native方法服务。

Native 方法就是一个Java调用非Java代码的接口,比如JNI。

本地方法栈也是线程私有的

1.3 程序计数器(PC Register)

要理解程序计数器,我们需要知道java代码最终都要编译成一条条的字节码,然后由字节码解释器一条条的执行的。而程序计数器可以看作是当前线程所执行的字节码的行号计数器

如果正在执行的是一个java方法,那么这个计数器记录的是正在执行的字节码指令地址。如果正在执行的是Native方法,那么计数器的值为Undefined。

程序计数器也是线程私有的,每条线程都有一个独立的程序计数器,各线程的程序计数器互不影响。

程序计数器是唯一一个不会OOM的内存区域

1.4堆(Heap)

堆应该是java内存中占用空间最大的一个区域,大家喜闻乐见的垃圾回收就主要发生在这个区域。

堆的唯一作用就是存放对象,不过并非所有对象都在堆中。这个我们以后会讲到。

堆如果空间不足,就会抛出OOM异常。

堆是可以让多个线程共享的

1.5 方法区(Method Area)

方法区也是可以让多个线程共享的

方法区主要用来存放类的版本,字段,方法,接口、常量池和运行时常量池。

常量池里存储着字面量和符号引用

 26,排查JVM问题的一般步骤是?

对于还在正常运⾏的系统:

1. 可以使⽤jmap来查看JVM中各个区域的使⽤情况

2. 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁

3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏ 调优了

4. 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析

5. ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表 示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效

6. 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存

对于已经发⽣了OOM(Out Of Memory 内存溢出)的系统:

1. ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(- XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)

2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件

3. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码

4. 然后再进⾏详细的分析和调试 总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题 

27,一个对象加载到JVM,再到被GC清除,都经历了什么过程?

1. ⾸先把字节码⽂件内容加载到⽅法区

2. 然后再根据类信息在堆区创建对象

3. 对象⾸先会分配在堆区中年轻代的Eden区,经过⼀次Minor GC(Minor年龄较小。是指对新生代进行垃圾回收的操作)后,对象如果存活,就会进⼊ Survivor(幸存者)区。在后续的每次Minor GC中,如果对象⼀直存活,就会在Survivor区来回拷⻉,每移动 ⼀次,年龄加1

4. 当年龄超过15后,对象依然存活,对象就会进⼊⽼年代

5. 如果经过Full GC(Full Garbage Collection指对整个堆内存进行垃圾回收的操作),被标记为垃圾对象,那么就会被GC线程清理掉。

28,JVM怎么确认一个对象是不是垃圾?

1. 引⽤计数算法: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为0的就认为是垃圾。这是早期JDK中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。 

2. 可达性算法: 这种⽅式是在内存中,从根对象向下⼀直找引⽤,找到的对象就不是垃圾,没找到的 对象就是垃圾。

29,JVM有哪些垃圾回收算法?

1. 标记清除算法

        a. 标记阶段:把垃圾内存标记出来

        b. 清除阶段:直接将垃圾内存回收。

        c. 这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚

2. 复制算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了复制算法。复制算法将内存分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽ 且,他的效率跟存活对象的个数有关

3. 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将边界以外的所有内存直接清除。

30,什么是STW?

STW: Stop-The-World,是在垃圾回收算法执⾏过程当中,需要将JVM内存冻结的⼀种状态。

在STW 状态下,JAVA的所有线程都是停⽌执⾏的,GC线程除外,native⽅法可以执⾏,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点

31,常用的JVM启动参数有哪些?

JVM参数⼤致可以分为三类:

        1. 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。

        2. ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。

        3. 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。

常用的JVM启动参数如下:

1. -Xms:设置JVM初始堆大小
2. -Xmx:设置JVM最大堆大小
3. -Xmn:设置年轻代大小
4. -XX:PermSize:设置永久代初始大小
5. -XX:MaxPermSize:设置永久代最大大小
6. -XX:MaxHeapSize:设置堆最大值
7. -XX:NewSize:设置年轻代的初始大小
8. -XX:MaxNewSize:设置年轻代的最大大小
9. -XX:SurvivorRatio:设置年轻代中eden区与survivor区的比例
10. -XX:MaxTenuringThreshold:设置对象进入老年代的年龄阈值
11. -XX:ParallelGCThreads:设置并行垃圾回收器的线程数
12. -XX:+UseConcMarkSweepGC:使用CMS垃圾回收器
13. -XX:+UseParallelGC:使用并行垃圾回收器
14. -XX:+UseG1GC:使用G1垃圾回收器
15. -XX:+PrintGCDetails:打印GC详细信息
16. -XX:+PrintGCTimeStamps:打印GC时间戳
17. -XX:+HeapDumpOnOutOfMemoryError:当发生OOM时,生成堆转储文件
18. -XX:HeapDumpPath:指定堆转储文件的路径

32,说说对线程安全的理解?

线程安全指的是,我们写的某段代码,在多个线程同时执⾏这段代码时,不会产⽣混乱,依然能够得到 正常的结果,⽐如i++,i初始化值为0,那么两个线程来同时执⾏这⾏代码,如果代码是线程安全的,那么最终的结果应该就是⼀个线程的结果为1,⼀个线程的结果为2,如果出现了两个线程的结果都为1,则 表示这段代码是线程不安全的。

所以线程安全,主要指的是指:⼀段代码在多个线程同时执⾏的情况下,能否得到正确的结果。

33,说说你对守护线程的理解

线程分为⽤户线程和守护线程,⽤户线程就是普通线程,守护线程(Daemon Thread)就是JVM的后台线程,⽐如垃圾回收线程就是⼀个守护线程,守护线程会在其他普通线程都停⽌运⾏之后⾃动关闭。我们可以通过设置 thread.setDaemon(true)来把⼀个线程设置为守护线程。

守护线程的特点如下:

  1. 守护线程是一种低优先级的线程,它不会影响到程序的正常执行。

  2. 守护线程通常用于执行一些后台任务,如垃圾回收、内存管理等。

  3. 守护线程在启动时需要通过thread.setDaemon(true)方法将其设置为守护线程。

  4. 守护线程不能访问程序中的非守护线程,因为当所有的非守护线程结束时,守护线程也会随之结束。

  5. 守护线程不能用来执行一些需要保证完整性和正确性的任务,因为它随时可能被中断或终止。

总之,守护线程是一种非常有用的线程类型,它可以帮助程序执行一些后台任务,提高程序的性能和效率。 

34,ThreadLocal的底层原理

1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据

2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的 值

3. 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把 设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强 引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象

4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)

Java经典面试题笔记_第3张图片

源码:

Java经典面试题笔记_第4张图片

 35,并发、并行和串行之间的区别?

1. 串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务

2. 并⾏(Parallelism):两个任务同时执⾏

3. 并发(Concurrency):两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后 ⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的

36,Java死锁如何避免?

造成死锁的⼏个原因:

        1. ⼀个资源每次只能被⼀个线程使⽤(非共享资源)

        2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源(堵塞等待)

        3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺(不可抢占)

        4. 若⼲线程形成头尾相接的循环等待资源关系(循环等待)

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

        1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁

        2. 要注意加锁时限,可以针对所设置⼀个超时时间

        3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决(银行家算法)

36,线程池的底层工作原理?

 线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:

1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。

2. 如果此时线程池中的线程数量等于coreP oolSize,但是缓冲队列workQueue(海底捞店外排列的座位)未满,那么任务被放⼊缓冲队列。

3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数 量⼩于maximumPoolSize,建新的线程来处理被添加的任务。

4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略(拒绝连接策略)来处理此任务。

5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数

37,线程池为什么是先添加队列而不是先创建线程(达到最大线程)?

当线程池中的核⼼线程都在忙时,如果继续往线程池中添加任务,那么任务会先放⼊队列,队列满了之 后,才会新开线程。这就相当于,⼀个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是⼀开始这些需求只会增加在待开发列表中(将线程加到队列),然后这 10个程序员加班加点的从待开发列表中获取需求并进⾏处理,但是某⼀天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员⼯了(新开线程)

38,ReentrantLock(可重入锁)的公平锁和非公平锁的底层实现

⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使 ⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队(直接插队),⽽是直接竞争锁(通过CAS操作(版本控制-乐观锁)去尝试获取锁)。

不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。 另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。

39,ReentrantLock中tryLock()和lock()方法的区别是什么?

1. tryLock()表示尝试加锁(非堵塞加锁),可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false;

2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值(返回则继续执行,否则堵塞);

40,CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤ CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对 CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。 对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。

41,Sychronized(同步的)的偏向锁、轻量级锁、重量级锁的定义

1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了

2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程

3. 重量级锁:如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞

4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统(API)去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中(通过代码来实现锁),相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

42,Sychronized(同步的)和ReentrantLock的区别

1. sychronized是⼀个关键字,ReentrantLock是⼀个

2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁

3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁

4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁

5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态(以及表示锁的数量(可重入))

6. sychronized底层有⼀个锁升级的过程

43,谈谈对AQS的理解,AQS如何实现可重入锁的?

1. AQS(AbstractQueuedSynchronizer-抽象同步队列)是⼀个JAVA线程同步的框架。是JDK中很多锁⼯具的核⼼实现框架。

2. 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下, 有不⽤的意义。

3. 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁(底层会判断以获取锁的线程ID和尝试获取锁的ID是否相等,相等的话state++,记为重入多一次。否则获取锁失败),state就加1。释放锁state就减1(state为0,才真正释放锁)。

二,Spring框架

1,谈谈对IOC的理解

通常,我们认为Spring有两⼤特性IOC和AOP,那到底该如何理解IOC呢? 对于很多初学者来说,IOC这个概念就是只记住是控制反转,但是控制反转是什么就不得而知了。

那么IOC到底是什么,接下来来说说我的理解,实际上这是⼀个⾮常⼤的问题,所以我们就把它拆细了 来回答,IOC表示控制反转,那么:

  1.  什么是控制?控制了什么?
  2.  什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
  3. 为什么要反转?反转之前有什么问题?反转之后有什么好处?

这就是解决这⼀类⼤问题的思路,⼤⽽化⼩。

那么,我们先来解决第⼀个问题:什么是控制?控制了什么?

我们在⽤Spring的时候,我们需要做什么:

  1. 建⼀些类,⽐如UserService、OrderService
  2. ⽤⼀些注解,⽐如@Autowired

但是,我们也知道,当程序运⾏时,⽤的是具体的UserService对象、OrderService对象,那这些对象是什么时候创建的?谁创建的?包括对象⾥的属性是什么时候赋的值?谁赋的?所有这些都是我们程序员做的,以为我们只是写了类⽽已,所有的这些都是Spring做的,它才是幕后⿊⼿。

这就是控制:

  1. 控制对象的创建(applicationContext.xml定义的bean标签就是创建对象)
  2. 控制对象内属性的赋值(类中使用到了@Autowire和@Resources等注入注解)

如果我们不⽤Spring,那我们得⾃⼰来做这两件事,反过来,我们⽤Spring,这两件事情就不⽤我们做 了,我们要做的仅仅是定义类,以及定义哪些属性需要Spring来赋值(⽐如某个属性上加 @Autowired),⽽这其实就是第⼆个问题的答案,这就是反转,表示⼀种对象控制权的转移。

那反转有什么⽤,为什么要反转? 如果我们⾃⼰来负责创建对象,⾃⼰来给对象中的属性赋值,会出现什么情况? ⽐如,现在有三个类:

  1. A类,A类⾥有⼀个属性C c;
  2. B类,B类⾥也有⼀个属性C c;
  3. C类

现在程序要运⾏,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除开定义这三个类之外,我们还得写:

  1. A a = new A();
  2. B b = new B();
  3. C c = new C();
  4. a.c = c;
  5. b.c = c;

这五⾏代码是不⽤Spring的情况下多出来的代码,⽽且,如果类在多⼀些,类中的属性在多⼀些,那相应的代码会更多,⽽且代码会更复杂。所以我们可以发现,我们⾃⼰来控制⽐交给Spring来控制,我们的代码量以及代码复杂度是要⾼很多的,反⾔之,将对象交给Spring来控制,减轻了程序员的负担。 总结⼀下,IOC表示控制反转,表示如果⽤Spring,那么Spring会负责来创建对象,以及给对象内的属性赋值,也就是如果⽤Spring,那么对象的控制权会转交给Spring。

定义:将对象的控制权交给Spring框架(反转:控制权转移),由Spring进行对象的创建和对象内属性的赋值(控制:由Spring进行对象的创建和属性赋值)。

 作用:减少代码量和降低代码复杂度。

2,单例bean和单例模式

单例模式表示JVM中某个类的对象只会存在唯⼀⼀个。

单例Bean并不表示JVM中只能存在唯⼀的某个类的Bean对象。

Java经典面试题笔记_第5张图片

3,Spring的事务传播机制

多个事务⽅法相互调⽤时,事务如何在这些⽅法间传播,⽅法A是⼀个事务的⽅法,⽅法A执⾏过程中调⽤了⽅法B,那么⽅法B有⽆事务以及⽅法B对事务的要求不同都会对⽅法A的事务具体执⾏造成影响,同时⽅法A的事务对⽅法B的事务执⾏也有影响,这种影响具体是什么就由两个⽅法所定义的事务传播类型所决定。

  1. REQUIRED(Spring默认的事务传播类型required):如果当前没有事务,则⾃⼰新建⼀个事务,如果当前存在事务,则加⼊这个事务
  2. SUPPORTS:当前存在事务,则加⼊当前事务,如果当前没有事务,就以⾮事务⽅法执⾏
  3. MANDATORY:当前存在事务,则加⼊当前事务,如果当前事务不存在,则抛出异常。
  4. REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。 单例Bean和单例模式
  5. NOT_SUPPORTED:以⾮事务⽅式执⾏,如果当前存在事务,则挂起当前事务
  6. NEVER:不使⽤事务,如果当前事务存在,则抛出异常
  7. NESTED:如果当前事务存在,则在嵌套事务中执⾏,否则REQUIRED的操作⼀样(开启⼀个事务)

4,Spring事务什么时候会失效呢? 

 spring事务的原理是AOP,进⾏了切⾯增强,那么失效的根本原因是这个AOP不起作⽤了!常⻅情况有 如下⼏种

  1. 发⽣⾃调⽤,类⾥⾯使⽤this调⽤本类的⽅法(this通常省略),此时这个this对象不是代理类,⽽是 UserService对象本身! 解决⽅法很简单,让那个this变成UserService的代理类即可!
  2. ⽅法不是public的:@Transactional只能⽤于 public 的⽅法上,否则事务不会失效,如果要⽤在⾮public ⽅法上,可以开启 AspectJ 代理模式。(AOP机制)
  3. 数据库不⽀持事务(数据库事务)
  4. 没有被spring管理
  5. 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

 5,Spring中Bean是线程安全的吗?

Spring本身并没有针对Bean做线程安全的处理,所以:

  1. 如果Bean是⽆状态的,那么Bean则是线程安全的
  2. 如果Bean是有状态的,那么Bean则不是线程安全的

有状态理解为:Bean对象在运行过程中,其内部属性会发生增删改等操作,即属性在运行的时候可能会变。

另外,Bean是不是线程安全,跟Bean的作⽤域没有关系,Bean的作⽤域只是表示Bean的⽣命周期范 围,对于任何⽣命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身(方法在进行操作某些变量时,有没有用sychronized或ReentrantLock加锁进行线程安全的处理等)。

 6,ApplicationContext和BeanFactory有什么区别?

BeanFactory是Spring中⾮常核⼼的组件,表示Bean⼯⼚,可以⽣成Bean,维护Bean,⽽ ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也 是⼀个Bean⼯⼚,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如 EnvironmentCapable、MessageSource、ApplicationEventPublisher等接⼝,从⽽ ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的。

7,Spring中的事务是如何实现的?

  1. Spring事务底层是基于数据库事务和AOP机制的
  2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
  3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
  4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重要的⼀步
  6. 然后执⾏当前⽅法,⽅法中会执⾏sql
  7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql。

8,请解释 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 接口,那将会调用以上几个步骤完成后,Bean 就已经被正确创建了,之后就可以使用这个 Bean 了。 postProcessBeforeInitialization(Object obj, String s)方法。由于这个方法是在 Bean 初始化结束时调用的,所以可以被应用于内存或缓存技术;
  5.  InitializingBean 与 init-method: 如果 Bean 在 Spring 配置文件中配置了 init-method 属性,则会自动调 用其配置的初始化方法。
  6. 如果这个 Bean 实现了 BeanPostProcessor 接口,将会调用 postProcessAfterInitialization(Object obj, String s)方法;
  7. DisposableBean: 当 Bean 不再需要时,会经过清理阶段,如果Bean 实现了 DisposableBean 这个接口,会调用其实现的 destroy()方法;
  8.  destroy-method: 最后,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会 自动调用其配置的销毁方法

 9,Spring中什么时候@Transactional会失效

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是代理对象调⽤时, 那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional是会失效的

同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类(动态代理)来实现的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效

AOP的两个动态代理实现方式:

Java经典面试题笔记_第6张图片

你可能感兴趣的:(java,笔记,面试)