JAVA面试题

一、JAVA基础

1.java常用的集合

Java常用的集合主要由两个接口派生出来,Collection和Map。其中Collection接口下有派生出三个子接口,List,Set和Queue。所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含hashNext(),next(),remove()三种方法。
常见的集合

2.谈谈HashMap,以及与HashTable的区别

HashMap底层是由数组+链表实现的,在jdk1.8之后,当链表长度超过8,数组长度大于64,链表会转为红黑树。hashMap非线程安全,允许key和value为null值。而hashTable在方法上加了synchronize,线程安全,且hashTable不允许key,value为null。
HashMap的扩容机制:当hashMap中的元素个数超过【数组大小*负载因子】值时,会触发扩容,将数组大小扩大为原来二倍,然后重新计算原来元素rehash的位置。
头插法与尾插法:
当HashMap要在链表里插入新的Entry时,在Java 8之前是将Entry插入到链表头部,在Java 8开始是插入链表尾部(Java 8用Node对象替代了Entry对象)。
Java 7插入链表头部,是考虑到新插入的数据,更可能作为热点数据被使用,放在头部可以减少查找时间。
Java 8改为插入链表尾部,原因就是防止环化。因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。

3.谈谈java中的反射机制

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的法的功能称为java语言的反射机制。

  1. 反射的第一步就是获取class对象,通常使用
    具体的Class.getClass()或者Class.forName(“全限定类名“)来获取class对象。

  2. 第二步通过获取的class对象调用newInstance()方法,来新建一个该类的实例。虽然可以使用newInstance()方法,但因为其使用的是无参构造函数,所以在需要参数构造的情况下,推荐使用Constructor.newInstance(Object… initargs)方法

  3. Class类:用于表示类的元数据信息,包括类的名称、方法、字段等。常用的方法包括:

  • getName():获取类的全限定名。
  • getMethods():获取类中定义的所有公共方法。
  • getFields():获取类中定义的所有公共字段。
  • getDeclaredMethods():获取类中定义的所有方法,包括私有方法。
  • getDeclaredFields():获取类中定义的所有字段,包括私有字段。

Method类:用于表示方法的元数据信息,包括方法的名称、参数类型、返回类型等。常用的方法包括:

  • getName():获取方法的名称。
  • getParameterTypes():获取方法的参数类型数组。
  • getReturnType():获取方法的返回类型。
  • invoke(Object obj, Object... args):调用方法,第一个参数是方法所属的对象,后面的参数是方法的参数。

Field类:用于表示字段的元数据信息,包括字段的名称、类型等。常用的方法包括:

  • getName():获取字段的名称。
  • getType():获取字段的类型。
  • get(Object obj):获取字段的值,第一个参数是字段所属的对象。

Constructor类:用于表示构造函数的元数据信息,包括构造函数的参数类型等。常用的方法包括:

  • getParameterTypes():获取构造函数的参数类型数组。
  • newInstance(Object... args):创建对象,传入构造函数的参数。

Modifier类:用于获取和操作修饰符的工具类。常用的方法包括:

  • isPublic(int modifiers):判断修饰符是否是public。
  • isPrivate(int modifiers):判断修饰符是否是private。
  • isStatic(int modifiers):判断修饰符是否是static。

java异常机制,及异常处理

在这里插入图片描述
Java 异常的基类是 Throwable 类型,然后它有两个子类 Error(错误)和 Exception()类型,然后 Exception 类又分为运行时异常(RuntimeException)及非运行时异常(编译异常)。
Error 一般表示编译时或者系统错误,例如:Virtual MachineError(虚拟机运行错误),StackOverflowError:栈溢出错误、OutOfMemoryError:内存不足错误等。此类错误开发者无法处理且不可恢复,JVM 将终止线程,因此不应该实现任何新的 Error 子类。
Exception 这种异常又分为两类:运行时异常(非受检查异常)和非运行时异常(受检查异常)。

  • RuntimeException 及其子类都统称为运行时异常,例如:NullPointExecrption(空指针)、ArithmeticException(算术错误)、ArrayIndexOutOfBoundsException(数组越界)等。这类异常是非受检查的,具体根据需要来判断是否需要捕获和处理,并不会在编译期强制要求。开发者应该从程序编码阶段就尽可能避免其发生。
  • 除RuntimeException 以外的异常都属于非运行时异常。例如 IOException、SQLException 等以及用户自定义的 Exception 异常等。这类异常是受检查的,在源代码里必须显式地进行捕获处理,是编译期检查的一部分。这种异常要么用 try-catch 语句捕获它,要么用 throws 子句声明抛出它,否则编译不会通过。

SpringBoot全局异常处理的三种方式

  1. 使用@ControllerAdvice 注解
    @ControllerAdvice 是 SpringBoot 提供的一个注解,用于定义全局异常处理器。在使用@ControllerAdvice 注解时,需要使用@ExceptionHandler 注解来指定处理的异常类型。

下面是一个使用@ControllerAdvice 注解的例子:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }
}

在上面的例子中,我们使用@ControllerAdvice 注解定义了一个全局异常处理器,然后使用@ExceptionHandler 注解指定了要处理的异常类型为 Exception。当应用中出现 Exception 类型的异常时,就会调用 handleException 方法来处理异常。

  1. 使用@ExceptionHandler 注解
    除了使用@ControllerAdvice 注解外,还可以在控制器中使用@ExceptionHandler 注解来处理异常。这种方式的好处是可以针对不同的控制器方法定义不同的异常处理器。

下面是一个使用@ExceptionHandler 注解的例子:

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
    }

    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user == null) {
            throw new UserNotFoundException("User not found");
        }
        return ResponseEntity.ok(user);
    }
}

在上面的例子中,我们在控制器中定义了一个 handleUserNotFoundException 方法,用于处理 UserNotFoundException 类型的异常。当 getUser 方法中出现 UserNotFoundException 类型的异常时,就会调用 handleUserNotFoundException 方法来处理异常。

  1. 使用 HandlerExceptionResolver 接口
    除了使用@ControllerAdvice 注解和@ExceptionHandler 注解外,还可以实现 HandlerExceptionResolver 接口来处理异常。这种方式比较灵活,可以自定义异常处理器的实现方式。

下面是一个使用 HandlerExceptionResolver 接口的例子:

public class GlobalExceptionHandler implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", ex);
        mav.setViewName("error");
        return mav;
    }
}

在上面的例子中,我们实现了 HandlerExceptionResolver 接口,并重写了 resolveException 方法来处理异常。当应用中出现异常时,就会调用 resolveException 方法来处理异常。

二、Java并发编程,JUC

4. 了解JUC吗?高并发中的集合有哪些?

JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题。
juc包结构

  1. Tools:
    1)CountDownLatch(闭锁) 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
    2)CyclicBarrier(栅栏) 之所以叫barrier,是因为是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 ,并且在释放等待线程后可以重用。
    3)Semaphore(信号量) 是一个计数信号量,它的本质是一个“共享锁“。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可
  2. Executor:是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService
  3. Atomic:是JDK提供原子操作类,包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。
  4. Lock:是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁包含的实现类有:
    1)ReentrantLock 它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
    2)ReentrantReadWriteLock 它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。
    3)LockSupport 它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。
  5. Collections:主要是提供线程安全的集合,例如:ArrayList对应的高并发类是CopyOnWriteArrayList,HashSet对应的高并发类是 CopyOnWriteArraySet,HashMap对应的高并发类是ConcurrentHashMap等等。

5.ConcurrentHashMap底层原理是什么?

Jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment数组中包含一个HashEntry数组,每个HashEntry又是一个链表结构。
锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其他Segment不受影响。并发度为Segment的个数,可以通过构造函数指定,数组扩容不会影响其他HashEntry。Get方法不加锁,用volatile保证。
元素查询:二次hash,第一次hash定位Segment,第二次hash定位到元素所在链表的头部(数组位置)

Jdk8:
数据结构:synchronized+CAS+node+红黑树,Node的Val和next用volatile修饰。
查找,替换,赋值都是用CAS(Compare-And-Swap):比较并交换。CAS就是通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。如果预期值和实际不同,我们就认为,其他线程更新了这个值,此时不做更新操作。而且这整个流程是原子性的,所以只要实际值和预期值相同,就能保证这次更新不会被其他线程影响。
锁:锁住链表的head节点,不影响其他元素的读写。锁粒度更细,效率更高,扩容时阻塞所有的读写操作,并发扩容。
读操作无锁:
Node的Val和next用volatile修饰,读写线程相互可见。
数组用volatile修饰保证,扩容时被读线程感知。

6.synchronized和Lock的区别

1.synchronized是java的一个关键字,可以修饰方法和代码块,当一个线程访问一个对象的同步方法或同步代码块时,同其他线程不能访问。而lock时juc.locks包的一个接口。
2.是否释放锁:synchronized在发生异常时会自动释放锁,因此不会出现死锁,而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
3.是否响应中断:lock在等待过程中可以通过interrupt来中断,而synchronized只能等待锁的释放,不能响应中断。
4.是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能。
5.Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
6.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
7.synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,
8.锁机制
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

7.谈谈synchronized锁升级过程

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,在大多数情况下,锁总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。但是对于锁竞争比较激烈的场合,偏向锁就失效了,先升级为轻量级锁。
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。避免操作系统线程之间的切换时,从用户态转换到核心态需要较高时间成本,因此自旋锁会假设在不久当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起。最后没办法也就只能升级为重量级锁了。

8.使用过线程池吗?

由于使用线程时,创建和销毁的代价比较高,所以用线程池将线程缓存下来。使用线程池可以提升性能,线程池能独立负责线程的创建,分配和维护。还方便对线程进行有效管理,使异步任务高校调度。
线程池的创建一般有两种,一种是使用Executors创建,一种是通过new ThreadPoolExecutor 创建。
Executors中提供了几个常用的线程池:
1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】
但是Executors创建的线程池是有问题的,其中FixedThreadPool,SingleThreadExecutor请求允许请求的队列长度是integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM,而CachedThreadPool,允许创建的线程数为integer.MAX_VALUE,可能创建大量线程,导致OOM。所以在项目中一般都使用new ThreadPoolExecutor来创建线程池。
线程池的参数:
1.corePoolSize,核心线程数,线程池的基本大小,提交一个任务到线程池时,线程池会创建一个新的线程来执行任务。注意: 即使有空闲的基本线程能执行该任务,也会创建新的线程。如果线程池中的线程数已经大于或等于corePoolSize,则不会创建新的线程。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2.maximumPoolSize(线程池的最大数量): 线程池允许创建的最大线程数。
阻塞队列已满,线程数小于maximumPoolSize便可以创建新的线程执行任务。
如果使用无界的阻塞队列,该参数没有什么效果。
3. workQueue(工作队列): 用于保存等待执行的任务的阻塞队列。
ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,能创建的最大线程数为maximumPoolSize。
LinkedBlockingQueue: 基于链表结构的无界阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue。使用该队列,线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。
SynchronousQueue: 一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作,否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。
PriorityBlokingQueue: 一个支持优先级的无界阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize。
4. keepAliveTime(线程活动保持时间): 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。
5. unit(线程活动保持时间的单位): 可选单位有DAYS、HOURS、MINUTES、毫秒、微秒、纳秒。
6. handler(饱和策略,或者又称拒绝策略): 当队列和线程池都满了,即线程池饱和了,必须采取一种策略处理提交的新任务。
AbortPolicy: 无法处理新任务时,直接抛出异常,这是默认策略。
CallerRunsPolicy:用调用者所在的线程来执行任务。
DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。
DiscardPolicy: 直接丢弃任务。
7. threadFactory: 构建线程的工厂类
线程池的工作流程:
当一个新的任务到线程池时,判断核心线程池里的线程是否都在执行任务。 如果不是,创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,线程池判断阻塞队列是否已满。如果阻塞队列没有满,将新提交的任务存储在阻塞队列中,如果BlockingQueue已满,而且当前运行的线程小于maximumPoolSize,则创建一个新的工作线程来执行任务。如果当前运行的线程大于或等于maximumPoolSize,则交给饱和策略来处理这个任务。

9.如何预防死锁

死锁发生的四个必要条件:
1.互斥:同一时间只能有一个线程获取资源。
2.不可剥夺:一个线程占有资源,在释放前不会被其它线程抢占。
3.请求和保持:线程等待时不会释放占有的资源。
4.循环等待
预防死锁就是破坏死锁发生的条件,比如说增加资源,当自己获取不到资源时主动释放占有资源等。

10.线程的生命周期和状态

线程状态
线程的方法

11.ThreadLocal的原理是什么,使用场景有哪些?

ThreadLocal类是用来解决多线程下变量安全的,在多线程并发场景下,将线程中的数据进行相互隔离。threadLocal是给每个线程建立一份单独的变量副本。
threadLocal在这里插入图片描述
在这里插入图片描述

三、JVM

12. 类的初始化过程

字节码–>加载–>验证–>准备–>解析–>初始化
在这里插入图片描述
1.加载:双亲委派机制,向上查找,向下委托。 主要是为了安全,防止重写已有的核心类库。其次为了不浪费资源重复加载。
2.验证:确保加载的字节码的是否符合虚拟机的要求,是java提供的一种自我保护机制,不让其危害虚拟机安全。其主要包括四种验证,字节码验证、文件格式验证,元数据验证、符号引用验证。
3.准备:为类变量分配地址和初始化值,类变量会分配到方法区(元空间)中,这里的初始化是指该数据类型的默认初始值,例如int对应的是0,long对应的0L,只有在初始化时才会动显示赋值。实例变量主要随着对象的实例化一块分配到java堆中。
4.解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
5.初始化:主要为类的静态变量赋予正确的值,比如int num = 10; 这里num的值会从准备阶段的0变为10;并且若该类有父类,会对其进行初始化操作;如果类中有初始化语句,系统会按照顺序进行初始化。初始化是类加载的最后一步,也是真正执行类中定义的Java程序代码(字节码),初始化阶段是执行类构造器init()方法的过程。

13.JVM内存模型

hotspot虚拟机:JVM将内存分为 堆、虚拟机栈、本地方法栈、程序计数器、方法区五部分。
JDK1.8与JDK1.7相比最大的区别就是:元数据区取代了永久代,元数据空间并不在虚拟机中,而是使用本地内存。
在这里插入图片描述

14.GC如何确定对象可被回收?

1.引用计数法
引用计数法的算法思路:给对象增加一个引用计数器,每当对象增加一个引用计数器+1,失去一个引用-1,所以当计数器是0的时候对象就没有引用了,就会被认为可回收垃圾。
2.可达性分析算法
可达性分析的算法思路:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(即从 GC Roots 到这个对象不可达)时,证明此对象不可用(视为垃圾)。
java中可作为GC Roots的对象包含以下几种:

    1.虚拟机栈(栈帧中的本地变量表)中引用的对象

    2.方法区中静态引用的对象

    3.方法区中常量引用的对象

    4.本地方法栈中JNI(Native方法)引用的对象

不同的引用类型回收机制是不同的
1.强引用:通过关键字new的对象,强引用对象不会回收。
2.软引用:通过java.lang.ref.SoftRefrence持有,当JVM堆内存不足时,会被回收
3.弱引用:通过java.lang.ref.WeakRefrence持有,在GC时,只要发现弱引用就会被回收。
4.虚引用:随时可以被回收

15.有哪些垃圾回收算法

1.标记-清楚算法:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。有两个问题:第一是标记和清除过程的效率不高,第二会产生大量不连续的碎片空间。
2.复制算法:将内存按容量分为两块大小相同的空间,每次只使用一般,当用完一块时将存活的对象复制到另一半空间,然后将使用过的空间一次性清除,实现简单,运行高效。但是将内存缩小为原来一半,利用率低。
3.标记-整理算法:与标记清除算法类似,但不是直接回收,而是将所有存活对象向一端移动,然后清理边界以外的内存。效率较低
4.分代收集算法:根据对象的存活周期划分不同的内存空间,根据各个年代的特点选择合适的收集算法。新生代用复制算法,老年代用标记-整理或标记清除。

16 java中的垃圾收集器

1、Serial 收集器:使用单线程。当垃圾收集器运行时,会停止应用程序
2、Parallel 收集器:像 Serial 收集器一样,Parallel 收集器也使用“stop the world”方法。但是不同的是,Parallel 收集器运行时有多个线程执行垃圾收集操作。这种类型的垃圾收集器适用于在多线程和多处理器环境中运行中到大型数据集的应用程序。这是 JVM 中的默认垃圾收集器,也被称为 吞吐量收集器 。使用该垃圾收集器时可以通过使用各种合适的 JVM 参数进行调优,例如吞吐量、暂停时间、线程数和内存占用。
3、Concurrent Mark Sweep(CMS)收集器:垃圾收集器与应用程序并行运行。对于新生代和老年代都使用了多线程。在 CMS 垃圾收集器删除无用对象后,不会对存活对象进行内存压缩。该垃圾收集器和应用程序并行运行,会降低应用程序的响应时间,适用于停顿时间较短的应用程序。这个收集器在 Java8 已过时,并在 Java14 中被移除。
初始标记阶段(STW):所有用户线程暂停,标记出GC Roots能直接关联的对象,由于直接关联对象比较小,速度非常快。
并发标记阶段(耗时):从GC Roots直接关联对象开始遍历整个对象图的过程,可以和用户线程并发执行,耗时,不会STW
重新标记阶段(STW):所有用户线程暂停,修正并发标记期间,因用户线程继续运作而导致标记产生变动部分对象的标记记录
并发清除阶段(耗时):清理标记阶段判断为死亡的对象,释放空间内存,不需要移动存活对象,可以和用户线程并发执行

4、G1 收集器:G1 垃圾收集器具备并行、并发以及增量压缩,且暂停时间较短。与 CMS 收集器使用的内存布局不同,G1 收集器将堆内存划分为大小相同的区域,通过多个线程触发全局标记阶段。标记阶段完成后,G1 知道哪个区域可能大部分是空的,并首选该区域作为清除/删除阶段。在 G1 收集器中,一个对象如果大小超过半个区域容量会被认为是一个“大对象” 。这些对象被放置在老年代中,在一个被称为“humongous region”的区域中。
从分代上看,G1依然它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
将堆空间分为若干个区域( Region) ,这些区域中包含了逻辑上的年轻代和老年代。
和之前的各类回收器不同,它同时管理年轻代和老年代
在这里插入图片描述

17.java对象分配过程

在这里插入图片描述
对象的内存布局:在这里插入图片描述

18.java对象都是在堆上分配吗?

Java中的对象不一定是在堆上分配的,因为JVM通过逃逸分析,能够分析出一个新对象的使用范围,并以此确定是否要将这个对象分配到堆上。

19 volatile

加入volatile关键字,汇编代码会多出一个lock前缀,实际上相当于一个内存屏障,保证指令重排序时,不会将屏障前面或后面的指令排到后面或前面。其次会将缓存的修改操作立即写入主存,如果是写操作,它会导致其他cpu中对应的缓存行无效。
jvm内存屏障策略:
storestore,storeload,loadload,loadstore

四、Spring相关

20.说一下你对spring核心模块的理解

spring是一个轻量级的ioc和aop的容器框架,目的是为了简化企业应用的开发,使开发者只需要关心业务需求。常见的配置方式有,基于xml配基于注解配基于java的配置。
主要有以下几个模块组成
spring Core:核心类库,提供ioc
spring context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等)
spring aop:aop服务
spring dao:对jdbc抽象,简化了数据库访问异常的处理
spring orm:对现有的orm框架支持
spring mvc:提供面向web应用的model-view-controller实现

21. 说说spring的ioc与aop

ioc:ioc就是控制反转,指创建对象的控制权转移 ,以前创建对象是自己把控的,而现在是由spring容器来创建,并由容器根据配置文件去创建实例及各实例间的依赖关系。降低了耦合度,也利于功能复用。DI依赖注入,即程序在运行期间依赖ioc容器来动态注入对象所需的外部资源。
aop:面向切面编程,能够将业务无关的逻辑(日志、事务、权限控制等)封装起来,减少重复代码,减低耦合度,便于维护和扩展。spring aop是基于动态代理实现的,如果代理对象实现了某个接口,那么会以jdk动态代理去创建代理对象;没有实现接口的对象,会使用cglib动态代理生成一个被代理对象的子类来作为代理。
spring aop中已经集成了AspectJ,是java生态系统中最完整的aop框架。spring aop是运行时增强,而AspectJ是编译时增强。 spring aop是基于代理,而AspectJ是记基于字节码操作。

22.简述spring bean生命周期

在这里插入图片描述
1.实例化bean对象:通过反射方式进行对象的创建,此时创建只是在堆空间中申请空间,属性都是默认值
2.设置对象属性:给对象中的属性赋值
3. 检查Aware相关接口并设置依赖:如果对象需要引用容器内部的对象,那么需要调用aware接口的子类方法来统一设置
4. BeanPostProcessor的前置处理:对生成的bean对象进行前置处理工作
5. 检查是否是initializingBean的子类来决定是否调用afterPropertiesSet方法,判断当前bean对象是否设置了initializingBean接口,然后进行属性设置等基本工作。
6. 检查是否配置有自定义的init-method:如果有,那么调用自定义方法进行初始化
7. BeanPostProcessor的后置处理:对生成的bean对象进行后置处理工作
8. 注册必要的destruction相关接口回调:方便对象销毁操作
9. 是否实现Disposabale接口
10.是否配置自定义的destory-method

BeanProcessor和BeanFactorProcessor有什么区别

23.spring怎么解决循环依赖问题

spring是用三级缓存来解决循环依赖问题
一级缓存:singletonObjects(单例池)
二级缓存:earlySingletionObjects(存放不完整的对象,或代理对象)
三级缓存:SingletonFactories(存放lamda表达式(beanName,Beandefinition,bean))
A依赖B,B依赖A
A对象:
0.CreatingSet(“a”)
1.创建A普通对象->放入SingletonFactories<a,lamda>. 其中Lamda是创建a代理对象的方法。
2.填充b属性->在单例池中找B对象->创建B对象
3.填充其他属性
4.其他操作
5.初始化后(aop)->在earlyeProxyRefrences中get提前aop的代理对象判断是否是一个对象,若没有,再去aop。
6.放入单例池

B对象:
1.创建B普通对象
2.填充a属性-> 在单例池中找A对象->creatingSet中发现正在创建A对象(产生循环依赖)->在二级缓存earlySingletionObjects中找->在SingletonFactories找->调用lamda,根据aop情况(是否提前AOP)创建代理对象或返回普通对象->放入二级缓存并删除三级缓存(lamda表达式只能执行一次)。
3.填充其他属性
4.其他操作
5.初始化后(aop)
6.放入单例池

24.spring事务失效原因

1.bean对象没有被spring容器管理
2.方法访问修饰符不是public
3.自身调用问题,自调用类里面使用this调用本类方法,此时this对象不是代理类。
4.数据源没有配置事务管理器
5.数据库不支持事务
6.异常被捕获
7.异常类型错误或者配置错误

25.spring的bean是线程安全的吗

spring本身对bean没有做线程安全处理,所以当bean本身是无状态的时候是线程安全的,若bean有状态,那么不是线程安全
对于有状态的bean,如果需要共享变量,则需要去加锁处理。如果不需要共享变量可以使用treadlocal。

26.spring的事务传播机制

REQUIRED(Spring默认的事务传播类型 required:需要、依赖、依靠):如果当前没有事务,则自己新建一个事务,如果当前存在事务则加入这个事务
当A调用B的时候:如果A中没有事务,B中有事务,那么B会新建一个事务;如果A中也有事务、B中也有事务,那么B会加入到A中去,变成一个事务,这时,要么都成功,要么都失败。(假如A中有2个SQL,B中有两个SQL,那么这四个SQL会变成一个SQL,要么都成功,要么都失败)

SUPPORTS(supports:支持;拥护):当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
如果A中有事务,则B方法的事务加入A事务中,成为一个事务(一起成功,一起失败),如果A中没有事务,那么B就以非事务方式运行(执行完直接提交);

MANDATORY(mandatory:强制性的):当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
如果A中有事务,则B方法的事务加入A事务中,成为一个事务(一起成功,一起失败);如果A中没有事务,B中有事务,那么B就直接抛异常了,意思是B必须要支持回滚的事务中运行

REQUIRES_NEW(requires_new:需要新建):创建一个新事务,如果存在当前事务,则挂起该事务。
B会新建一个事务,A和B事务互不干扰,他们出现问题回滚的时候,也都只回滚自己的事务;

NOT_SUPPORTED(not supported:不支持):以非事务方式执行,如果当前存在事务,则挂起当前事务
被调用者B会以非事务方式运行(直接提交),如果当前有事务,也就是A中有事务,A会被挂起(不执行,等待B执行完,返回);A和B出现异常需要回滚,互不影响

NEVER(never:从不): 如果当前没有事务存在,就以非事务方式执行;如果有,就抛出异常。就是B从不以事务方式运行
A中不能有事务,如果没有,B就以非事务方式执行,如果A存在事务,那么直接抛异常

NESTED(nested:嵌套的)嵌套事务:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
如果A中没有事务,那么B创建一个事务执行,如果A中也有事务,那么B会会把事务嵌套在里面。

spring事务什么时候失效

1.方法不是public的
2.发生自调用,使用的不是代理对象而是本身对象。
3.错误的异常处理
4.数据库本身不支持事务
5.没用被spring管理

Spring MVC的工作流程

  1. 用户发请求到前端控制器DispatchServlet。
  2. DispatchServlet收到请求调用HandlerMapping处理映射(HandlerMapping:url–>handler)
  3. 找到具体的处理器,将具体的处理器和拦截器返回给DispatchServlet
  4. 调用HandlerAdapter适配器
  5. HandlerAdapter经过适配具体的Controller
  6. Controller执行完成后返回ModelAndView
  7. HandlerAdapter将ModelAndView返回给DispatchServlet
  8. DispatchServlet将ModelAndView返回给ViewReslover
  9. ViewReslover解析返回具体的view
  10. DispatchServlet对view进行渲染并响应用户

SpringBoot自动配置原理

在这里插入图片描述
在这里插入图片描述

Mysql相关

27.mysql索引

索引是是帮助MySQL高效获取数据的数据结构,和具体的存储引擎有关,innodb的索引实现为B+树,B+树是一个平衡多叉树,数据存储在叶子节点,并且有双向指针,可以高效的进行范围查找。
索引分为聚簇索引和非聚簇索引,和数据绑定在一起的索引称为聚簇索引,一般是主键。为了避免数据冗余存储,其他索引的叶子节点中存储的是聚簇索引的key值, 因此在查询非聚簇索引时需要回表操作。
索引类型有:
1.主键索引:索引列中的值必须是唯一的,不允许有空值。
2.普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值。(在普通列上建立的索引)
3.唯一索引:索引列中的值必须是唯一的,但是允许为空值。
4.全文索引:只能在文本类型CHAR,VARCHAR,TEXT类型字段上创建全文索引。字段长度比较大时,如果创建普通索引,在进行like模糊查询时效率比较低,这时可以创建全文索引。 MyISAM和InnoDB中都可以使用全文索引。
5.,空间索引:MySQL在5.7之后的版本支持了空间索引,而且支持OpenGIS几何数据模型。MySQL在空间索引这方面遵循OpenGIS几何数据模型规则。
5.前缀索引:在文本类型如CHAR,VARCHAR,TEXT类列上创建索引时,可以指定索引列的长度,但是数值类型不能指定。

组合索引的最左前缀匹配原则:使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)就停止匹配。
覆盖索引
覆盖索引并不是说是索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到主键值,相当于获取数据还需要再根据主键查询主键索引再获取到数据。但是试想下这么一种情况,在上面abc_innodb表中的组合索引查询时,如果我只需要abc字段的,那是不是意味着我们查询到组合索引的叶子节点就可以直接返回了,而不需要回表。这种情况就是覆盖索引。
索引失效的情况:
1.不符合最左匹配。2.在索引列上使用函数或其他操作。3.使用范围查询列右边的失效(>,<,between,and,!=)4.使用 is null,is not null,5.like以%开头。6.使用or中有非索引列。

28.ACID是怎么保证的

原子性:由undolog日志来保证,记录需要回滚的日志信息,事务回滚时撤销执行成功的sql
一致性:通过其他三个保证。
隔离性:通过MVCC保证
持久性:通过redolog保证,mysql修改数据时会在redolog记录日志,只要redolog记录成功,就算数据没有保存成功,数据仍然不会丢失。

29. 如何理解MVCC

多版本并发控制,是一种解决读写冲突的无锁并发控制,为事务分配单独增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的快照。可以在并发读时,不阻塞写操作;解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
mvcc在mysql中实现是由三个隐藏字段(DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID),undolog,readview来实现。
readview中有三个全局属性:
trx_list:维护read view生成时系统正在活跃的事务id
up_limit_id:trx_list中最小的事务id
low_limit_id:read view 生成时尚未分配的下一个事务id
比较规则:
1.比较db_trx_id<up_limit_id,则当前事务能看到db_trx_id所在的记录
2.比较db_trx_id>=low_limit_id,则代表db_trx_id所在的记录在read view生成之后出现的,那么对当前事务不可见。
3.判断db_trx_id是否在活跃事务中,如果在代表read view 生成时,该事务仍在活跃,该没有commit,修改的数据当前事务不可见。反之可见。

RC和RR下快照读不同:生成快照的时机不同,RC隔离级别下,每个快照读都会生成获取最新的read view,而在RR隔离级别下则是同一个事务中的第一个快照读才会创建read view,之后的快照读都是同一个read view。

30 mysql的主从复制

Mysql的主从复制指数据从一个mysql主节点复制到一个或多个从节点。mysql采用异步复制。
1.可以做读写分离
2.数据热备
3.架构扩展
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Mybatis

在这里插入图片描述

mybatis工作原理

在这里插入图片描述

协议相关

TCP

为什么tcp需要三次握手,而不是两次,四次

如果是两次握手无法确认客户端的接受能力,由于三次握手已经能确定双方的发送和接受能力,四次的话就浪费资源了。

TCP的四次挥手,为什么不是三次

不能将2,3合成一次,因为在ACK+3和FIN中间服务器需要释放资源,有一定的等待时间,此时如果时间超时,会引起客户端重试。
在这里插入图片描述

TCP粘包问题怎么解决

因为TCP是一种面向连接的、可靠的字节流协议,而在网络传输中,交换机会根据MTU进行合并或拆分包,因此会导致粘包出现。一般解决方案有三种,是通过上层协议实现的。

  1. 定长数据包。2.特殊分隔符,3TLV:type length,value。通过报文的length来确定数据分包。

Redis

redis为什么快

1.基于内存操作
2.redis执行命令是单线程的,避免上下文切换
3.渐进式rehash和、缓存时间戳

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值