2021面试

98 阅读7分钟

@[TOC]

A

B

C

Java基础

JVM & JMM

JMM (Java Memory Model)java内存模型

它是一种虚拟协议规范,主要为了解决在并发编程情况下出现的原子性、可见性、有序性的问题,它的实现方式在java中通过synchronized、Volatile、final、concurren包等,其中synchronized通过monitorenter和monitorexit关键字来保重多线程情况下只有一个线程可以执行被锁住的代码,从而达到原子性、有序性,而Volatile通过禁止指令重排,来保证所有被其生命的变量在更新到主存后都能同步到工作内存当中,达到有序性和可见性,不能保证原子性。

GC回收机制

首先我们要清楚GC的目的,触发GC的条件,当我们使用new来创建对象的时候,我们将这个对象的引用称为可达,当该对象为null的时候我们称为不可达,而一个对象不可达的时候就会触发gc将其回收。
hotspot jvm将堆内存划分为新生代、老年代,新生代内存空间分为eden、survivor两个区,一般情况下新创建的对象会被分配到新生代中的eden或survivorfrom区,当新生代内存空间不足的时候会触发一次minorGC,minorGC采用的是复制算法,其基本思想就是将内存一分为二,每次只用其中一块,当这块内存用完将幸存的对象复制到另外一块上面,另外复制算法不会产生内存碎片,而这个过程也就是将eden、survivorfrom区中的对象复制到survivorto中并清空from里的对象,这样的步骤会重复执行,每执行一次幸存下来的对象年龄会+1,当达到新生代的年龄阈值后,这个对象会被移至老年代。因为对象的创建相对频繁,所以会经常触发minorGC。
当一个对象大到老年代容纳不下或者在minorGC中幸存下来的对象大于老年大所剩的连续内存空间就会触发majorGC,一般majorGC都会伴随一次minorGC,MajorGC清理的是老年代内存空间,fullGC可以理解为minorGC加上MajorGC。
避免fullGC的方法主要是尽可能将对象生命周期缩短,适当的使用弱引用,避免大方法,避免一个变量跨方法调用,且被不同方法调用,对象不用了就即时清空。

类加载机制

类加载过程其实就是jvm ClassLoader类加载器将Class加载到jvm中,并将class字节码重新解析成JVM统一要求的对象格式的一个过程。 过程分为:加载、验证、准备、解析、初始化;
加载:这个阶段会在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的入口;
验证:确保class文件中的字节流锁包含的信息是否满足虚拟机要求;
准备:正式为类变量分配内存并设置类变量初始值;
解析:虚拟机将常量池中的符号引用替换为直接引用的过程;
(符号引用就是一个类中,引入了其他的类,可是JVM并不知道引入的其他类在哪里,所以就用唯一符号来代替,等到类加载器去解析的时候,就把符号引用找到那个引用类的地址,这个地址也就是直接引用。)
初始化:真正执行类中定义的java代码。
java类加载器分为bootstrapClassLoader 、extClassLoader、applicationClassLoader,这里需要说明的是JVM默认采用的加载方式是双亲委派机制,也就是当一个类被加载时,首先将这个加载请求委托给父类,层层委托,只有当最顶层的父类无法满足委托时才会自己去加载,双亲委派的好处是使用不同类加载器最终得到的对象是同一个。
不会被初始化的6种情况:
1.通过子类引用父类的静态变量,父类初始化,子类不会;
2,定义对象数组,该类不会初始化;
3.通过类名获取class对象;
4.常量在编译期间会存放在常量池,本质上没有直接引用定义常量的类,所以定义常量的类不会;
5.class.forName加载指定的类时,initialize为false的时候;
6.通过classLoader默认的loadClass方法;

并发编程

CAS(CompareAndSwap)

它也是一种理念规范,想阐述的是只有当内存值等于预期值的时候才会将新值更新、赋值给内存值,这是一种乐观的态度,总认为自己的操作会成功,当多个线程同事使用CAS操作一个变量时,只有一个线程会成功,失败的也不会被挂起,而是告知失败,并允许再次尝试,当然也允许失败的线程放弃。
(需要注意的是CAS会产生ABA的问题,也就是当多个线程操作同一个内存数据的时候,线程1获取内存数据a,同事线程2也获取到内存数据a,并将内存数据a改成b,然后线程2又将b改回了a,这个时候线程1发现内存数据与预期值相同,对内存数据a进行操作,虽然线程1的操作成功执行,但不代表这个过程没问题。)
一般解决aba问题是通过版本号,每次执行数据修改的时候都会带上版本号,一旦版本号和数据版本号一直就进行修改,并对版本号+1,否则操作失败,因为每次版本号都会增加所以不会出现aba的问题。

AQS(AbstractQueuedSynchronizer)抽象队列同步器

首先它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。state的访问方式有三种:
getState()
setState()
compareAndSetState()
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。   以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
      再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
      一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。