【Java】面试题
Java基础
1、浮点数、BigDecimal
1.1 为什么不能用浮点数表示金额?
十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。但是不是所有小数都能用二进制精确表示。
比如0.1,是没办法将他转换成一个确定的二进制数的。因为他乘2取整后会进入一个循环。
参考链接:【Java基础】不能用浮点数表示金额
1.2 为什么不能用BigDecimal的equals方法做等值比较?
因为BigDecimal的equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值(value)和标度(scale),而对于0.1和0.10这两个数字,他们的值虽然一样,但是精度是不一样的,所以在使用equals比较的时候会返回false。
参考链接:
https://www.yuque.com/hollis666/wk6won/qmx8yss8tve7w73q
1.3 BigDecimal(double)和BigDecimal(String)有什么区别?
浮点数不一定能精确地表示小数,double是不精确的。使用一个不精确的数字来创建BigDecimal,得到的数字也是不精确的。如0.1这个数字,double只能表示他的近似值。
所以,当使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。
而是0.1000000000000000055511151231257827021181583404541015625。这是因为double自身表示的只是一个近似值。
而对于BigDecimal(String) ,当我们使用new BigDecimal(“0.1”)创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。他的标度也就是1
参考链接:
https://www.yuque.com/hollis666/wk6won/tv3ne5taonetgiip
2、Java语法糖
参考链接:【Java基础】常见语法糖
2.1 字符串的switch的工作原理?
字符串的switch是通过hashCode()和equals()方法来实现的。
进行switch的是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。
2.2 一个类有两个重载函数,函数名一样,其中一个函数参数是List<String>,另一个函数参数是List<Integer>,问这个类能编译通过么?为什么?
无法编译通过,泛型擦除之后,2个函数的方法签名就是一模一样了。即泛型不可以重载。
3、Java的动态代理如何实现?
在Java中,实现动态代理有两种方式:
- JDK动态代理:Java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。
- Cglib动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。
参考链接:https://www.yuque.com/hollis666/wk6won/ugvfzx
4、异常处理
参考链接:【Java基础】异常处理
4.1 代码分层以及异常处理细节
问题:新增一个http接口,该接口的业务逻辑主要是调用下游rpc接口,并对结果做简单处理。开发过程和关注点有哪些?
不要直接新建controller类,新增方法后,在该方法里调用下游rpc接口,并做后续逻辑处理。
注意需要分3层:controller、service、integrationService。
注意integrationService里要封装自定义异常,要提供入参让调用者选择是否吃异常还是想上抛异常。
4.2 try…catch…finally执行顺序和返回结果逻辑
问题:变量i初始值是1,try中执行i++并return变量i,catch中设置i=66并return变量i,finally中设置i=100并return变量i,返回结果是多少?finally中只设置i=100不return变量i,返回结果是多少?
如果finally块中有return语句,则其返回值将是整个try-catch-finally结构的返回值。
如果finally块中没有return语句,则try或catch块中的return语句(取决于哪个执行了)将确定最终的返回值。
因此finally中有return,返回结果是100。没有return,返回结果是2。
5、怎么实现深拷贝?
1、实现Cloneable接口,重写clone()
2、序列化实现深拷贝
参考链接:https://www.yuque.com/hollis666/wk6won/br3qgdim5xz2pngx
6、SimpleDateFormat是线程安全的吗?使用时应该注意什么?如何解决?
1、使用时注意不要定义为static变量。

2、解决方式
- 作为局部变量使用
- 使用时加同步锁
- 使用ThreadLocal
参考链接:https://www.yuque.com/hollis666/wk6won/gyz59h
7、字符串不可变性、string的intern原理
参考链接:【Java基础】字符串不可变性、string的intern原理
7.1 string是可变类型,还是不可变类型?实现的原理是什么?为什么这么设计?
string是不可变类型。
实现原理:
- String类被声明为final,这意味着它不能被继承。那么他里面的方法就是没办法被覆盖的。
- 用final修饰字符串内容的char[](从JDK 1.9开始,char[]变成了byte[]),由于该数组被声明为final,一旦数组被初始化,就不能再指向其他数组。
- String类没有提供用于修改字符串内容的公共方法。例如,没有提供用于追加、删除或修改字符的方法。如果需要对字符串进行修改,会创建一个新的String对象。
设计原理:
- 缓存:字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以节省堆空间。
- 安全性:字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
- 线程安全:不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。
- hashcode缓存:由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。
7.2 string的intern原理
分别说出下面2段代码的结果和原因:
1、第②行,对 s1执行 intern,但是因为"a"这个字符串已经在字符串池中,所以会直接返回原来的引用,但是并没有赋值给任何一个变量。
所以s1和s2不相等。
2、第⑥行,对 s3 执行 intern,但是目前字符串池中还没有"aa"这个字符串,于是会把<s3指向的String对象的引用>放入<字符串常量池>。
第⑦行,因为"aa"这个字符串已经在字符串池中,所以会直接返回原来的引用,并赋值给 s4;
所以s3和s4相等。
public static void main(String[] args) {
String s1 = new String("a"); // ①
s1.intern(); // ②
String s2 = "a";// ③
System.out.println(s1 == s2); // ④ false
String s3 = new String("a") + new String("a");// ⑤
s3.intern();// ⑥
String s4 = "aa";// ⑦
System.out.println(s3 == s4);// ⑧ true
}
7.3 什么时候使用intern
我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。
这时候,对于那种可能经常使用的字符串,使用intern进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。
8、面向对象的设计原则
面向对象主要有5大设计原则
1、单一职责原则(Single-Responsibility Principle)
意思是:一个类最好只做一件事。
优点是:提高可维护性和减少代码修改的影响。
当一个类只负责一个功能时,其实现通常更简单、更直接,这使得理解和维护变得更容易。修改的时候,只用修改这个类即可,不用修改多个地方。
2、开放封闭原则(Open-Closed principle)
意思是:对扩展开放、对修改封闭。
优点是:降低风险。
可以在不修改现有代码的情况下扩展功能,这意味着新的功能可以添加,而不会影响旧的功能。由于不需要修改现有代码,因此引入新错误的风险较低。
3、Liskov替换原则(Liskov-Substituion Principle)
意思是:子类必须能够替换其基类。
优点是:提高代码的互换性和复用性。
能够用派生类的实例替换基类的实例,使得代码更加模块化,提高了其灵活性。遵循LSP的类和组件更容易被重用于不同的上下文。
4、依赖倒置原则(Dependency-Inversion Principle)
意思是:程序要依赖于抽象接口,而不是具体的实现。
优点是:减少系统耦合。
系统的高层模块不依赖于低层模块的具体实现,从而使得系统更加灵活和可维护。
5、接口隔离原则(Interface-Segregation Principle)
意思是:使用多个小的专门的接口,而不要使用一个大的总接口。
优点是:减少系统耦合和降低风险。
通过使用专门的接口而不是一个大而全的接口,系统中的不同部分之间的依赖性减少了。同时更改一个小接口比更改一个大接口风险更低,更容易管理。
Java集合类
1、集合排序的方式有哪些?
- 实体类自己实现Comparable接口比较
- 借助比较器进行排序。
- 借助Stream进行排序,借助Stream的API,底层还是通过Comparable实现的。
参考链接:【Java集合类】集合排序的方式有哪些?
2、ArrayList的subList方法有什么需要注意的地方吗?
- 对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。
- 对子List做结构性修改,操作同样会反映到父List上。
- 对父List做结构性修改,会抛出异常ConcurrentModificationException。

参考链接:https://www.yuque.com/hollis666/wk6won/em9xr6
3、HashMap原理
参考链接:【Java集合类】HashMap实现原理
3.1 HashMap如何定位key?
先通过 (table.length - 1) & (key.hashCode ^ (key.hashCode >>> 16))定位到key位于哪个下标中,然后再通过key.equals(rowKey)来判断两个key是否相同,综上,是先通过hashCode和equals来定位KEY的。
3.2 ConcurrentHashMap是如何保证线程安全的?
在JDK 1.7中,ConcurrentHashMap使用了分段锁技术,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。
在JDK 1.8中,ConcurrentHashMap的实现方式进行了改进,使用节点锁的思想,即采用“CAS+Synchronized”的机制来保证线程安全。
在JDK 1.8中,ConcurrentHashMap会在添加元素时,如果某个节点为空,那么使用CAS操作来添加新节点;如果节点不为空,使用Synchronized锁住当前节点,再次尝试put。这样可以避免分段锁机制下的锁粒度太大,以及在高并发场景下,由于线程数量过多导致的锁竞争问题,提高了并发性能。
Java并发
1、什么是死锁,如何解决?
死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可强行占有:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何解除死锁:
最常用的避免方法就是破坏循环等待,就是当我们有多个事务的时候,最好让这几个事务的执行顺序相同。
参考:https://www.yuque.com/hollis666/wk6won/mtdxsd
2、什么是Java内存模型(Java Memory Model ,JMM)?
Java内存模型规定了所有的共享变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。比如synchronized、volatile等关键字。
参考链接:https://www.yuque.com/hollis666/wk6won/hmi3m1
3、AtomicInteger类的getAndIncrement方法是怎么保证多线程情况下数据的正确性?
AtomicInteger类是一个支持原子操作的Integer类,getAndIncrement方法主要是通过cas实现保证了线程安全,同时该变量用volatile修饰,保证了可见性。
4、谈谈对volatile的理解?
volatile是Java虚拟机提供的轻量级的同步机制,可以保证有序性和可见性,但是不能保证原子性 。
1、可见性:如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主内存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主内存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
如果不保证可见性的话,一个全局共享变量,某个线程更新值之后,其他线程感知不到,就认为还是之前的值。
2、有序性:volatile通过内存屏障来禁止指令重排,保证代码程序会严格按照代码的先后顺序执行。这就保证了有序性。
如果不保证有序性的话,比如双重校验锁(先判空,对象为空则加锁并再次判空)还是存在问题的可能性。对象的创建分为3步:分配内存空间,初始化、设置对象地址。不保证有序性的话,可能某个线程先执行了步骤1和3,此时对象已经不为空了,其他线程会认为对象已经创建好了,而此时对象实际还没有初始化,其他对象使用会报NPE。
3、原子性:不保证原子性,比如全局共享int类型变量,执行i++,结果可能不正确。需要保证原子性的话,需要使用synchronized,或者使用使用JUC包下的AtomicInteger。
参考链接:Java基础:volatile详解
https://www.yuque.com/hollis666/wk6won/aylaul
5、synchronized的锁优化有哪些?
1、自旋锁
重量级锁如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,因此状态转换需要花费很多的处理器时间。
而共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。
引入自旋锁或者说轻量级锁,程序在用户态自旋一会儿,如果能很快获取到锁。那就比重量级锁要节省不少时间和资源。
2、锁消除
在动态编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。
3、锁粗化
如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。
当JIT发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。
6、为什么不建议通过Executors构建线程池?构建线程池的核心线程数怎么设定?
Executors类看起来功能是比较强大的,用到了工厂模式、又有比较强的扩展性,创建线程池比较方便。比如Executors.newFixedThreadPool可以创建一个固定大小的线程池。但是阿里巴巴Java开发手册中明确指出,不允许使用Executors创建线程池。

构建线程池核心线程数的通用公式:
线程数 = C P U 核心数 × 目标 C P U 利用率 × ( 1 + 等待时间 / 计算时间 ) 1 线程数=CPU核心数×目标CPU利用率×\frac{(1+等待时间/计算时间)}{1} 线程数=CPU核心数×目标CPU利用率×1(1+等待时间/计算时间)
在这个公式中,"等待时间 / 计算时间"的比例是一个关键因素,它帮助决定合适的线程数量以平衡CPU利用和等待效率。
7、如何理解AQS
AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中。AQS 是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等都是基于 AQS 实现的。
在AQS内部,维护了一个FIFO队列和一个volatile的int类型的state变量。在state=1的时候表示当前对象锁已经被占有了,state变量的值修改的动作通过CAS来完成。
FIFO队列用来实现多线程的排队工作,当线程加锁失败时,该线程会被封装成一个Node节点来置于队列尾部。

8、ThreadLocal为什么会导致内存泄漏?如何解决的?
Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。

情况1:栈上的ThreadLocal Ref引用不在使用了,即方法结束后这个对象引用就不再用了,那么,ThreadLocal对象因为还有一条引用链在,所以就会导致他无法被回收,久而久之可能就会对导致OOM。

针对情况1,ThreadLocalMap使用了弱引用解决内存泄漏。
从ThreadLocal的内部静态类Entry的代码设计可知,ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,从这个层面来说,可以理解ThreadLocalMap中的键是ThreadLocal的弱引用。
如果用了弱引用,那么ThreadLocal对象就可以在下次GC的时候被回收掉了。
这样做可以很大程度上的避免因为ThreadLocal的使用而导致的OOM问题,但是这个问题却无法彻底避免。
情况2:Thread对象如果一直在被使用,比如在线程池中被重复使用,那么从这条引用链就一直在,那么就会导致ThreadLocalMap无法被回收。

针对情况2。虽然key是弱引用,但是value的那条引用,还是个强引用。而且他的生命周期是和Thread一样的,也就是说,只要这个Thread还在, 这个对象就无法被回收。
那么,什么情况下,Thread会一直在呢?那就是线程池。
在线程池中,重复利用线程的时候,就会导致这个引用一直在,而value就一直无法被回收。因此在线程池中使用ThreadLocal会有内存泄漏风险。
ThreadLocalMap的每次get、set、remove,都会清理key为null,但是value还存在的Entry。
所以,当在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。
JVM
1、Java中的对象一定在堆上分配内存吗?
不一定,在HotSpot虚拟机中,存在JIT优化的机制,JIT优化中可能会进行逃逸分析,当经过逃逸分析发现某一个局部对象没有逃逸到线程和方法外的话,那么这个对象就可能不会在堆上分配内存,而是进行栈上分配。
2、JVM运行时内存区域有哪些?方法区是如何实现的?
根据Java虚拟机规范的定义,JVM的运行时内存区域主要由Java堆、虚拟机栈、本地方法栈、方法区和程序计数器以及运行时常量池组成。其中堆、方法区以及运行时常量池是线程之间共享的区域,而栈(本地方法栈+虚拟机栈)、程序计数器都是线程独享的。
在JDK 1.7及之前的版本中,方法区通常被实现为永久代(Permanent Generation),用于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。不过在1.6中,方法区中包含了字符串常量池,而在1.7中,把字符串常量池、和静态变量都移到了堆内存中。
从JDK 1.8开始,HotSpot虚拟机对方法区的实现进行了重大改变。永久代被移除,取而代之的是元空间(Metaspace)。元空间是使用本地内存(Native Memory)来存储类的元数据信息的,它不再位于堆内存中。
3、JVM如何判断对象是否存活?
3.1 引用计数法。
这个方法实现简单,效率高,但是很难解决对象之间相互循环引用的问题。
3.2 可达性分析法。
通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
GC Roots包含:系统类加载器加载的类、活着的线程、方法中的本地变量、被synchronized锁定的对象等。
可达性分析算法需要对程序进行全局分析,需要很长的时间才能完成分析,并且整个过程都是STW的,所以对应用的整体性能有很大影响。
3.3 三色标记法
三色标记法将对象分为三种状态:白色、灰色和黑色。
三色标记法的标记过程可以分为三个阶段:初始标记(Initial Marking)、并发标记(Concurrent Marking)和重新标记(Remark)。
以上三个标记阶段中,初始标记和重新标记是需要STW的,而并发标记是不需要STW的。其中最耗时的其实就是并发标记的这个阶段,因为这个阶段需要遍历整个对象树,而三色标记把这个阶段做到了和应用线程并发执行,大大降低了GC的停顿时长。
4、对象在什么情况下会晋升到老年代?
一般情况下,对象将在新生代进行分配,首先会尝试在Eden区分配对象,当Eden内存耗尽,无法满足新的对象分配请求时,将触发新生代的GC(Young GC、MinorGC),在新生代的GC过程中,没有被回收的对象会从Eden区被搬运到Survivor区,这个过程通常被称为"晋升"
同样的,对象也可能会晋升到老年代,触发条件主要看对象的大小和年龄。对象进入老年代的条件有三个,满足一个就会进入到老年代:
- 躲过15次GC。每次垃圾回收后,存活的对象的年龄就会加1,累计加到15次(jdk8默认的),也就是某个对象躲过了15次垃圾回收,那么JVM就认为这个是经常被使用的对象,就没必要再待在年轻代中了。具体的次数可以通过 -XX:MaxTenuringThreshold 来设置在躲过多少次垃圾收集后进去老年代。
- 动态对象年龄判断。规则:如果在Survivor空间中小于等于某个年龄的所有对象大小的总和大于Survivor空间的一半时,那么就把大于等于这个年龄的对象都晋升到老年代。
- 大对象直接进入老年代。-XX:PretenureSizeThreshold 来设置大对象的临界值,大于该值的就被认为是大对象,就会直接进入老年代。(PretenureSizeThreshold默认是0,也就是说,默认情况下对象不会提前进入老年代,而是直接在新生代分配。然后就GC次数和基于动态年龄判断来进入老年代。)
5、YoungGC和FullGC的触发条件是什么?
5.1 YoungGC触发条件
1、YoungGC的触发条件比较简单,那就是当年轻代中的eden区分配满的时候就会触发。
5.2 FullGC触发条件
1、老年代空间不足
1) 创建一个大对象,超过指定阈值会直接保存在老年代当中,如果老年代空间也不足,会触发Full GC。
2) YoungGC之后,发现要移到老年代的对象,老年代存不下的时候,会触发一次FullGC。
2、空间分配担保失败
在每一次执行YoungGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
如果大于,那么说明本次Young GC是安全的。
如果小于,那么虚拟机会查看HandlePromotionFailure 参数设置的值判断是否允许担保失败。如果值为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小(一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考)。如果大于,则尝试进行一次YoungGC,但这次YoungGC依然是有风险的;如果小于,或者HandlePromotionFailure=false,则会直接触发一次Full GC。
但是,需要注意的是HandlePromotionFailure这个参数,在JDK 7中就不再支持了,默认该参数为true。
3、代码中执行System.gc()
代码中执行System.gc()的时候,会触发FullGC,但是并不保证一定会立即触发。
4、永久代空间不足
如果有永久代的话,当在永久代分配空间时没有足够空间的时候,会触发FullGC。从JDK1.8及其之后,用元空间取代了永久代。
6、项目中如何选择垃圾回收器?
1、根据机器情况判断,如果是单核机器,或者内存较小的机器,则选择Serial GC。
2、根据业务类型判断,看你的应用更在意的是吞吐量还是 STW 的时长。比如批处理任务的应用,更在意的就是吞吐量,选择并行收集器Parallel。
3、实时交易系统,更在意的就是 STW 的时长,则选择并发收集器CMS/G1。此时如果内存小于4G,则使用CMS。一把来说,我们认为至少达到4G 以上才可以用 G1、ZGC 等,通常要比如超过8G、16G 这样效果才更好。
4、然后看JDK版本小于11,则使用G1。JDK版本大于等于11,则使用ZGC。
7、什么是双亲委派?如何破坏?什么场景下需要破坏?
当加载类的时候,需要用到类加载器将类从外部加载到JVM的内存当中。类加载的代码都集中在java.lang.ClassLoader类的loadClass方法中,主要有下面3个步骤:
- 先查缓存,如果该类已经被加载过,则不加载。
- 若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父加载器。
- 如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。在findClass方法中,会调用defineClass方法将二进制文件加载为类。
双亲委派模型主要是由ClassLoader类的loadClass方法实现的,只需要自定义类加载器,并且重写其中的loadClass方法,即可破坏双亲委派模型。
破坏双亲委派的例子–TOMCAT
一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的,如果采用默认的类加载机制,那么就会无法加载多个相同的类。Tomcat 为了实现隔离性,所以并没有完全遵守双亲委派的原则。
8、JVM中一个Java对象的结构是怎样的?
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头、实例数据以及对齐填充。
1、对象头:对象头信息是与对象自身定义的数据无关的额外存储成本,主要包含2部分:Mark Word(标记字)和Class Metadata Address(类元数据地址)。Mark Word中用于存储对象的标记信息,包括对象的锁状态、GC标记等。Class Metadata Address指向对象所属类的元数据信息,包括类的类型、方法、字段等。
2、实例数据:实例数据是对象的成员变量(字段)的实际存储区域,它包含了对象的各个字段的值。
3、对齐填充:对齐填充是为了使得对象的起始地址符合特定的对齐要求,以提高访问效率。
Spring
1、Spring Bean的生命周期是怎么样的?
bean的生命周期,大致可以分为以下5步,当然最核心的就是前3步。
1、bean的实例化:调用AbstractAutowireCapableBeanFactory类中的createBeanInstance方法,通过反射创建对象。
2、bean的属性赋值:通过PopulateBean方法完成属性复制。
3、bean的初始化:主要有4个步骤:
- 检查是否实现Aware接口,可以对Bean设置beanName/类加载器/beanFactory等属性
- 执行初始化前的增强方法:postProcessBeforeInitialization
- 执行初始化方法:invokeInitMethods
- 执行初始化前的增强方法:postProcessAfterInitialization
4、bean的使用
5、bean的销毁
2、spring是怎么解决循环依赖问题的?使用二级缓存可以解决么?为什么需要三级缓存?
首先明确循环依赖产生的原因,是2个或2个以上的对象存在相互依赖的情况,当bean在进行属性赋值的时候,就会去找它所需要的bean,当相互依赖时,就存在循环依赖问题。
解决循环依赖问题的核心其实就1个点:
实例化和初始化过程分离。在属性赋值前,已经进行了实例化了;
综上,其实使用2个缓存就可以解决循环依赖问题了。
那Spring为什么还需要三级缓存呢?
如果完全依靠二级缓存解决循环依赖,意味着当我们依赖了一个代理类的时候,就需要在Bean实例化之后完成AOP代理。而在Spring的设计中,为了解耦Bean的初始化和代理,是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理的。因此如果只用二级缓存的话,就违背了这个设计原则。
3、Spring的注解事务失效可能是哪些原因?
1、代理失效——@Transactional 应用在非 public 修饰的方法上。对象自调用,没有走到代理对象上。
2、代理失效——同一个类中方法调用。对象自调用,没有走到代理对象上。
3、代理失效——调用的是代理对象的final或者static方法,
4、代理失效——无代理对象,比如该bean没有被spring管理,是自己new的。
5、@Transactional使用错误——传播机制注解使用错误,导致内外层事务表现不符合预期。
6、@Transactional使用错误——rollbackFor 设置错误。
7、@Transactional使用错误——用错了注解引用,比如引用了javax.transaction.Transactional,不是spring的注解。
8、异常被内部捕获。
9、事务中用了多线程
4、BeanFactory和FactroyBean的关系?
首先BeanFactory和FactoryBean都是接口,都是用来创建bean的。
BeanFactory是Spring IoC容器的一个接口,用来获取Bean以及管理Bean的依赖注入和生命周期。常用的ApplicationContext就是实现了BeanFactory接口。
FactoryBean通常用于创建很复杂的对象,比如需要通过某种特定的创建过程才能得到的对象。比如Dubbo中的ReferenceBean就是实现了FactoryBean。ReferenceBean既有Spring的依赖注入,生命周期管理等特性,还可以延迟创建代理对象直到真正需要时,这样可以提升启动速度并减少资源消耗。
5、Springboot是如何实现自动配置的?
参考链接:springboot核心技术1:springboot自动装配原理
1、springboot的注解@SpringBootConfiguration中,有一个注解@EnableAutoConfiguration,@EnableAutoConfiguration注解中会Import类型为AutoConfigurationImportSelector的组件,该组件会读取ClassPath下面的META-INF/spring.factories文件。
2、spring.factories文件,它是一个典型的java properties文件,配置的格式为Key = Value形式。文件中配置了多个类,名称都是xxxAutoConfiguration。这些都是Spring Boot中的自动配置相关类;在启动过程中会解析对应类配置信息。每个Configuration都定义了相关bean的实例化配置。按照条件装配规则,比如使用ConditionalOnBean,@ConditionalOnMissingBean等,最终把所有相关bean都实例化出来。
2230

被折叠的 条评论
为什么被折叠?



