63-Java面试专题(2)-Java并发 + 虚拟机

144 阅读22分钟

一、线程和线程池

①:线程状态

目标

  • 掌握」ava线程的状态

  • 掌握Java线程状态之间的转换

  • 辨析两种说法,六种状态VS五种状态

1. Java中线程分成六种状态\color{#00FF00}{Java中线程分成六种状态}

image.png

1. 初始状态(NEW)

  • 实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.1. 就绪状态(RUNNABLE之READY)

  • 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
  • 调用线程的start()方法,此线程进入就绪状态。
  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
  • 锁池里的线程拿到对象锁后,进入就绪状态。

2.2. 运行中状态(RUNNABLE之RUNNING)

  • 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

3. 阻塞状态(BLOCKED)

  • 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

4. 等待(WAITING)

  • 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

5. 超时等待(TIMED_WAITING)

  • 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

6. 终止状态(TERMINATED)

  • 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
  • 在一个终止的线程上调用start()方法会抛出java.lang.IllegalThreadStateException异常。

2. 操作系统层面有5种状态\color{#00FF00}{操作系统层面有5种状态}

  1. 分到CPU时间的:运行
  2. 可以分到CPU时间的:就绪
  3. 分不到CPU时间的:阻塞

image.png

②:线程池的核心参数

1. 参数详情\color{#00FF00}{参数详情}

image.png

线程池核心参数可以参照实现类ThreadPoolExecutor来探讨(共有七个参数)

/**
使用给定的初始参数创建一个新的ThreadPoolExecutor。 
参数: 
corePoolSize—即使线程是空闲的,也要保留在池中的线程数,除非设置了allowCoreThreadTimeOut 
maximumPoolSize—池中允许的最大线程数 
keepAliveTime—当线程数大于核心数时,这是多余的空闲线程在终止前等待新任务的最大时间。 
unit—keepAliveTime参数的时间单位 
workQueue—用于在执行任务之前保存任务的队列。该队列将只保存由execute方法提交的可运行任务。 
threadFactory——执行程序创建新线程时要使用的工厂 
处理程序——当由于达到线程边界和队列容量而阻塞执行时使用的处理程序 
抛出: 
IllegalArgumentException -如果下列情况之一成立:corePoolSize < 0 keepAliveTime < 0 
maximumPoolSize <= 0 maximumPoolSize < corePoolSize 
如果workQueue或threadFactory或handler为空
*/
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
                          }
  1. corePoolSize核心线程数目
  • 最多保留的线程数
  1. maximumPoolSize最大线程数目
  • 核心线程+救急线程
  1. keepAliveTime生存时间
  • 针对救急线程
  1. unit时间单位
  • 针对救急线程
  1. workQueue
  • 阻塞队列
  1. threadFactory线程工厂
  • 可以为线程创建时起个好名字
  1. handler拒绝策略
  • 四种

2. 代码实现\color{#00FF00}{代码实现}

@Test
public void testThreadPoolExecutor() {
    // 创建工作队列
    ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
    // 原子递增计数器
    AtomicInteger integer = new AtomicInteger(1);

    ThreadFactory threadFactory = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(() -> {
                // integer.getAndIncrement() 每次都增加1
                String s = "myThread" + integer.getAndIncrement();
            });
        }
    };
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            3,
            0,
            TimeUnit.MILLISECONDS,
            queue,
            threadFactory,
            new ThreadPoolExecutor.CallerRunsPolicy());


}

3. JDK内置4种线程池拒绝策略\color{#00FF00}{JDK内置4种线程池拒绝策略}

拒绝策略接口定义

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
  • 接口定义很明确,当触发拒绝策略时,线程池会调用你设置的具体的策略,将当前提交的任务以及线程池实例本身传递给你处理,不同场景会有不同的考虑,下面看下JDK内置了哪些拒绝策略

1.CallerRunsPolicy(调用者运行策略)

public satatic class CallerRunsPolicy implements RejectExecutionHandler{
    public CallerRunsPolicy(){}
    
    public void rejectedExecution(Runnable r,ThreadPoolExecutor e){
        if(!e.isShutdown()){
            r.run();
        }
    }
}
  • 功能:当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。
  • 使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。

2.AbortPolicy(中止策略)

public static class AbortPolicy implements RejectedExecutionHandler{
    public AbortPolicy(){}

    public void rejectedExecution(Runnable r,ThreadPoolExecutor e){
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}
  • 功能:当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程
  • 使用场景:这个就没有特殊的场景了,但是有一点要正确处理抛出的异常。ThreadPoolExecutor中默认的策略就是AbortPolicy,ExecutorService接口的系列ThreadPoolExecutor因为都没有显示的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。

3.DiscardPolicy(丢弃策略)

public static class DiscardPolicy implements RejectedExecutionHandler {

    public DiscardPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}
  • 功能:直接静悄悄的丢弃这个任务,不触发任何动作
  • 使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了

4.DiscardOldestPolicy(弃老策略)

public static class DiscardOldestPolicy implements RejectExecutionHandler{
    public DiscardOldestPolicy(){}
    
    public void rejectedExecution(Runnable r,ThreadPoolExecutor e){
        if(!e.isShutdown()){
            e.getQueue().poll();
            e.execute(r);
        }
    }
}
  • 功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行
  • 使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,想到的场景就是,发布消息和修改消息,当消息发布出去后,还未执行,此时更新的ixaoxi又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。

③:sleep 和 wait 的区别

1. 共同点:

  • wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

2. 方法归属不同

  • sleep(long)是Thread的静态方法

  • 而wait(),wait(long)都是Object的成员方法,每个对象都有

3. 醒来时机不同

  • 执行sleep(long)和Wait(long)的线程都会在等待相应毫秒后醒来

  • Wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去

  • 它们都可以被打断唤醒

@Test
public void testIllegalWait() throws InterruptedException {

    Thread t1 = new Thread(() -> {
        System.out.println("自己创建的线程");
        synchronized (lock) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                System.err.println("被interrupt()打断了");
                e.printStackTrace();
            }
        }
    }, "t1");

    t1.start();
    // 打断唤醒
    t1.interrupt();
}

image.png 4. 锁特性不同

  • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制

image.png

image.png

  • wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃,但你们还可以用)

  • 而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃,你们也用不了)

④:lock 和 synchronized 的区别

1. 语法层面

  • synchronized是关键字,源码在jvm中,用c+语言实现
  • Lock是接口,源码由jdk提供,用java语言实现
  • 使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁

2. 功能层面

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
  • Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
  • Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock

3. 性能层面

  • 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
  • 在竞争激烈时,LOck的实现通常会提供更好的性能

⑤:volatile 能否保证线程安全

1. 线程安全要考虑三个方面\color{#00FF00}{线程安全要考虑三个方面}

  1. 可见性: 一个线程对共享变量修改,另一个线程能看到最新的结果

  2. 有序性: 一个线程内代码按编写顺序执行

  3. 原子性: 一个线程内多行代码以一个整体运行,期间不能有其它线程的代码插队

2. volatile不能保证线程安全\color{#00FF00}{volatile不能保证线程安全}

volatile能够保证共享变量的可见性与有序性,但并不能保证原子性

01. 原子性举例

private static volatile  int  number = 10;
public static  void  add(){
    number += 5;
}
public static  void  subtract(){
    number -= 5;
}
@Test
public void testAddAndSubtract() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(2);
    new Thread(() ->  {
        subtract();
        latch.countDown();
    }).start();
    new Thread(() ->  {
        add();
        latch.countDown();
    }).start();

    latch.await();
}
@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 30000; i++) {
        testAddAndSubtract();
        if (number != 10){
            System.err.println("第 "+ i +"次执行结果: = " + number);
        }
        System.out.println("第 "+ i +"次执行结果: = " + number);
    }
}

image.png

  • 造成这一原因主要有两点:加法和减法方法不是原子性操作; 多线程情况下可能出现交错执行 image.png

02. 可见性举例

private static boolean stop = false;
@Test
public  void testForeverLop(){
    new Thread(() -> {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop = true;
        System.out.println("修改 stop 值为 true");
    }).start();
    foo();
}
private static void foo() {
    System.out.println("foo() 开始执行~~ ");
    int i = 0;
    while (!stop) {
        i++;
    }
    System.out.println("foo() 结束~~ 循环了 " + i+ " 次");
}

image.png

volatile 可见性(原因与解决)

  • 原因

image.png

在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为 “Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。

  • 测试

image.png

image.png

  • 解决(共享变量上加上volatile关键字)
private static volatile boolean stop = false;

image.png

03. 有序性举例

  • 导入依赖(压测工具)
<dependency>
   <groupId>org.openjdk.jcstress</groupId>
   <artifactId>jcstress-core</artifactId>
   <version>0.14</version>
</dependency

image.png

image.png

image.png

二、悲观锁和乐观锁

①:理论

悲观锁的代表是synchronized和Lock锁

  1. 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
  2. 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
  3. 实际上,线程在获取synchronized和Lock锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会

乐观锁的代表是Atomiclnteger,使用cas来保证原子性

  1. 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
  2. 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
  3. 它需要多核Cpu支持,且线程数不应超过Cpu核数

②:悲观锁和乐观锁代码对比

  • 悲观锁

image.png

  • 乐观锁

image.png

三、Hashtable 和 ConcurrentHashMap

①:理论

  1. Hashtable与ConcurrentHashMap都是线程安全的Map集合

  2. Hashtable并发度低,整个Hashtable对应一把锁,同一时刻,只能有一个线程操作它

  3. 1.8之前ConcurrentHashMap使用了Segment+数组+链表的结构,每个Segment对应一把锁,如果多个线程访问不同的Segment,则不会冲突

  4. 1.8开始ConcurrentHashMap将数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突

1. put扩容

image.png

  • 看Hashtable源码也可以得出

image.png

②:Java7 ConcurrentHashMap

说明三个问题forwardingNode,扩容时的get,扩容时的put

1. 并发度

image.png

2. 索引计算

image.png

image.png

3. 扩容

  • 每个segment下的小数组扩容是互不干扰的各扩容各的

image.png

4. segment[0] 原型

  • segment[0]小数组的大小=初始容量/segment的容量结果小于2则等于最小值2

image.png

  • 其他segment下的小数组大小以segment[0]小数组的大小为模型

image.png

image.png

  • 修改segment[0]小数组的大小后在添加元素

image.png

③:ConcurrentHashMap_Java8yuJava7比较

1.数据结构

  • Java7 segment + 数组 + 链表
  • Java8 数组 + (链表 或 红黑树)
  1. 数组初始化时机
  • Java7 一旦调用构造方法 就会创建数组(segment[0]) 饿汉式
  • Java8 调用构造方法并不会创建数组(第一次put元素时创建数组)懒汉式

3.链表插入数据方式

  • Java7 头插法
  • Java8 尾插法

4.数组扩容时机不一样

  • Java7 数据超过数组本身大小的3/4
  • Java8 数据达到数组本身大小的3/4就会扩容

④:Java8 ConcurrentHashMap

1. 构造参数含义

/**
创建一个新的空映射,其初始表大小基于给定的元素数量(initialCapacity)和初始表密度(loadFactor)。 
参数: 
initialCapacity—初始容量。在给定指定的负载因子的情况下,实现执行内部调整以容纳这么多元素。 
loadFactor—用于确定初始表大小的负载因子(表密度)
抛出: 
如果元素的初始容量是负的或者负载因子是非正的
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

image.png

2. 并发put

image.png

3. 扩容

  • 扩容前

image.png

  • 扩容后

image.png

  • 扩容过程 以下视频都来自黑马教学视频

视频链接: reccloud.cn/p/h0fmb3h

4. 扩容细节

  1. 扩容时并发get(查询数据)

视频链接: reccloud.cn/u/uwuay2n

  1. 扩容时并发put(添加数据)

视频链接: reccloud.cn/u/j2ok9kb

四、谈谈对ThreadLocal的理解

①:ThreadLocal的作用

  1. ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题

  2. ThreadLocal同时实现了线程内的资源共享

  • 代码实现
public class TestThreadLocal {
    public static void main(String[] args) {
        test1();
    }
    // 多个线程调用,得到的是自己的Connection 对象
    private static void  test1() {

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Utils.getConnection());
            }, "t" + (i + 1)).start();
        }
    }
    private static class Utils {
        private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

        public static Connection getConnection() {
            // 到当前线程获取资源
            Connection conn = threadLocal.get();
            if (conn == null){
                // 创建新的连接对象
                conn = innerGetConnection();
                // 将资源存入到当前线程中
                threadLocal.set(conn);
            }
            return conn;
        }
        private static Connection innerGetConnection() {
            try {

                return DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&serverTimezone=GMT%2B8", "root", "root");
            } catch (SQLException throwables) {
                throwables.printStackTrace();
                throw new RuntimeException(throwables);            }
        }
    }
    // 一个线程内调用,得到的是同一个Connection 对象
    public static void  test2() {
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                System.out.println(Utils.getConnection());
                System.out.println(Utils.getConnection());
                System.out.println(Utils.getConnection());
            }, "t" + (i + 1)).start();
        }
    }
}
  • 测试1(调用测试方法一)

image.png

  • 测试2(调用测试方法二)

image.png

②:ThreadLocal的原理

思考一个问题一个ThreadLocal对象是怎样实现线程间隔离的呢?

// ThreadLocal是static修饰的 只有一个
private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

1.线程隔离的关键点在于ThreadLocalMap

1.其原理是,每个线程内有一个ThreadLocalMap类型的成员变量,\color{#00FF00}{1. 其原理是,每个线程内有一个ThreadLocalMap类型的成员变量,}

用来存储资源对象\color{#00FF00}{用来存储资源对象}

  • 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中

  • 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值

  • 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值

image.png

2.ThreadLocalMap索引的计算规则\color{#00FF00}{2. ThreadLocalMap索引的计算规则}

image.png

3.添加数据时下标冲突会如何(开放寻址发)\color{#00FF00}{3. 添加数据时下标冲突会如何(开放寻址发)}

开放寻址发又称开放定址法,当哈希碰撞发生时,从发生碰撞的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。这个空闲单元又称为开放单元或者空白单元。

       查找时,如果探查到空白单元,即表中无待查的关键字,则查找失败。开放寻址法需要的表长度要大于等于所需要存放的元素数量,非常适用于装载因子较小(小于0.5)的散列表。

开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记,直到有下个元素插入才能真正删除该元素。

       可以把开放寻址法想象成一个停车问题。若当前车位已经有车,则继续往前开,直到找到一个空停车位。

image.png image.png

4.ThreadLocalMap扩容\color{#00FF00}{4. ThreadLocalMap扩容}

image.png

③:key为何设计为弱引用(释放时机)

为什么ThreadLocalMap中的key(即ThreadLocal)要设计为弱引用?

  1. Thread可能需要长时间运行(如线程池中的线程),如果key不再使用,需要在内存不足(GC)时释放其占用的内存

image.png

④:value内存释放时机

GC仅是让key的内存释放,后续还要根据key是否为nul来进一步释放值的内存,释放时机有

1.获取key发现nullkey\color{#00FF00}{1. 获取key发现null key}

image.png

2.setkey时,会使用启发式扫描,清除临近的nullkey,启发次数与元素\color{#00FF00}{2. set key时,会使用启发式扫描,清除临近的null key,启发次数与元素} 个数,是否发现nullkey有关\color{#00FF00}{个数,是否发现null key有关}

image.png

image.png

3.remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变\color{#00FF00}{3. remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变} 量,因此GC(垃圾回收器)无法回收\color{#00FF00}{量,因此GC(垃圾回收器)无法回收}

image.png

// ThreadLocal是static修饰的 只有一个
private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

综上:最好的办法就是自己调用remove方法

五、虚拟机-jvm

①:jvm内存结构

1. 代码执行流程

image.png

2. 哪些区域会有内存溢出

哪些部分会出现内存溢出

  • 不会出现内存溢出的区域-程序计数器

  • 出现OutOfMemoryError的情况

    • ①堆内存耗尽-对象越来越多,又一直在使用,不能被垃圾回收
    • ②方法区内存耗尽-加载的类越来越多,很多框架都会在运行期间动态产生新的类
    • ③虚拟机栈累积-每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时
  • 出现StackOverflowError的区域

    • ①虚拟机栈内部-方法调用次数过多

3. 方法区-元空间

方法区与永久代、元空间之间的关系

  1. 方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等

  2. 永久代是Hotspot虚拟机对JVM规范的实现(1.8之前)

  3. 元空间是Hotspot虚拟机对JVM规范的实现(1.8以后),使用本地内存作为这些信息的存储空间

image.png

image.png

②:JVM内存参数

image.png

  • 堆内存设置

image.png

  • 元空间内存设置

image.png

  • 代码缓存区

image.png

  • 每个线程占用的内存(虚拟机栈)

image.png

③:jvm垃圾回收

1.jvm垃圾回收算法

垃圾回收算法一共有三种:标记清除、标记整理、标记复制

1.标记清除\color{#00FF00}{1. 标记清除}

  • 缺点:虽然可以释放很多内存(但内存都不连续)碎片化
    • 如数组需要一个连续的内存依然不够 image.png

2.标记整理(适用于老年代对象存活对象多不适合新生代存活时间短)\color{#00FF00}{2. 标记整理(适用于老年代对象存活对象多不适合新生代存活时间短)}

  • 优点:解决了内存碎片化问题
  • 缺点:效率低 image.png

3.标记复制(适用于新生代对象存活时间短不适合老年代存活对象多)\color{#00FF00}{3. 标记复制(适用于新生代对象存活时间短不适合老年代存活对象多)}

  • 优点:解决了内存碎片化问题、效率高
  • 缺点:占用内存大 image.png

2.概述

  • GC的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

  • GC要点

    • ① 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
    • ② 判断无用对象,使用可达性分析算法三色标记法标记存活对象,回收未标记对象
    • ③ GC具体的实现称为垃圾回收器
    • ④ GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代老年代,不同区域应用不同的回收策略
    • ⑤ 根据GC的规模可以分成Minor GC,Mixed GC,Full GC

3. 分代回收

分代回收

  1. 伊甸园eden,最初对象都分配到这里,与幸存区合称新生代
  2. 幸存区survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成from和to,采用标记复制算法
  3. 老年代od,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
  • 第一次垃圾回收

image.png

  • 又一次垃圾回收

image.png

  • 对象何时放到老年代区

image.png

GC规模

  1. Minor GC 发生在新生代的垃圾回收,暂停时间短
  2. Mixed GC 新生代+老年代部分区域的垃圾回收,G1收集器特有
  3. Full GC 新生代+老年代完整垃圾回收,暂停时间长,应尽力避免

4.三色标记

用三种颜色记录对象的标记状态

  1. 黑色-已标记 (沿着根对象已经找到的对象并且该对象的其他引用都处理完成了)

  2. 灰色-标记中 (沿着根对象已经找到的对象但该对象的其他引用没有处理完成)

  3. 白色-还未标记 (未被处理的对象)

image.png

5.并发漏标

1.问题分析

image.png

2.解决办法- 记录标记过程中变化(两种方法)

  1. Incremental Update(增长更新)
  • 只要赋值发生,被赋值的对象就会被记录
  1. Snapshot At The Beginning, SATB(原始快照)
  • 新加对象会被记录
  • 被删除引用关系的对象也被记录

6.垃圾回收器

Parallel GC

  1. eden内存不足发生Minor GC,标记复制STW
  2. old内存不足发生Full GC,标记整理STW
  3. 注重吞吐量

ConcurrentMarkSweep GC

  1. old并发标记,重新标记时需要STW,并发清除
  2. Failback Full GC
  3. 注重响应时间

G1 GC(jdk9开始为默认的垃圾回收器)

  1. 响应时间与吞吐量兼顾
  2. 划分成多个区域,每个区域都可以充当eden, survivor,old,humongous
  3. 新生代回收:eden内存不足,标记复制STW
  4. 并发标记:old并发标记,重新标记时需要 STW
  5. 混合收集:并发标记完成,开始混合收集,参与复制的 有eden、survivor、old,其中old会根据暂停时间目 标,选择部分回收价值高的区域,复制时STW
  6. Failback Full GC
  1. 第一次垃圾回收

image.png

  1. 第二次垃圾回收

image.png

  1. 触发老年区的垃圾回收

image.png

④:内存溢出

1. 误用线程池导致的内存溢出(情况1)

// 加上虚拟机参数 -Xmx64m (设置虚拟机堆内存大小,更容易内存溢出)
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThhreadPool{
    // 压制警告
    @SuppressWarnings("AlibabaThreadPoolCreation")
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        System.out.println("begin.....");
        while (true) {
            pool.submit(() -> {
                System.out.println("发送短信~");
                // 睡眠30秒
                try {
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

image.png

image.png

内存溢出的原因 1 使用Executors.newFixedThreadPool()

image.png

解决办法

  • 不要使用工具类中的newFixedThreadPool
public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(2);
}
  • 可以自己创建指定队列大小
public static void main(String[] args) {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,
            2,
            0, TimeUnit.SECONDS,
            // 设置队列的上限
            new LinkedBlockingQueue<>(10));
}

内存溢出的原因 2 使用 Executors.newCachedThreadPool()

@Test
public void test1(){
    ExecutorService pool = Executors.newCachedThreadPool();
}

image.png

解决办法

  • 不要使用工具类中的 newCachedThreadPool

  • 可以自己创建指定队列大小

2. 查询数据量太大导致的内存溢出(情况2)

1.导入依赖\color{#00FF00}{1. 导入依赖}

<!-- 计算对象的大小-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.15</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

2.测试代码实现\color{#00FF00}{2. 测试代码实现}

@Test
  public void test1(){
    // Product 对象本身内存
    long productSize = ClassLayout.parseInstance(new Product()).instanceSize();
    System.out.println("productSize = " + productSize);
    // 一个字符串占用内存
    String name = "联想小新Air14经薄本英特尔酷容1514英寸全面屏学生笔记本电脑(i5-1135G71665126MX450独显高色域)银";
    long name2 = ClassLayout.parseInstance(name).instanceSize();
    System.out.println("name字符串本身 = " + name2);

    String desc = "【全金属全面屏】学生商务办公,全新11代处理器,MX450独显,100%sRGB高色域,指纹识别,快充(更多好货)";
    long desc2 = ClassLayout.parseInstance(desc).instanceSize();
    System.out.println("desc字符串本身 = " + desc2);

    // 16 是Byte[] 数组本身大小
    int nameSize = 16 + name.getBytes(StandardCharsets.UTF_8).length;
    System.out.println("name字符串+内容 = " + nameSize);
    int descSize = 16 + desc.getBytes(StandardCharsets.UTF_8).length;
    System.out.println("desc字符串+内容 = " + descSize);
    // 一个对象估算大小
    long size = 16 + productSize + name2 + desc2 + nameSize + descSize;
    System.out.println("一个对象估算大小 = " + size);

    //将对象放到ArrayList中(一个ArrayList对象占用24字节,ArrayList中Object[]数组占16,共40字节)
    long dataSize = (1000000 * size + 40) / 1024 / 1024;
    System.out.println("假设100万个商品对象估算占用  = " + dataSize + "Mb");
}

3.测试结果\color{#00FF00}{3. 测试结果} image.png

3. 动态生成类导致的内存溢出(情况3)

image.png

  • 针对上面的代码解决方法

image.png

⑤:类加载(过程、双亲委派)

1.类加载三个阶段

第一个阶段加载\color{#00FF00}{第一个阶段-加载}

  1. 将类的字节码载入方法区,并创建类.class对象(存储在堆中)
  2. 如果此类的父类没有加载,先加载父类
  3. 加载是懒惰执行

第二个阶段链接\color{#00FF00}{第二个阶段-链接}

  1. 验证-验证类是否符合Class规范,合法性、安全性检查
  2. 准备-为static变量分配空间,设置默认值
  3. 解析-将常量池的符号引用解析为直接引用

第三个阶段初始化\color{#00FF00}{第三个阶段-初始化}

  1. 执行静态代码块与非final静态变量的赋值
  2. 初始化是懒惰执行

2. 验证类加载是懒惰的

1.代码实现\color{#00FF00}{1. 代码实现}

  • 创建Student类
public class Student {
    private static int a = 0x77;
    static {
        System.out.println("Student.class 初始化了~~");
    }
    private static int b = 0x88;
    private static final int c = 0x99;
    int d = 0x55;
    int e = 0x66;
}
  • 创建测试类
public class TestLazy {
    public static void main(String[] args) throws IOException {
        System.err.println("未用到 Student");
        // 只有在控制台输入后代码才能继续向下执行
        System.in.read(); 
        // 关键代码1 会触发类加载
        System.out.println(Student.class);
        System.err.println("已加载 Student");
        System.in.read();
        // 关键代码2 会触发类初始化
        Student student = new Student();
        System.err.println("已初始化 Student");
        System.in.read();
    }
}

2.为了更好的观察使用jdk自带的一个工具\color{#00FF00}{2. 为了更好的观察使用jdk自带的一个工具}

  • 查看工具名称 jhat.exe

image.png

  • 运行工具 jhsdb.exe hsdb

image.png

  • 工具与Java代码关联

image.png

image.png

  • 查看内存中有没有Student

image.png

  • 关闭调试工具,程序继续向下执行触发类的加载

image.png

  • 再次查看内存中有没有Student

image.png

3. 验证类对象位于堆

  • 设置推空间的大小 image.png

  • 查看堆内存的地址

image.png

  • 查看堆内存的大小 universe

image.png

  • 查看堆内存的详细 g1regiondetails

image.png

image.png

4. 静态变量在初始化时赋值

image.png

5. 类对象地址如何找

image.png

image.png

image.png

6. 类初始化方法原理

使用javap 查看字节码文件 javap -c -v -p Student.class

image.png

image.png

7. final修饰基本类型变量的原理

  • 只要是final修改的基本变量其他类想使用的时间根本不用加载类
  • 如果数值比较小直接写死在方法中,如果数值超过short的最大范围会放在常量池中

image.png

8. 将符号引用变成直接引用

public class TestResolution {
    static class A{
        static {
            System.out.println("A 类被加载~~");
        }
    }

    static class B{
        static {
            System.out.println("B 类被加载~~");
        }
    }

    static class C{
        static {
            System.out.println("C 类被加载~~");
        }
    }

    public static void main(String[] args) throws IOException {
        System.in.read();
        A a = new A();
        System.in.read();
        B b = new B();
        System.in.read();
        C c = new C();
        System.in.read();
    }
}
  1. 还未加载类时(是符号应用) image.png

2.继续向下执行代码(加载A类)

image.png

image.png

9. 双亲委派

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  • ① 能找到这个类,由上级加载,加载后该类也对下级加载器可见

  • ② 找不到这个类,则下级类加载器才有资格执行加载

image.png

image.png

当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException

image.png

双亲委派的目的有两点

  • ① 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到jd业k提供的核心类

  • ② 让类的加载有优先次序,保证核类优先加载

10. 能假冒一个System类吗

image.png

⑥:四种引用

1. 概述

1.强引用 image.png

① 普通变量赋值即为强引用,如Aa=newA();

② 通过GC Root的引用链,如果强引用不到该对象,该对象才能被回收

2.软引用(SoftReference) image.png

① 例如:SoftReference a=new SoftReference(newA)i

② 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象

③ 软引用自身需要配合引用队列来释放

④ 典型例子是反射数据

3.弱引用(WeakReference) image.png

① 例如:WeakReference a=new WeakReference(new A());

② 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象

③ 弱引用自身需要配合引用队列来释放

④ 典型例子是ThreadLocalMap中的Entry对象

4.虚引用(PhantomReference) image.png

① 例如:PhantomReference a=new PhantomReference(new A(O):

② 必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚 引用对象入队,由Reference Handler线程释放其关联的外部资源

③ 典型例子是Cleaner释放DirectByteBuffer占用的盔接内存

2. 虚引用

public class TestPhantomReference {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        ArrayList<MyResource> list = new ArrayList<>();
        list.add(new MyResource(new String("a"), queue));
        list.add(new MyResource("b", queue));
        list.add(new MyResource(new String("c"), queue));

        System.gc();

        Object ref;
        while ((ref = queue.poll()) != null) {
            if (ref instanceof MyResource resource) {
                resource.clean();
            }
        }
    }

    static class MyResource extends PhantomReference<String> {
        public MyResource(String reference, ReferenceQueue<String> queue){
            super(reference, queue);
        }
        // 释放外部资源的方法
        public void clean() {
            System.err.println("执行了清除方法~~");
        }
    }
}

image.png

3. 弱引用

黑马教学视频地址: www.bilibili.com/video/BV15b…

4. Cleaner

黑马教学视频地址: www.bilibili.com/video/BV15b…

⑦:finalize

1. finalize概述

  • 一般的回答是:它是Object中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些资源释放和清理工作

  • 较为优秀的回答是:将资源释放和清理放在finalize方法中非常不好,非常影响性能,严重时甚至会引起OOM,从Java9开始就被标注为@Deprecated,不建议被使用了

  • 但是,为什么?很多人都答不上来了,大多回答都是一知半解,没有说到点上

public class TestFinalize {

    static class Dog {
        private String name;

        public Dog(String name) {
            this.name = name;
        }

        @Override
        protected void finalize(){
            System.out.println(this.name + "被干掉了?");
        }
    }

    public static void main(String[] args) throws IOException {
        new Dog("大傻");
        new Dog("二哈");
        new Dog("三笨");
        System.gc();
        System.in.read();
    }

/*  第一,从表面上我们能看出来finalize方法的调用次序并不能保证
    第二,日志中的Finalizer表示输出日志的线程名称,从这我们看出是这个叫做Finalizer的线程调用的finalize方法
    第三,你不能注释掉`System.in.read()`,否则会发现(绝大概率)并不会有任何输出结果了,从这我们看出finalize中的代码并不能保证被执行
    第四,如果将finalize中的代码出现异常,会发现根本没有异常输出
    第五,还有个疑问,垃圾回收时就会立刻调用finalize方法吗?*/
}

image.png

2. finalize_unfinalized链表

黑马教学视频地址:www.bilibili.com/video/BV15b…

3. finalize调用原理

黑马教学视频地址: www.bilibili.com/video/BV15b…

finalize()在什么时候被调用?

有三种情况

  • 所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候.
  • 程序退出时为每个对象调用一次finalize方法。
  • 显式的调用finalize方法

除此以外,正常情况下,当某个对象被系统收集为无用信息的时候,finalize()将被自动调用,但是jvm不保证finalize()一定被调用,也就是说,finalize()的调用是不确定的,这也就是为什么sun不提倡使用finalize()的原因