Java学习笔记

一、基础

1.1 基本数据类型

基本类型大小(字节)包装类型默认包装类型常量池
byte1Byte0-128~127
short2Short0-128~127
int4Integer0-128~127
long8Long0L-128~127
float4Float0f-
double8Double0d-
boolean-Booleanfalsetrue、false
char2Character‘u0000’0~127

1.2 类的初始化顺序

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

1.3 重载和重写

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

重写:

  1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写

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 线程安全的集合

线程不安全线程安全
ArrayListCopyOnWriteArrayList
HashSetCopyOnWriteArraySet
HashMapConcurrentHashMap

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 程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
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(阻塞) 状态。

  1. t.yield():让出CPU,进入就绪状态,等待获取CPU(让出完成之后有可能立马又抢到CPU)。
  2. t.join():线程还可以等待另一个线程直到其运行结束。比如t线程调用t.join()方法,则其他线程需要等待t线程运行结束。
  3. Thread.sleep(long):线程睡眠,不让出CPU,睡眠时间结束后继续运行。
  4. object.wait()/object.wait(long):让出CPU,线程进入等待,等待调用object.notify()或object.notifyAll()方法进行唤醒。(有时间设置的,到时间自动唤醒)
  5. object.notify()/object.notifyAll():唤醒一个/所以的线程去抢占锁,没抢到锁的线程继续回到等锁池。

3.5 synchronized

  1. 修饰实例方法:锁的是当前对象(this);
  2. 修饰静态方法:锁的是当前类Class;
  3. 修饰代码块:锁的是synchronized后面括号里的对象或者类;

3.6 volatile

轻量级锁

  1. 不保证原子性
  2. 保证可见性
  3. 禁止指令重排

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创建对象的过程

在这里插入图片描述

  1. 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

  2. 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

在这里插入图片描述

  1. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  2. 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的原数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。

  3. 执行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;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。但是难以解决循环引用问题。
  2. 可达性分析:这个算法的基本思想就是通过一系列的称为 “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)

  1. 原子性
  2. 一致性
  3. 隔离性
  4. 持久性

6.2 事务的隔离级别

脏读:事务2读取到事务1还未提交的修改,当事务1进行回滚,则事务2读取到的数据为脏数据,这就是脏读。
不可重复读:在一个事务中,同一条查询语句,执行两次,发现读取的内容是不一样的。
幻读:在一个事务中,同一条查询语句,执行两次,第二次查询结果中出现第一次未出现的行或缺少了某行(有数据新增或者删除)。

  1. 读未提交(可能会导致脏读、幻读或不可重复读)
  2. 读已提交(可以阻止脏读,但是幻读或不可重复读仍有可能发生)
  3. 可重复读(可以阻止脏读和不可重复读,但幻读仍有可能发生。)
  4. 序列化(该级别可以防止脏读、不可重复读以及幻读)

6.3 事务的传播

  1. PROPAGATION_REQUIRED —如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中
  2. PROPAGATION_REQUIRES_NEW —创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  3. PROPAGATION_NESTED —如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。
  4. PROPAGATION_MANDATORY —如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  5. PROPAGATION_SUPPORTS: —如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  6. PROPAGATION_NOT_SUPPORTED:—以非事务方式运行,如果当前存在事务,则把当前事务挂。
  7. 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 工作原理

在这里插入图片描述

流程说明(重要):

  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter 会根据 Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
  6. ViewResolver 会根据逻辑 View 查找实际的 View
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. 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上。
    解决方案:对于热点数据设置永不过期。
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值