一、基础
1.1 基本数据类型
基本类型 | 大小(字节) | 包装类型 | 默认 | 包装类型常量池 |
---|---|---|---|---|
byte | 1 | Byte | 0 | -128~127 |
short | 2 | Short | 0 | -128~127 |
int | 4 | Integer | 0 | -128~127 |
long | 8 | Long | 0L | -128~127 |
float | 4 | Float | 0f | - |
double | 8 | Double | 0d | - |
boolean | - | Boolean | false | true、false |
char | 2 | Character | ‘u0000’ | 0~127 |
1.2 类的初始化顺序
存在继承的情况下,初始化顺序为:
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
1.3 重载和重写
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写:
- 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 - 构造方法无法被重写
1.4 泛型
1.4.1 泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
1.4.2 泛型接口
public interface Generator<T> {
public T method();
}
//实现1:不指定类型
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
//实现2:指定类型
class GeneratorImpl implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
1.4.3 泛型方法
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
}
1.4.4 无界通配符 ?
1.4.5 上界通配符<? extends Number>
1.4.6 下界通配符<? super Number>
1.5 ==和equals()的区别
== | equals | |
---|---|---|
基本类型 | 比较值 | (无法调用) |
引用类型 | 比较内存地址 | 如果不重写方法则调用Object的equals,等价于== 。String重写了equals方法进行比较字符串的值 |
1.6 字符串的创建
String str = new String("abc");//new创建:在堆上创建一个对象,如果常量池中没有"abc",则会在池中也创建一个对象
String str1 = "str";//常量池中创建对象
String str2 = "ing";
String str3 = "str" + "ing";//常量池中创建对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string"; //常量池中已存在,不需要创建,
String str6 = str1+"ing";//在堆上创建的新的对象
1.7 hashCode()和equals()
1.7.1 为什么重写 equals() 时必须重写 hashCode() 方法?
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
equals相等,hashCode一定相等
hashCode相等,equals不一定相等
1.8 String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
1.8.1 String 为什么是不可变的?
因为String类用final修饰,不可被继承,而且不提供修改保存数据的char[]数组的方法。
1.8.2 区别
可变 | 线程安全 | 性能 | |
---|---|---|---|
String | 不可变 | 线程安全 | - |
StringBuffer | 可变 | 线程安全 | 慢 |
StringBuilder | 可变 | 线程不安全 | 快 |
1.9 浅拷贝和深拷贝
1.9.1 浅拷贝
浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
public class ConcretePrototype implements Cloneable{
private String attr;//成员属性
private User user;
//set get..
@Override
protected ConcretePrototype clone() throws CloneNotSupportedException {
return (ConcretePrototype)super.clone();
}
}
1.9.2 深拷贝
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
public class ConcretePrototype implements Serializable{
private String attr;//成员属性
private User user;
//set get...
protected ConcretePrototype deepClone() {
try {
//将对象写入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bai = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bai);
return (ConcretePrototype) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
1.10 代理
1.10.1静态代理
public interface Hello {
void sayHello(String name);
}
public class HelloSpeaker implements Hello {
@Override
public void sayHello(String name) {
System.out.println("hello "+name);
}
}
public class HelloProxy implements Hello{
private Hello hello;
public HelloProxy(Hello hello) {
this.hello = hello;
}
@Override
public void sayHello(String name) {
before();
hello.sayHello(name);
after();
}
private void before(){
System.out.println("我执行在代理方法执行之前");
}
private void after(){
System.out.println("我执行在代理方法执行之后");
}
}
public class Main {
public static void main(String[] args) {
Hello hello = new HelloProxy(new HelloSpeaker());
hello.sayHello("xpp");
}
}
1.10.2 动态代理之JDK动态代理
public interface Hello {
void sayHello(String firstName,String lastName);
}
public class HelloSpeaker implements Hello{
public void sayHello(String firstName,String lastName){
System.out.println("hello "+firstName+lastName);
}
}
public class LoggingHandler<T> implements InvocationHandler {
private T target;//被代理类
public T bind(T target){
this.target = target;
return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;//执行代理方法的返回结果
log(method.getName()+"方法执行之前");
result = method.invoke(target,args);
log(method.getName()+"方法执行之后");
return result;
}
private void log(String msg){
LoggerFactory.getLogger(getClass()).info(msg);
}
}
public class Main {
public static void main(String[] args) {
LoggingHandler<HelloSpeaker> loggingHandler = new LoggingHandler();
Hello hello = loggingHandler.bind(new HelloSpeaker());
hello.sayHello("x","pp");
}
}
1.10.3 动态代理之CGLIB动态代理
public class AliSmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
public class DebugMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}
}
public class Main {
public static void main(String[] args) {
AliSmsService aliSmsService =(AliSmsService)CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("aabb");
}
}
二、容器(集合)
2.1 说说 List, Set, Queue, Map 四者的区别?
-
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。 -
Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。 -
Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 -
Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值.
2.2 Collection 子接口之 Queue
Queue
是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
Queue
扩展了 Collection
的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
Queue 接口 | 抛出异常 | 返回特殊值 |
---|---|---|
插入队尾 | add(E e) | offer(E e) |
删除队首 | remove() | poll() |
查询队首元素 | element() | peek() |
Deque
扩展了 Queue
的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
Deque 接口 | 抛出异常 | 返回特殊值 |
---|---|---|
插入队首 | addFirst(E e) | offerFirst(E e) |
插入队尾 | addLast(E e) | offerLast(E e) |
删除队首 | removeFirst() | pollFirst() |
删除队尾 | removeLast() | pollLast() |
查询队首元素 | getFirst() | peekFirst() |
查询队尾元素 | getLast() | peekLast() |
事实上,Deque
还提供有 push()
和 pop()
等其他方法,可用于模拟栈。
2.1 HashMap的putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.2 HashMap 的长度为什么是 2 的幂次方
取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)
采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方
2.3 线程安全的集合
线程不安全 | 线程安全 |
---|---|
ArrayList | CopyOnWriteArrayList |
HashSet | CopyOnWriteArraySet |
HashMap | ConcurrentHashMap |
2.4 数组和集合之间转换
2.4.1 集合转数组
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
//没有指定类型的话会报错
s=list.toArray(new String[0]);
2.4.2 数组转集合
//方法一
String[] arr ={"a","b"};
List<String> list = new ArrayList<>(Arrays.asList(arr))
//方法二
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
三、并发编程
3.1 Java内存区域
3.2 为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
3.2.1 程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
3.2.2 虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程
3.2.3 一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
3.3 多线程的创建
3.3.1 继承Thread类
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
3.3.2 实现Runnable
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
3.3.4 实现Callable
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
}
}
class Task implements Callable<String> {
public String call() throws Exception {
return longTimeCalculation();
}
}
3.4 线程状态
当线程执行
wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)
方法或wait(long millis)
方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。
- t.yield():让出CPU,进入就绪状态,等待获取CPU(让出完成之后有可能立马又抢到CPU)。
- t.join():线程还可以等待另一个线程直到其运行结束。比如t线程调用t.join()方法,则其他线程需要等待t线程运行结束。
- Thread.sleep(long):线程睡眠,不让出CPU,睡眠时间结束后继续运行。
- object.wait()/object.wait(long):让出CPU,线程进入等待,等待调用object.notify()或object.notifyAll()方法进行唤醒。(有时间设置的,到时间自动唤醒)
- object.notify()/object.notifyAll():唤醒一个/所以的线程去抢占锁,没抢到锁的线程继续回到等锁池。
3.5 synchronized
- 修饰实例方法:锁的是当前对象(this);
- 修饰静态方法:锁的是当前类Class;
- 修饰代码块:锁的是synchronized后面括号里的对象或者类;
3.6 volatile
轻量级锁
- 不保证原子性
- 保证可见性
- 禁止指令重排
3.7 ReentrantLock+Condition实现和synchronized+wait()+notifyAll()同样效果
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
3.8 并发编程的三个重要特性
原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized
可以保证代码片段的原子性。
可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile
关键字可以保证共享变量的可见性。
有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile
关键字可以禁止指令进行重排序优化。
3.9 说说 synchronized 关键字和 volatile 关键字的区别
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
3.10 ThreadLocal
线程的私有变量,使用完成之后注意调用remove(),进行资源的释放,防止内存泄漏。
通过
Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象,ThreadLocal的set(value)方法保存数据,key为当前的ThreadLocal实例this。
3.11 线程池
阿里编码规约希望通过new ThreadPoolExecutor(...)方式创建线程池,有利于清楚的知道线程池的创建规则
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1.corePoolSize代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程
2.maximumPoolSize 代表的是最大线程数,与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务比较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
3.keepAliveTime 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,keepAliveTime设置空闲时间
4.unit为keepAliveTime的时间单位
5.workQueue用来存放待执行的任务,假设我们现在核心线程都被使用,还有任务进来,则全部放入队列,直到整个队列被放满但任务还再持续进来,则会开始创建新的线程
6.threadFactory 线程工厂,就是用来创建线程的
7.handler 任务拒绝策略
3.12 Atomic 原子类
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
3.13 CountDownLatch
CountDownLatch
的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
public class CountDownLatchExample1 {
// 处理文件的数量
private static final int threadCount = 6;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {
try {
//处理文件的业务操作
//......
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//表示一个文件已经被完成
countDownLatch.countDown();
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
}
四、JVM
4.1 双亲委派
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
4.1 Java创建对象的过程
-
类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
-
分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
-
初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
-
设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的原数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。
-
执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
4.2 堆
对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为大于 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置默认值,这个值会在虚拟机运行过程中进行调整,可以通过-XX:+PrintTenuringDistribution
来打印出当次GC后的Threshold。Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。
有可能当次Minor GC后,Survivor 的"From"区域空间不够用,有一些还达不到进入老年代条件的实例放不下,则放不下的部分会提前进入老年代
4.3 如何判断对象已经无效?
- 引用计数法(已淘汰):给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。但是难以解决循环引用问题。
- 可达性分析:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的 。
能作为GC Roots的对象:
- 虚拟机栈(栈针中的本地变量表)中引用的对象。
- 本地方法栈(Native方法)中引用的对象
- 方法区中类静态属性(static修饰)引用的对象
- 方法区中常量(final修饰)引用的对象
- 所有被同步锁持有的对象
4.4 如何判断一个字符串常量是废弃的?
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。
4.5 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
4.6 垃圾收集算法
4.7 垃圾收集器
五、定位CPU性能问题
5.1 环境
基础环境 jdk1.8,采用 SpringBoot 框架来写几个接口来触发模拟场景,首先是模拟 CPU 占满情况
5.2 CPU占满
/**
* 模拟CPU占满
*/
@GetMapping("/cpu/loop")
public void testCPULoop() throws InterruptedException {
System.out.println("请求cpu死循环");
Thread.currentThread().setName("loop-thread-cpu");
int num = 0;
while (true) {
num++;
if (num == Integer.MAX_VALUE) {
System.out.println("reset");
}
num = 0;
}
}
请求接口地址测试
curl localhost:8080/cpu/loop
,发现 CPU 立马飙升到 100%,top查看
通过执行
top -Hp 32805
查看 Java 线程情况
执行
printf '%x' 32826
获取 16 进制的线程 id,用于dump
信息查询,结果为803a
。最后我们执行jstack 32805 |grep -A 20 803a
来查看下详细的dump
信息。
这里
dump
信息直接定位出了问题方法以及代码行,这就定位出了 CPU 占满的问题。
六、事务
6.1 事务的特性(ACID)
- 原子性
- 一致性
- 隔离性
- 持久性
6.2 事务的隔离级别
脏读:事务2读取到事务1还未提交的修改,当事务1进行回滚,则事务2读取到的数据为脏数据,这就是脏读。
不可重复读:在一个事务中,同一条查询语句,执行两次,发现读取的内容是不一样的。
幻读:在一个事务中,同一条查询语句,执行两次,第二次查询结果中出现第一次未出现的行或缺少了某行(有数据新增或者删除)。
- 读未提交(可能会导致脏读、幻读或不可重复读)
- 读已提交(可以阻止脏读,但是幻读或不可重复读仍有可能发生)
- 可重复读(可以阻止脏读和不可重复读,但幻读仍有可能发生。)
- 序列化(该级别可以防止脏读、不可重复读以及幻读)
6.3 事务的传播
- PROPAGATION_REQUIRED —如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中
- PROPAGATION_REQUIRES_NEW —创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- PROPAGATION_NESTED —如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。
- PROPAGATION_MANDATORY —如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- PROPAGATION_SUPPORTS: —如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- PROPAGATION_NOT_SUPPORTED:—以非事务方式运行,如果当前存在事务,则把当前事务挂。
- PROPAGATION_NEVER: —以非事务方式运行,如果当前存在事务,则抛出异常。
七、Spring
Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
7.1 IOC
IOC(Inverse of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IOC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。
- 控制 :指的是对象创建(实例化、管理)的权力
- 反转 :控制权交给外部环境(Spring 框架、IoC 容器)
7.2 AOP
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
7.3 Spring Bean
Bean指的是交由Spring容器进行管理的对象。
Bean的作用于:
-
singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的,对单例设计模式的应用。
-
prototype : 每次请求都会创建一个新的 bean 实例。
-
request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
-
session : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
-
global-session : 全局 session 作用域,仅仅在基于 portlet 的 web 应用中才有意义,Spring5 已经没有了。Portlet 是能够生成语义代码(例如:HTML)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。
//@Bean不指定name,则会使用方法名,即‘personPrototype’
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}
7.4 Spring MVC
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
7.4.1 SpringMVC 工作原理
流程说明(重要):
- 客户端(浏览器)发送请求,直接请求到
DispatcherServlet
。 DispatcherServlet
根据请求信息调用HandlerMapping
,解析请求对应的Handler
。- 解析到对应的
Handler
(也就是我们平常说的Controller
控制器)后,开始由HandlerAdapter
适配器处理。 HandlerAdapter
会根据Handler
来调用真正的处理器开处理请求,并处理相应的业务逻辑。- 处理器处理完业务后,会返回一个
ModelAndView
对象,Model
是返回的数据对象,View
是个逻辑上的View
。 ViewResolver
会根据逻辑View
查找实际的View
。DispaterServlet
把返回的Model
传给View
(视图渲染)。- 把
View
返回给请求者(浏览器)
7.5 常见注解
7.5.1 @PathVariable和@RequestParam
@PathVariable
用于获取路径参数,@RequestParam
用于获取查询参数
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
@PathVariable("klassId") Long klassId,
@RequestParam(value = "type", required = false) String type ) {
...
}
如果我们请求的 url 是:/klasses/123456/teachers?type=web
那么我们服务获取到的数据就是:klassId=123456,type=web
。
7.5.2 @RequestBody
用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter
或者自定义的HttpMessageConverter
将请求的 body 中的 json 字符串转换为 java 对象。
@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody UserRegisterRequest userRegisterRequest) {
userService.save(userRegisterRequest);
return ResponseEntity.ok().build();
}
7.5.3 @ControllerAdvice和@ExceptionHandler
全局异常处理
@ControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result exception(Exception e){
return new Result(false,e.getMessage());
}
}
7.5.4 @EventListener
@Component
public class Main {
@EventListener(ApplicationReadyEvent.class)
public void init(){
out.println("上下文初始化后我就可以执行了。。。");
}
}
八、消息队列(MQ)
1.消息队列的用作
解耦、异步、削峰
2.如何保证消息队列高可用性?
主从配置(RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式)
3. 如何保证消息不被重复消费?
根据业务进行处理(代码层面),比如:数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了
九、Redis缓存
1.缓存的作用
高性能、高并发
2.过期策略有哪些?
为什么会过期?原因:①内存有限,当有新的数据写入时,但是内存已满,则会淘汰旧的。②设置了过期时间,时间到了就过期了。
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
3.如何保证Redis高并发、高可用?
-
Redis主从配置
-
Redis基于哨兵实现高可用
4.雪崩、穿透、击穿
- 雪崩:同一时间大面积缓存失效,数据访问全都落在mysql上。
解决方案:
事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 - 穿透:黑客恶意访问不存在的key,导致数据访问全都落在mysql上。
解决方案:
对于不可能存在的key(如id<0),直接返回错误提示。
布隆过滤器 - 击穿:某个热点key失效的瞬间,大量的数据访问全都落在mysql上。
解决方案:对于热点数据设置永不过期。