Java面试汇总:java基础、多线程、spring、jvm、分布式

Java面试汇总:java基础、多线程、spring、jvm、分布式

Java基础

1、面向对象的5大原则

面向对象的三大特征和五大原则

多线程

1、ThreadLocal

【Java并发】ThreadLocal

1.1 什么是ThreadLocal,如何实现的?

ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题。

ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构。key就是当前的ThreadLocal对象,而v就是我们想要保存的值。

Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。

1.2 ThreadLocal为什么会导致内存泄漏?如何解决的?

ThreadLocal的内存泄露问题是一个比较典型的问题,ThreadLocal帮我们解决了一半,还有一半需要开发者自己解决。

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

由于ThreadLocalMap中的键是ThreadLocal的弱引用,那么ThreadLocal对象就可以在下次GC的时候被回收掉。

case2:虽然key是弱引用,但是value的那条引用,还是个强引用。而且他的生命周期是和Thread一样的,也就是说,只要这个Thread还在, 这个对象就无法被回收。在线程池中,重复利用线程的时候,就会导致这个引用一直在,而value就一直无法被回收。因此在线程池中使用ThreadLocal会有内存泄漏风险

ThreadLocalMap的每次get、set、remove,都会清理key为null,但是value还存在的Entry。所以,当在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。

InheritableThreadLocal && TransmittableThreadLocal

当我们在同一个线程中,想要共享变量的话,是可以直接使用ThreadLocal的,但是如果在父子线程之间,共享变量,ThreadLocal就不行了。

与 ThreadLocal 不同,InheritableThreadLocal 可以在子线程中继承父线程中的值。在创建子线程时,子线程将复制父线程中的 InheritableThreadLocal 变量。

InheritableThreadLocal是用于主子线程之间传递参数,但是,这种方式有一个问题,那就是必须要是在主线程中手动创建的子线程才可以,而现在池化技术非常普遍了,很多时候线程都是通过线程池进行创建和复用,这时候InheritableThreadLocal就不行了。

TransmittableThreadLocal是阿里开源的一个方案 ,这个类继承并加强InheritableThreadLocal类,可以用来更好地实现线程之间的参数传递

2、线程

【Java并发】线程状态
创建线程的方式:

1、继承Thread类并重写run方法。

2、实现Runnable接口并重写run方法。将Runnable对象作为Thread的target来创建Thread。

3、实现Callable接口并重写call方法、创建FutureTask对象包装该Callable对象,将FutureTask对象作为Thread的target来创建Thread。

4、线程池

线程有几种状态,状态之间的流转是怎样的?

1、初始(NEW)、运行(RUNNABLE)、终止(TERMINATED)、

2、等待(WAITING)、超时等待(TIMED_WAITING)、

3、阻塞(BLOCKED)、

WAITING和BLOCKED的区别?

1、触发条件不一样

BLOCKED是锁被占用,等待进入 synchronized中。等待别人释放锁。

WAITING主动调用 wait()/join()/park(),主动释放了锁。等待别人通知(notify()/notifyAll()/unpark())后才会继续 。

2、等待队列不一样

BLOCKED是在Monitor的Entry List(入口队列 / 竞争锁的队列)中等待。

WAITING是在Monitor的Wait Set(等待队列)中等待。其他线程调用 notify()/notifyAll(),然后回到 Entry List 去竞争锁

线程同步的方式有哪些?

1、synchronized:synchronized是Java中最基本的线程同步机制,可以修饰代码块或方法。保证同一时间只有一个线程访问该代码块或方法,其他线程需要等待锁的释放。。

2、ReentrantLock:与synchronized关键字类似,但是更灵活。支持公平锁、可中断锁、多个条件变量等功能。

3、Semaphore:允许多个线程同时访问共享资源,但是限制访问的线程数量。可以用于控制并发访问的线程数量,避免系统资源被过度占用。

4、CountDownLatch:一个或多个线程等待其他线程执行完毕之后再执行。计数器归0后,不可重置,再次调用await()方法会直接通过,因此不可重用。典型场景是一等多(主线程等多个子线程)或者只需一次等待。

5、CyclicBarrier:一组线程在一个栅栏处相互等待,直到所有线程都到达栅栏位置之后,才会继续执行。栅栏打卡后,会自动重置计数器,因此可以重用。典型场景是多等多(多个线程相互等待同步执行)或者需要重复使用。

3、AQS

AbstractQueuedSynchronizer (抽象队列同步器)出现在 JDK 1.5 中。AQS 是很多同步器的基础框架,比如 ReentrantLock、Semaphore、CountDownLatch 和 CyclicBarrier 等都是基于 AQS 实现的。

在AQS内部,维护了一个FIFO队列和一个volatile的int类型的state变量。在state=1的时候表示当前对象锁已经被占有了,state变量值的修改动作通过CAS来完成

FIFO队列用来实现多线程的排队工作,当线程加锁失败时,该线程会被封装成一个Node节点来置于队列尾部

4、synchronized

synchronized 是 Java 中的一个很重要的关键字,主要用来加锁,synchronized 所添加的锁有以下几个特点:互斥性、阻塞性、可重入性。

当多个线程同时访问一段同步代码时,未抢到锁的线程会进入 _EntryList 队列中。若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor,同时该线程进入 _WaitSet 集合中等待被唤醒。

synchronized是如何保证原子性、有序性、可见性?

1、原子性是通过 monitorenter 和 monitorexit 这两个字节码指令实现的。当线程执行到 monitorenter 的时候要先获得锁,才能执行后面的方法。当线程执行到 monitorexit 的时候则要释放锁。

2、as-if-serial语义保证了单线程中,不管怎么重排序,单线程程序的执行结果都不能被改变

3、被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值

synchronized的锁升级过程是怎样的?

1、无锁。

2、偏向锁:首次进入synchronized块时自动开启,假设JVM启动参数没有禁用偏向锁。需要注意,在JDK 15中,偏向锁已被废除。

3、轻量级锁(Lightweight Locking):当有另一个线程尝试获取已被偏向的锁时,偏向锁会升级为轻量级锁。

4、重量级锁(Heavyweight Locking):当轻量级锁的CAS操作失败,轻量级锁升级为重量级锁。

synchronized和reentrantLock区别?

ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。二者相同点是,都是可重入锁。二者也有很多不同,如:

  1. synchronized是Java内置特性,而ReentrantLock是通过Java代码实现的。
  2. synchronized是可以自动获取/释放锁的,但是ReentrantLock需要手动获取/释放锁。
  3. ReentrantLock还具有响应中断、超时等待等特性。
  4. ReentrantLock可以实现公平锁和非公平锁,而synchronized只是非公平锁。

5、Volatile

volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:

1)保证可见性

2)不保证原子性

3)禁止指令重排

6、如何实现主线程捕获子线程异常?

在Java中,主线程不能直接捕获子线程抛出的异常!主要是因为子线程和主线程是独立的执行单元,它们的执行是并发的,因此主线程无法捕获子线程的异常。子线程的异常通常由子线程自己处理或通过适当的异常处理机制处理。线程隔离,这也是Java保证线程出现异常不会影响整个进程的一个主要原理。

方法一:使用Future捕获子线程异常

如果想要在主线程能够捕获子线程的异常,可以考虑使用Callable和Future,它们允许主线程获取子线程的执行结果和异常。这样,主线程可以检查子线程是否抛出了异常。调用future的get方法时,可以捕获到ExecutionException

方法二:使用UncaughtExceptionHandler处理子线程异常

UncaughtExceptionHandler 是 Java 中的一个接口,用于处理未捕获异常,即那些没有被 try-catch 块捕获的异常。它允许定义自定义异常处理器,以便在线程出现未捕获异常时采取特定的操作。可以为线程设置一个自定义的未捕获异常处理器,当线程抛出未捕获异常时,该处理器会被调用,我们可以在其中记录异常信息、执行清理操作等

方法三:使用CompletableFuture捕获子线程异常

可以使用CompletableFuture的handle方法处理正常结果和异常,也可以使用exceptionally方法仅处理异常。

同时在每一个子线程的任务中,建议也做一下try...catch的动作,在任务中查看详细的报错信息。

Spring

1、Bean生命周期

【Spring】Spring Bean的生命周期

bean的生命周期,大致可以分为以下5步。

第1步是bean的实例化。在堆内存申请空间,通过反射创建对象。

第2步是bean的属性赋值。

对象属性赋值,在populateBean方法中通过set完成属性赋值,如果有@Autowired注解,则进行属性填充,这里可能存在循环依赖问题。

第3步是bean的初始化。

首先是检查对象是否实现Aware接口,可以对Bean设置beanName/类加载器/beanFactory等属性。

然后执行初始化前增强方法postProcessBeforeInitialization。

然后执行初始化方法,先调用执行afterPropertiesSet方法,再调用init-method方法。

最后执行初始化后增强方法postProcessAfterInitialization,在postProcessAfterInitialization方法中,会判断是否需要创建代理对象。

第4步是bean的使用。

第5步是bean的销毁,调用destory-method方法。

2、Spring的三级缓存、循环依赖

singletonObjects是一级缓存,存储的是完整创建好的单例bean对象。在创建一个单例bean时,会先从singletonObjects中尝试获取该bean的实例,如果能够获取到,则直接返回该实例,否则继续创建该bean。

earlySingletonObjects是二级缓存,存储的是尚未完全创建好的单例bean对象。在创建单例bean时,如果发现该bean存在循环依赖,则会先创建该bean的"半成品"对象,并将"半成品"对象存储到earlySingletonObjects中。当循环依赖的bean创建完成后,Spring会将完整的bean实例对象存储到singletonObjects中,并将earlySingletonObjects中存储的代理对象替换为完整的bean实例对象。这样可以保证单例bean的创建过程不会出现循环依赖问题。

singletonFactories是三级缓存,存储的是单例bean的创建工厂。当一个单例bean被创建时,Spring会先将该bean的创建工厂存储到singletonFactories中,然后再执行创建工厂的getObject()方法,生成该bean的实例对象。在该bean被其他bean引用时,Spring会从singletonFactories中获取该bean的创建工厂,创建出该bean的实例对象,并将该bean的实例对象存储到singletonObjects中。

三级缓存是怎么解决循环依赖的?可以改成2级缓存解决这个问题么?

Spring之所以可以解决循环依赖就是因为对象的初始化是可以延后的,也就是说,当创建一个Bean ServiceA的时候,会先把这个对象实例化出来,然后再初始化其中的serviceB属性。

是可以改成2级缓存解决这个问题的。但是违背了Spring设计原则,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理

Spring解决循环依赖的限制?

1、首先要求互相依赖的Bean必须要是单例的Bean。

2、另外就是依赖注入的方式不能都是构造函数注入的方式。

都是构造函数注入的循环依赖,也可以通过一定的手段解决:

1、重新设计:彻底消除循环依赖。循环依赖一般都是设计不合理导致的,可以从根本上做一些重构,来彻底解决,

2、改成非构造器注入:可以改成setter注入或者字段注入。

3、使用@Lazy解决。

3、Spring事务

3.1 Spring的事务传播机制有哪些?

spring有7种事务传播机制:

  1. REQUIRED:默认的传播特性,如果当前没有事务,则新建一个事务;如果当前存在事务,则加入这个事务
  2. SUPPORTS:当前存在事务,则加入当前事务;如果当前没有事务,则以非事务的方式执行
  3. MANDATORY:当前存在事务,则加入当前事务;如果当前事务不存在,则抛出异常
  4. REQUIRED_NEW:创建一个新事务,如果存在当前事务,则挂起该事务
  5. NOT_SUPPORTED:以非事务方式执行,如果存在当前事务,则挂起当前事务
  6. NEVER:不使用事务,如果当前事务存在,则抛出异常
  7. NESTED:如果当前事务存在,则在嵌套事务中执行;如果当前不存在事务,则创建新事务

REQUIRED, REQUIRES_NEW, NESTED对比
REQUIRED(spring默认级别)外部和内部方法,任何一个方法异常,都会全部回滚。即使外部方法使用了try...catch处理了内部方法的异常,外部依然回滚。因为当前存在事务,则加入这个事务,所以共用的是一个事务,即同一个数据库连接。

REQUIRES_NEW外部异常,内部不回滚;内部异常,根据外部是否catch来决定是否回滚。

NESTED外部异常,内部回滚;内部异常,根据外部是否catch来决定是否回滚。

3.2 Spring事务失效可能是哪些原因?

1、代理失效的情况

1.1 @Transactional 应用在非 public 修饰的方法上

1.2 同一个类中方法调用,导致@Transactional失效

1.3 final、static方法

1.4 没有代理

2、 @Transactional用的不对

2.1 @Transactional 注解属性 propagation事务传播机制 设置错误

2.2 @Transactional 注解属性 rollbackFor 事务异常回滚机制 设置错误

2.3 用错注解

3、异常被捕获

事务中用了多线程

4、谈谈对spring ioc的理解,原理和实现?

ioc(Inversion Of Control)是一个理论思想,DI(Dependency Injection)是具体实现。

原来的对象是由使用者来进行控制,有了spring ioc之后,可以把整个对象交给spring来帮我们进行管理

DI:依赖注入,把对应的属性的值注入到具体的对象中,@Autowired, populateBean完成属性值的注入。

容器:存储对象,使用map结构来存储,在spring中存在三级缓存,singletonObjects存放完整的bean对象。bean的整个生命周期从创建到使用到销毁的过程,全部都是由容器来管理。

5、AOP的原理

AOP(Aspect Oriented Programming)表示面向切面编程,通过AOP可以实现公共的功能,避免与业务代码耦合。

AOP是在bean对象初始化步骤中执行postProcessorAfterInitialization时得到的。

产生AOP的前提,是要在配置类上加@EnableAspectJAutoProxy注解,该注解最终会为spring容器注册一个类型为AnnotationAwareAspectJAutoProxyCreator的组件。

正是因为注册了该组件,在createBean的postProcessAfterInitialization方法中,最终会调用

AnnotationAwareAspectJAutoProxyCreator类的wrapIfNecessary方法,即如果有必要的话就进行包装。

在wrapIfNecessary方法中,先获取bean的增强方法。当增强方法不为空,就调用createProxy方法创建一个代理对象。

6、spring的自动装配机制

1、@SpringBootConfiguration注解中添加了@EnableAutoConfiguration注解,@EnableAutoConfiguration注解中会Import类型为AutoConfigurationImportSelector的组件,该组建中默认扫描当前系统里面所有META-INF/spring.factories位置的文件,给容器中加载所有的自动配置类xxx-AutoConfiguration。

2、每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。即xxxAutoConfiguration和xxxProperties进行了绑定,相关参数会从xxxProperties里面拿。然后按照条件装配规则,比如使用ConditionalOnBean,@ConditionalOnMissingBean等。

3、如果需要定制化配置。如果需要替换组件,可以查看配置类是怎么配的,可以自己使用@Bean进行替换。如果要修改参数:可以先查看xxxAutoConfiguration-->xxx组件-->xxxProperties-->application.properties里进行对应修改。

7、Gateway和Nginx有什么区别?为什么需要SpringCloud Gateway,他起到了什么作用?

Gateway和Nginx有什么区别?

直接拿Gateway和Nginx对比并不一定不合适,因为Gateway中的负载均衡的功能是靠LoadBalancer实现的。而loadbalancer和nginx的主要区别是,虽然都是负载均衡,但是loadbalancer是客户端负载均衡,而nginx是服务端负载均衡

为什么需要SpringCloud Gateway,他起到了什么作用?

  • 路由转发
  • 负载均衡
  • 统一授权
  • 流量过滤
  • 限流降级
  • 跨域支持

8、服务通信中的Dubbo和Feign有什么区别?

Dubbo 是一个高性能的、基于 Java 的开源RPC(远程过程调用)框架。它主要用于构建高性能和透明化的服务间远程调用的微服务架构。Dubbo 支持多种通信协议和负载均衡策略,允许服务之间以高效率进行数据交换。

Feign 是一个声明式的Web服务客户端,它使得编写Web服务客户端变得更容易。使用 Feign 可以通过简单的接口和注解来调用HTTP API。Feign 的目标是尽可能简化HTTP API客户端的实现过程。

9、有了限流,为什么还需要熔断降级?

限流是防止系统因过多的请求而崩溃。即使没有发生限流,如果下游系统故障,会导致调用方线程阻塞,进而导致线程池耗尽,可能引发级联故障导致服务雪崩。

限流既可以是调用方也可以是提供方配置,而熔断是调用方进行配置。

因此限流和熔断是高可用防护的两大基石,缺一不可‌。

Jvm

1、运行时内存区域

参考链接:【JVM】运行时内存区域

1、JVM的运行时内存区域的组成?

2、为什么JDK1.7中,方法区的字符串常量池移到了堆中?

3、为什么JDK1.8中,方法区的实现从永久代换成了元空间?

根据Java虚拟机规范的定义,JVM的运行时内存区域主要由Java堆、方法区、运行时常量池;虚拟机栈、本地方法栈、以及程序计数器组成

其中堆、方法区以及运行时常量池是线程之间共享的区域,而虚拟机栈、本地方法栈、程序计数器都是线程独享的。

在JDK 1.7版本中,方法区通常被实现为永久代(Permanent Generation),用于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。

不过在1.6中,方法区中包含了字符串常量池,而在1.7中,把字符串常量池移到了堆内存中。这么做的主要原因是因为永久代的 GC 回收效率太低,只有在FullGC的时候才会被执行回收 。但是Java中往往会有很多字符串也是朝生夕死的,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

由于方法区作为元空间和永久代的主要区域,他里面存储了大量的被加载的类,而很多时候,项目中用了很多动态生成类的框架之后,比如Spring,就会占用很多空间,如果在堆上,就会导致频繁的GC,以及OOM的问题。

而把方法区放到元空间,即放到堆外内存中, JVM 可以根据机器内存大小动态扩展元空间的大小,减少OOM的发生。

所以,从JDK 1.8开始,HotSpot虚拟机对方法区的实现进行了重大改变。永久代被移除,取而代之的是元空间(Metaspace)。元空间是使用本地内存(Native Memory)来存储类的元数据信息的,它不再位于堆内存中。

元空间的特点是可以根据应用程序的需要动态调整其大小,因此更加灵活。它能够有效地避免了永久代的内存溢出问题,并且可以减少垃圾回收的压力。元空间的内存使用量受限于操作系统对本地内存的限制。

2、垃圾回收算法

【JVM】垃圾回收算法

1、标记-清除算法

优点

1)速度快,因为不需要移动和复制对象

缺点

1)会产生内存碎片,造成内存的浪费

2、标记-复制算法

优点:

1)内存空间是连续的,不会产生内存碎片

缺点

1)浪费了一半的内存空间

2)复制对象会造成性能和时间上的消耗

3、标记-整理算法

优点

1)不会产生内存碎片

2)不会浪费内存空间

缺点

1)太耗时间(性能低)

新生代和老年代分别使用什么算法?

新生代选择了复制算法进行垃圾回收。但是标记复制算法存在一个缺点就是会浪费空间,新生代为了解决这个问题,把区域进一步细分成一个Eden区和两个Survivor区,同时工作的只有一个Eden区+一个Survivor区,这样,另外一个Survivor主要用来复制就可以了。

对于老年代来说,通常会采用标记整理算法,虽然效率低了一点,但是可以减少空间的浪费并且不会有空间碎片等问题。在有些回收器上面,如CMS,为了降低STW的时长,也会采用标记清除算法。

3、JVM如何判断对象是否存活?

【JVM】垃圾回收算法

3.1 引用计数法

引用计数法主要是很难解决对象之间相互循环引用的问题。循环引用会导致对象无法被回收,最终会导致内存泄漏及内存溢出。

3.2 可达性分析法

可达性分析法的基本思想就是通过一系列称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

所谓"GC Roots",就是一组必须活跃的引用。比如系统类加载器加载的类、活着的线程、栈上的本地变量、被synchronized锁定的对象、Remembered Set,都符合活跃的引用这个条件。

可达性分析算法的不足:

1、STW时间长

2、内存消耗

3.3 三色标记法

三色标记法的标记过程可以分为三个阶段:初始标记、并发标记、重新标记、并发清理。

初始标记和重新标记是需要STW的,而并发标记和并发清理是不需要STW的。其中最耗时的其实就是并发标记的这个阶段,因为这个阶段需要遍历整个对象树,而三色标记把这个阶段做到了和应用线程并发执行,大大降低了GC的停顿时长。

3.4 什么是跨代引用,怎么解决?

JVM的跨代引用问题是指在Java堆内存的不同代之间存在引用关系,导致对象在不同代之间的引用被称为跨代引用。比如:新生代到老年代的引用,老年代到新生代的引用等。

Remembered Set 记录了老年代对象指向年轻代对象的引用关系,此后当发生Minor GC时,垃圾回收器不需要扫描整个老年代来确定哪些对象存活。它只需扫描Remembered Set中的条目,从而减少了扫描的开销。所以,在Remember Set中的对象也会被加入到GC Roots进行扫描。

4、JVM垃圾收集器有哪些?

【JVM】垃圾回收器

1、串行垃圾回收器(Serial Garbage Collector) 如:Serial GC和Serial Old

2、并行垃圾回收器(Parallel Garbage Collector) 如:ParNew、Parallel Scavenge和Parallel Old

与ParNew最大的不同是,Parallel Scavenge 关注的是垃圾回收的吞吐量(吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),以吞吐量优先。

3、并发标记扫描垃圾回收器(CMS Garbage Collector)

4、G1垃圾回收器(G1 Garbage Collector,JDK 7中推出,JDK 9中设置为默认)

5、ZGC垃圾回收器(The Z Garbage Collector,JDK 11 推出)

G1和CMS有什么区别?

G1 和 CMS相比,他们都是基于三色标记法实现的。

1、回收位置:CMS回收的是老年代、G1回收的是整堆。

2、GC算法:CMS用的是标记-清除算法。G1在年轻代用的是标记-复制算法、在老年代用的是标记-整理算法。所以CMS会产生内存碎片,G1不会产生内存碎片。

3、STW时间可预测性:CMS无法预测,G1可预测STW时间。

4、堆内存要求:CMS一般要求不高、G1要求4G及其以上。

CMS使用"标记-清理"法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间,但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC。为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了。

5、对象在什么情况下会晋升到老年代?

【JVM】堆内存分代和GC触发条件

1、躲过15次GC。

2、大对象直接进入老年代。

3、动态对象年龄判断。规则:如果在Survivor空间中小于等于某个年龄的所有对象大小的总和大于Survivor空间的一半时,那么就把大于等于这个年龄的对象都晋升到老年代。

6、FullGC触发条件?出现full gc的典型场景和排查思路?

【JVM】堆内存分代和GC触发条件

1、老年代空间不足。

2、空间分配担保失败。在每一次执行YoungGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。如果小于,则会触发full gc。如果大于,触发young gc,young gc之后,发现要移到老年代的对象,老年代存不下的时候,会触发一次FullGC。

3、代码中执行System.gc()。

4、如果有永久代且永久代空间不足。

YoungGC触发条件?

YoungGC的触发条件比较简单,那就是当年轻代中的eden区分配满的时候就会触发。

出现full gc的典型场景和排查思路?

一、大对象

1、大日志打印导致大对象,需要代码优化。

2、数据过大导致大对象:比如一次从数据库中取出的数据量过大,那么是不是可以考虑通过sql分批次取出,通过代码逻辑降低大对象。比如获取图片高宽,是不是不一定要加载大图片,是不是通过图片类的头信息或参数就可以获取了,通过代码逻辑降低大对象。

二、频繁新建对象

1、非预期地在频繁地new新对象,需要代码优化。

2、比如业务量过大:导致同一时刻创建了过多的对象。这个时候是不是先计算下高峰时业务量产生的内存大小,然后增大新生代的大小,让更多的对象在minor gc中进行处理。

7、类的生命周期?

【JVM】类的生命周期

类的生命周期,大的阶段可以分为类的加载、类的使用、以及类的卸载。

其中类的加载阶段又分为加载、链接、初始化。

其中连接过程又包含了验证、准备和解析。

类的加载过程:

1、缓存思想,如果该类已经被加载过,则不加载。

2、使用双亲委派模型,对该类进行加载。若父加载器为空则默认使用启动类加载器作为父加载器。

3、如果通过CLASSPATH找不到该类的定义,则会通过findClass让子类自定义的去获取类定义的二进制文件。

4、然后通过defineClass将二进制文件加载为类。

双亲委派模型主要是由ClassLoader#loadClass实现的,我们只需要自定义类加载器,并且重写其中的loadClass方法,即可破坏双亲委派模型。

TOMCAT为什么破坏双亲委派?

一个Tomcat,是可以同时运行多个应用的,而不同的应用可能会同时依赖一些相同的类库,但是他们使用的版本可能是不一样的,但是这些类库中的Class的全路径名因为是一样的,如果都采用双亲委派的机制的话,是无法重复加载同一个类的,那么就会导致版本冲突。

而为了有更好的隔离性,所以在Tomcat中,每个应用都由一个独立的WebappClassLoader进行加载,这样就可以完全隔离开。而多个WebAppClassLoader之间是没有委派关系的,他们就是各自加载各自需要加载的Jar包。

Tomcat的类加载机制是怎么样的?

  1. 加锁: 方法使用同步块确保线程安全。
  2. 检查已加载类缓存: 首先,通过调用 findLoadedClass方法检查本地缓存是否已加载该类,如果是,则直接返回缓存中的 Class 对象。
  3. 检查已加载类缓存(GraalVM 兼容性处理): 通过调用 findLoadedClass 方法检查另一个类加载缓存,如果是GraalVM环境,直接返回缓存中的 Class 对象。
  4. 尝试使用Bootstrap类加载器加载: 尝试使用Bootstrap类加载器加载类,以防止Web应用程序覆盖Java SE类。如果加载成功,则返回加载的 Class 对象。
  5. 决定是否委派加载: 根据 delegate 属性和其他条件判断是否应该委派加载给父类加载器。
  6. 委派给父类加载器: 如果需要委派加载(delegate为true),尝试使用父类加载器加载类。
  7. 自己尝试加载: 如果未指定需要委派(delegate为false),或者未从父类加载器中找到类,则调用 findClass 方法尝试自己进行类加载。
  8. 委派给父类加载器: 如果未指定需要委派(delegate为false),且自己没加载到类,则尝试使用父类加载器加载类。

如果不打破双亲委派机制,正常的加载顺序为:

Bootstrap->System->Common->WebApp。

tomcat通过重写loadClass方法打破了双亲委派机制,默认加载顺序为:

Bootstrap->WebApp->System->Common。

8、jvm命令行

1、jps

jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。

bash 复制代码
hollis@hos:/tmp/hsperfdata_hollis$ jps
2679 org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
4445 Jps

执行了jps命令之后,发现有两个java进程,一个是pid为2679的eclipse运行的进程,另外一个是pid为4445的jps使用的进程(他也是java命令,也要开一个进程)

2、jstack

jstack用于生成java虚拟机当前时刻的线程快照。

bash 复制代码
hollis@hos:~$ jstack 29788
2015-04-17 23:47:31
...此处省略若干内容...
"main" prio=10 tid=0x00007f197800a000 nid=0x7462 runnable [0x00007f197f7e1000]
   java.lang.Thread.State: RUNNABLE
    at javaCommand.JStackDemo1.main(JStackDemo1.java:7)

可以看到,当前一共有一条用户级别线程,线程处于runnable状态,执行到JStackDemo1.java的第七行。

3、jmap

可以使用jmap生成Heap Dump。如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况。

bash 复制代码
查看java 堆(heap)使用情况,执行命令:
hollis@hos:~/workspace/design_apaas/apaasweb/control/bin$ jmap -heap 31846

查看堆内存(histogram)中的对象数量及大小。执行命令:
hollis@hos:~/workspace/design_apaas/apaasweb/control/bin$ jmap -histo 3331

4、jhat

jhat(Java Heap Analysis Tool),是一个用来分析java的堆情况的命令。使用jmap生成的Java堆的Dump文件可以用jhat命令将其转成html的形式,然后通过http访问可以查看堆情况。

bash 复制代码
// 1、首先使用jmap命令生成dump:
$ jmap -dump:format=b,file=heapDump 62247
Dumping heap to /Users/hollis/workspace/test/heapDump ...
Heap dump file created

// 2、解析Java堆转储文件,并启动一个 web server。
// 端口是7000 ,然后在访问http://localhost:7000/
$ jhat heapDump

Reading from heapDump...
Dump file created Thu Jan 21 18:59:51 CST 2016
Snapshot read, resolving...
Resolving 341297 objects...
Chasing references, expect 68 dots....................................................................
Eliminating duplicate references....................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

具体排查时需要结合代码,观察是否大量应该被回收的对象在一直被引用或者是否有占用内存特别大的对象无法被回收。

5、javap

javap是jdk自带的一个工具,可以对代码反编译,也可以查看java编译器生成的字节码。

分布式

1、有哪些分布式事务解决方案?

【分布式】分布式事务方案:两阶段、TCC、SEATA

分布式事务主要解决的是一致性问题,主要方案有以下4种:

1、两阶段提交

第一阶段:协调者 向所有 参与者 发送 Prepare 请求,询问能否提交事务。参与者 执行事务,但不提交(只是写入日志或加锁,进入"预提交"状态)。

第二阶段:如果 所有参与者都返回 Yes,协调者向所有参与者发送 Commit 请求,参与者正式提交事务,并释放锁资源。

主要有3个问题:

1、同步阻塞问题:在参与者收到协调者最终指令之前,其资源一直处于锁定状态。如果协调者一直不发送指令,参与者会一直阻塞,严重影响系统性能和可用性。

2、单点故障问题。一旦协调者发生故障,参与者会一直阻塞下去。

3、数据不一致问题:假设参与者收到rollback命令且执行了rollback,而其他参与者都还没有收到commit命令,此时参与者和协调者同时都挂了。新的协调者被选出来后,由于剩下没挂的参与者都是commit,因此执行了commit命令。之后挂了的参与者又恢复了,此时就会出现数据不一致问题。

2、三阶段提交

第一阶段:协调者向所有参与者发送 CanCommit 请求,询问是否可以执行事务。

第二阶段:协调者向所有参与者发送 PreCommit 请求,进入预提交阶段。参与者执行事务操作(写日志、加锁),并进入 等待提交状态。

第三阶段:如果 PreCommit 阶段所有参与者都确认成功,协调者向所有参与者发送 DoCommit 请求。

三阶段可以解决两阶段提交的数据不一致问题。

假设参与者收到rollback命令且执行了rollback,而其他参与者都还没有收到commit命令,此时参与者和协调者同时都挂了。新的协调者被选出来之后,可以看一下剩下没挂的参与者的状态,如果都是commit,说明挂了的那台执行的也是commit命令。

3、TCC

基于业务逻辑的补偿机制,将整个分布式事务分解为若干个子事务,每个子事务都有一个try、confirm和cancel三个操作,通过这些操作来实现分布式事务的执行和回滚。

TCC方案的缺点:

1、实现复杂

2、业务代码侵入性

3、存在悬挂事务问题:即在执行过程中可能会有部分子事务成功,而其他子事务失败,导致整个事务无法回滚或提交。比如:一次分布式事务,先发生了Try,但是因为有的节点失败,又发生了Cancel,而下游的某个节点因为网络延迟导致先接到了Cancel,在空回滚完成后,又接到了Try的请求,然后执行了,这就会导致这个节点的Try占用的资源无法释放,也没人会再来处理了,就会导致了事务悬挂。

4、空回滚问题:TCC中的Try过程中,有的参与者成功了,有的参与者失败了,这时候就需要所有参与者都执行Cancel,这时候,对于那些没有Try成功的参与者来说,本次回滚就是一次空回滚。需要在业务中做好对空回滚的识别和处理,否则就会出现异常报错的情况,甚至可能导致Cancel一直失败,最终导致整个分布式事务失败。

解决悬挂事务问题和空回滚问题,可以引入分布式事务记录表,在表中记录每一个事务的最新状态,从而处理这2个问题。

4、基于本地消息表实现分布式事务

一般来说的做法是,在发送消息之前,先创建一条本地消息,并且保证写本地业务数据的操作,和写本地消息记录的操作在同一个事务中。这样就能确保只要业务操作成功,本地消息一定可以写成功。然后再基于本地消息,调用MQ发送远程消息。消息发出去之后,等待消费者消费,在消费者端,接收到消息之后,做业务处理,处理成功后再修改本地消息表的状态。

2、seata的四种模式分别适用于什么场景?

1、seata四种模式的适用场景?

1、seata的AT模式和XA模式都是二阶段提交,区别是AT模式在第一阶段各个分支事务就进行了实际提交,而XA是在第二阶段各个分支事务才进行提交,因此XA模式是强一致性,AT模式是最终一致性。

2、seata的saga模式和AT模式一样,也是在第一阶段各个分支事务就进行了实际提交,区别在于:

1)适用场景不同:AT模式适用于传统关系型数据库的分布式事务,saga模式适用于业务流程长,步骤多的场景。

2)事务执行方式不同:AT模式基于两阶段提交,saga模式基于事件驱动或状态机的长事务编排。

3)锁机制不同:AT模式有全局锁,saga模式是无锁。所以saga模式的性能优于AT模式。

3、seata的tcc模式与AT模式的区别:

1)使用场景不同:AT模式适用于数据库操作为主,对性能要求一般,希望快速接入且尽量减少代码修改的场景。tcc模式适用于高性能、高并发的业务,或涉及非数据库资源(MQ、Redis、第三方API)的分布式事务场景。

2、Seata的AT模式和XA有什么区别?

XA是一个典型的分布式事务解决方案,是一个强一致性模型,基于XA的2PC和3PC一直都是业内比较成熟的分布式方案,但是,他们都存在着各种各样的问题。

Seata中的AT模式其实和2PC很像,都是把一个分布式事务分成了2个阶段,那他们之间有什么区别呢?

他们的主要区别就在于一阶段是否直接提交事务,Seata的AT模式中,为了提升性能,直接提交了事务,在二阶段,需要回滚的话再执行回滚。而XA中,一阶段是只做资源占用,二阶段在进行回滚或者提交。

在一致性方面,XA确实要比AT好,因为在一阶段所有参与者都只进行预提交操作,二阶段再根据协调器的决定进行实际提交或回滚,确保了全局事务的一致性。

而AT这种模式,一阶段是做了数据提交的,提交后就可能被其他事物看到,那么就会出现事务被提前看到的情况。所以会存在短暂的不一致。

但是AT的性能是明显高于XA的,因为在一阶段已经执行了实际的数据库操作,并不需要做资源的占用和锁定,而二阶段也只是在失败的情况下再执行回滚,所以性能相对较高。

3、分布式ID生成方案都有哪些?

1、UUID

UUID通常由以下几部分的组合而成:当前日期和时间,时钟序列,全局唯一的IEEE机器识别号,从而保证唯一。

优点是性能比较高,不依赖网络,本地就可以生成。

缺点是长度过长、没有任何含义、无序。

2、数据库自增

缺点是存在单点故障问题,一旦这个数据库挂了,那整个分布式ID的生成服务就挂了。而且还存在一个性能问题,如果高并发访问数据库的话,就会带来阻塞问题。

3、号段模式

号段模式是在数据库的基础上,为了解决性能问题而产生的一种方案。他的意思就是每次去数据库中取ID的时候取出来一批,并放在缓存中,然后下一次生成新ID的时候就从缓存中取。这一批用完了再去数据库中拿新的。

优点是在同一个客户端中,生成的ID是顺序递增的。并且不需要频繁的访问数据库,也能提升获取ID的性能。缺点是没办法保证全局顺序递增,也存在数据库的单点故障问题。

4、Redis 实现

可以依赖Redis的incr命令实现ID的原子性自增。

Redis的优点就是可以借助集群解决单点故障的问题,并且他基于内存性能也比较高。

5、雪花算法(Snowflake)

雪花算法可以生成全局唯一且递增的ID。它的核心思想是将一个64位的ID划分成多个部分,每个部分都有不同的含义,包括时间戳、数据中心标识、机器标识和序列号等。

优点是保证全局唯一,时间戳位于ID的最高位,保证新生成的ID比旧的ID大,在不同的毫秒内,时间戳肯定不一样。

缺点是每个节点的机器ID和数据中心ID都是硬编码在代码中的,而且这些ID是全局唯一的。当某个节点出现故障或者需要扩容时,就需要更改其对应的机器ID或数据中心ID,但是这个过程比较麻烦,需要重新编译代码,重新部署系统。

4、什么是一致性哈希?

一致性哈希(Consistent Hashing)是一种用于分布式系统中数据分片和负载均衡的算法。它的目标是在节点的动态增加或删除时,尽可能地减少数据迁移和重新分布的成本。

一致性哈希算法将整个哈希空间视为一个环状结构,将节点和数据都映射到这个环上。每个节点通过计算一个哈希值,将节点映射到环上的一个位置。而数据也通过计算一个哈希值,将数据映射到环上的一个位置。

当有新的数据需要存储时,首先计算数据的哈希值,然后顺时针或逆时针在环上找到最近的节点,将数据存储在这个节点上。当需要查找数据时,同样计算数据的哈希值,然后顺时针或逆时针在环上找到最近的节点,从该节点获取数据。

优点是数据均衡和高扩展性,在增加或删除节点时,一致性哈希算法只会影响到少量的数据迁移,保持了数据的均衡性。

相关推荐
摇滚侠2 小时前
IDEA 开发,Mybatis 中,@Insert 注解如何提示出列名
java·intellij-idea·mybatis
程序员Terry2 小时前
Docker 部署 RocketMQ 5.1.0 踩坑实录:从超时到 Console 连不上的完整解决之路
java·后端
tsyjjOvO2 小时前
SpringMVC 从入门到精通(续)
java·后端·spring
Binary-Jeff2 小时前
MySQL MVCC 原理解析:Undo Log、ReadView 与版本可见性机制
java·数据库·后端·mysql·spring
于先生吖2 小时前
基于 Java 开发短剧系统:完整架构与核心功能实现
java·开发语言·架构
badhope2 小时前
GitHub超有用项目推荐:skill仓库--用技能树打造AI超频引擎
java·开发语言·前端·人工智能·python·重构·github
一只鹿鹿鹿2 小时前
网络安全风险评估报告如何写?(Word文件)
java·大数据·spring boot·安全·web安全·小程序
逆境不可逃2 小时前
【后端新手谈 04】Spring 依赖注入所有方式 + 构造器注入成官方推荐的原因
java·开发语言·spring boot·后端·算法·spring·注入方式
Anastasiozzzz2 小时前
深度解析 Java 单例模式
java·开发语言