Java知识点

目录

0、并发语义

1、synchronized关键字和volatile关键字

1.1、synchronized实现原理

1.2、synchronized同步锁膨胀

QA: Synchronized 加到方法和代码块有什么区别?

1.3、volatile关键字实现原理

1.4、Java的内存模型

QA: 为什么volatile在并发下也是线程不安全的?

2、队列同步器(AQS)

2.1、AQS构建同步器

2.2、AQS内部方法

2.3、AQS实现ReentrantLock原理        

2.4、AQS实现的同步器框架

 QA:  ReentransLock与Synchronized比较

3、Java线程池

3.1、线程池核心参数 

3.2、线程池工作原理解析

3.3、常用的线程池

3.4、线程的生命周期

3.5、线程池的关闭

3.6、线程池应用

3.7、Runnable和Callable比较

3.8、 sleep、yield、wait、join的区别

4、HashMap、HashTable、ConcurrentHashMap详解

4.1、HashMap的结构

4.2、HashMap的散列函数 

4.3、put操作

4.4、HashMap、HashTable、ConcurrentHashMap对比

5、双亲委派机制

6、Java垃圾回收

6.1、Java内存结构

6.2、哪些对象需要回收

QA:不可达的对象一定会被回收吗?

QA:GC Root 对象主要指的是哪些对象?

6.3、垃圾回收算法

6.4、 常用的垃圾回收器

 6.5、G1垃圾回收器

6.5.1、G1结构

6.5.2、G1运行原理

7、Java基础

7.1、equals和==的区别


0、并发语义

happens-before语义:例如锁的获取先于锁的释放。

as-if-seiral语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

1、synchronized关键字和volatile关键字

1.1、synchronized实现原理

        每个Java对象都有一个对象头,synchronized用的锁是存在Java对象头的Mark Word字段里的。执行到synchronized时对应JVM的monitorentermonitorexit操作指令。

       java的线程是映射到操作系统线程上的,阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态切到核心态,因此状态转换需要耗费很多的处理器时间。切换时间可能比代码执行时间还要长,所以synchronized是Java语言中的一个重量级的操作。[synchronized总结]

1.2、synchronized同步锁膨胀

  • 无锁:所有线程都可以访问资源。
  • 偏向锁:从始至终都没有竞争,没必要上锁,只需要打个标记就行了。
  • 轻量级锁:synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用CAS就可以解决。这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
  • 重量级锁:重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

QA: Synchronized 加到方法和代码块有什么区别?

        synchronized 是对对象加锁。锁代码块和锁方法其实共享的都是同一个锁,锁住同一个对象。区别在于对代码块加锁时,线程A访问代码块,线程B依然可以访问对象中其余非synchronized 块的部分。然后锁方法的范围更大,性能会差一些。

1.3、volatile关键字实现原理

当一个变量定义为 volatile 之后,将具备两种特性:

  • 保证此变量对所有的线程的可见性,这里的“可见性”,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
  • 有序性-禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。主要有四种内存屏障,StoreStore、StoreLoad、LoadStore、LoadLoad。主要遵循as-if-serial语义,告诉处理器那些地方不允许发生java指令重排序

QA:Volatile关键字修饰的变量怎么感知到要刷新的系统内存的?

 

参考:volatile总结

1.4、Java的内存模型

假设有一个共享变量a在主内存中,线程1和线程2将a的副本copy一份至线程的工作内存中。线程1执行a=1,然后将a的结果刷新回主内存。线程2执行a=a+1时,发现a是volatile变量修饰的共享变量,强制将工作内存中的a置为失效,从主内存中重新读取a的值到工作内存中。

QA: 为什么volatile在并发下也是线程不安全的?

        volatile只能保证内存的可见性,不能保证操作的原子性。即volatile修饰的变量在各个线程的工作内存中不存在一致性的问题。但是java的运算并非原子性的操作,导致volatile在并发下并非是线程安全的。所以一般为了线程安全,需要将volatile与CAS合并使用。

2、队列同步器(AQS)

2.1、AQS构建同步器

        AQS全称是 AbstractQueuedSynchronizer,是一个用来构建「锁」和「同步器」的框架,它维护了一个共享资源 state(volatile关键字修饰) 和一个 FIFO 的等待队列。底层利用了 CAS 机制来保证操作的原子性。如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态封装成一个Node节点加入到同步队列中不停的自旋,同时阻塞该线程。当同步状态释放时,会把首节点唤醒,使其再次尝试获取同步状态。

2.2、AQS内部方法

        AQS内部主要有独占式的tryAcquire()、独占式的tryRelease()、共享式的tryAcquireShared()、共享式的tryReleaseShared()方法。

AQS内部方法

2.3、AQS实现ReentrantLock原理        

以实现独占锁为例(即当前资源只能被一个线程占有),其实现原理如下:

  1. 「state」 初始化 0,在多线程条件下,线程要执行临界区的代码,必须首先获取 state,某个线程获取成功之后,state加 1,其他线程再获取的话由于共享资源已被占用,所以会到 「FIFO」 等待队列去等待,等占有state的线程执行完临界区的代码释放资源(state 减 1)后,会唤醒 FIFO 中的下一个等待线程(head 中的下一个结点)去获取state。
  2. 「state」 由于是多线程共享变量,所以必须定义成 「volatile」,以保证 state的可见性,同时虽然 volatile 能保证可见性,但不能保证原子性,所以 AQS 提供了对state的原子操作方法compareAndSetTail(CAS操作),保证了线程安全
  3. 另外 AQS 中实现的 FIFO 队列(「CLH」 队列)其实是双向链表实现的,由 head、tail 节点表示,head 结点代表当前占用的线程,其他节点由于暂时获取不到锁所以依次排队等待锁释放。 
AQS内部FIFI队列

2.4、AQS实现的同步器框架

AQS实现的同步器

 QA:  ReentransLock与Synchronized比较

3、Java线程池

        线程池的好处:线程的创建和销毁很消耗系统资源,而线程池化技术可以避免重复的创建和销毁线程,节省资源的开销。而且线程池还可以使运行线程数可控,避免系统无脑创建线程,将服务CPU打爆。

3.1、线程池7个核心参数 

  1. corePoolSize:核心线程数。
  2. maxPoolSize:线程池最大线程数
  3. keepAliveTime:保活时间
  4. TimeUnit:时间单位
  5. BlockingQueue:线程池工作队列
    1. ArrayBlockingQueue(基于数组实现有界阻塞队列): 内部维护了一个定长数组,还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置.ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象(默认用的非公平锁,可控),由此也意味着两者无法真正并行运行。

    2. LinkedBlockingQueue(基于链表实现有界阻塞队列): 其能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据。但是插入和删除Node可能会对GC产生影响。

    3. DelayQueue(使用优先级队列实现的延迟无界阻塞队列): 其中的元素只有当其指定的延迟时间到了,才能够从队列中获取。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

    4. SynchronousQueue(不存储元素的阻塞队列,也即单个元素的队列)

  6. ThreadFactory:创建线程的工厂
  7. RejectedExecutionHandler:拒绝策略
    1. 直接抛异常;

    2. 丢弃最近的任务,处理投递的新任务;

    3. 让发起调用的线程处理这个任务;

    4. 直接丢弃新任务;

3.2、线程池工作原理解析

        任务提交到线程池,如果工作线程数小于核心线程数,此时创建新的线程处理任务。如果工作线程数>=核心线程数 && 队列未满,将任务塞到任务队列中。如果队列已满 && 工作线程数 < 最大线程数,创建新的线程处理任务。如果工作线程数=最大线程数了,触发拒绝策略。如果所有任务处理完毕,超过keepAliveTime无新任务投递,将线程缩容至核心线程数大小。

QA:阿里巴巴为什么不建议通过Executors创建线程池? 

  1. FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。建议通过ThreadPoolExecutor创建线程池,自己写入所有参数

3.3、常用的线程池

  • singleThreadPool: 底层的工作队列是ArrayBolckQueue,数组结构
  • fixedThreadPool: 底层的工作队列是LinkedBlockedQueue,链表结构
  • cacheThreadPool: 可缓存的线程池,线程保活为60s,基本上对最大线程数不做限制。底层的阻塞队列是SynchronousQueue。特别在处在于它的内部没有容器。一个生产线程执行put操作时,如果没有消费线程take(), 会一直阻塞。吞吐量贼高、
  • newScheduleThreadPool定时调度线程池。支持定时或者周期性执行的任务。底层队列是DelayedWorkQueue。 线程池从DelayedWorkQueue每次取出(take函数)的任务就是延迟时间最小的任务,  如果到达时间的任务, 则执行任务。否则则用条件锁Conditon的wait进行等待, 执行完后,则用signal进行唤醒下一个任务的执行。

3.4、线程的生命周期

  • NEW - 初始态:即用new关键字新建一个线程,这个线程就处于新建状态
  • RUNNABLE - 运行态:操作系统中的就绪和运行两种状态,在Java中统称为RUNNABLE。
    1. READY - 就绪状态:当线程对象调用了start()方法之后,线程处于就绪状态,就绪意味着该线程可以执行,但具体啥时候执行将取决于JVM里线程调度器的调度。
    2. RUNNING - 运行状态:处于就绪状态的线程获得了CPU之后,真正开始执行run()方法的线程执行体时,意味着该线程就已经处于运行状态。需要注意的是,对于单处理器,一个时刻只能有一个线程处于运行状态。对于抢占式策略的系统来说,系统会给每个线程一小段时间处理各自的任务。时间用完之后,系统负责夺回线程占用的资源。下一段时间里,系统会根据一定规则,再次进行调度。
  • BLOCKED - 阻塞状态:阻塞状态表示线程正等待监视器锁,而陷入的状态。
  • WAITING - 等待状态:进入该状态表示当前线程需要等待其他线程做出一些的特定的动作(通知或中断)。
  • TIMED_WAITING - 超时等待状态:区别于WAITING,它可以在指定的时间自行返回。
  • TERMINATED - 消亡状态:即线程的终止,表示线程已经执行完毕。前面已经说了,已经消亡的线程不能通过start再次唤醒。
线程状态转换图

3.5、线程池的关闭

ShutDownNow方法:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。

    1、将线程池的状态改为Stop状态。

    2、遍历所有工作线程,调用interrupt方法。

    3、将还没有执行的任务放入列表中,返回给调用方。

ShutDown方法:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。

    1、将线程池状态改为Shutdown。

    2、对空闲的线程调用interrupt方法。

        调用shutdownNow或者shutdown方法去尝试关闭 java 线程池,都只是异步的通知线程池,此时线程池不会立即停止。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination方法进行同步等待。

3.6、线程池应用

public void syncConsume(int concurrentNum, Collection collection, Consumer consumer) {

    ExecutorService executor = Executors.newSingleThreadExecutor();

    CountDownLatch countDownLatch = new CountDownLatch(collection.size());

    Semaphore semaphore = new Semaphore(concurrentNum);

    for (T t : collection) {

        try {

            semaphore.acquire();

        } catch (InterruptedException e) {

            e.printStackTrace();

            throw e;

        }

        executor.execute(() -> {

                try {

                    consumer.accept(t);

                }catch (Exception e) {

                    e.printStackTrace();

                } finally {

                    semaphore.release();

                    countDownLatch.countDown();

                }

            });

    }

    try {

        countDownLatch.await();

    } catch (InterruptedException e) {

        e.printStackTrace();

      throw e;throw e;

    }

}

3.7、Runnable和Callable比较

相同点:都是接口,都可以并发编程,都是通过thread.start()触发。

区别

  1. runnable无返回值,callable有返回值。
  2. runnable运行时抛运行时异常,无法捕获异常。Callable 接口 call 方法允许抛出异常,可以获取异常信息。

3.8、 sleep、yield、wait、join的区别

  • Thread.sleep(): Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU资源,但是不会释放锁。无法外部唤醒,只能自己醒来。
  • Thread.yield(): Thread类的方法,类似sleep只是不能由用户指定暂停多长时间让出CPU调度,不释放锁。使得当前线程重新回到可执行状态。
  • Thread.join():一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。无法自己醒来,只能等待别人唤醒。
  • Thread.wait(): Object类的方法(notify()、notifyAll()  也是Object对象),必须放在循环体和同步代码块(synchronized代码块)中,执行该方法的线程会释放锁同时进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁。无法自己醒来,只能等待别人唤醒。如果在wait方法之前调用notify方法,抛出IllegalMonitorStateException异常。
  • LockSupport.park()、LockSupport.parkNanos(): 都是阻塞当前线程的执行,且不会释放当前线程占有的锁资源。可以在任意地方执行。不带时间的park方法无法自己醒来,只能等待别人唤醒。在park()之前执行了unpark()线程不会被阻塞,直接跳过park(),继续执行后续内容。Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。

参考:LockSupport是否会释放锁

        wait用于锁机制,sleep不是,这就是为啥sleep不释放锁,wait释放锁的原因。sleep是线程的方法,跟锁没半毛钱关系,wait,notify,notifyall 都是Object对象的方法,是一起使用的,用于锁机制。

4、HashMap、HashTable、ConcurrentHashMap详解

4.1、HashMap的结构

        HashMap底层是散列表。Java1.7中是(数组 + 链表)。在Java1.8中引入了红黑树。同时它的数组的默认初始容量是 16、扩容因子为 0.75,每次采用 2 倍的扩容。Java1.8中新增了一个树化阈值,默认是64,即使单个索引下标Node个数超过8也不会树化,而是选择扩容(树化代价较大)。

散列表结构

4.2、HashMap的散列函数 

        为了让Key尽可能的散列开,Java1.7中进行了9次扰动(分别是四次位运算和五次异或运算). Java1.8中进行了两次扰动(将key的hashCode高16位 异或 低16位)。这也就是为啥HashMap的容量一直是2的倍数(当n是2的倍数时, &操作等同于%,但是开销更低)

// 计算数组下标,n为HashMap 容量
int tableIndex = hash(key) & (n-1);

static final int hash(Object key) {
        int h;
        // key的hash值高16位与低16位异或
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

4.3、put操作

  1. 判断键值对数组tab是否为空或为null,如果为空则执行resize()进行扩容;
  2. 根据键值key计算hash值得到索引i,如果tab[i]==null,则直接新建节点添加,进入第6步,如果tab[i]不为空,进入第3步;
  3. 判断tab[i]的首个元素的key是否和传入key一样并且hashCode相同,如果相同直接覆盖value,否则转进入第4步;
  4. 判断tab[i] 是否为treeNode(红黑树),如果是红黑树,则直接在树中插入新节点,否则进入第5步;
  5. 遍历tab[i]判断是否遍历至链表尾部,如果到了尾部,则在尾部链入一个新节点,然后判断链表长度是否大于8,如果大于8的话把链表转换为红黑树,否则进入6;遍历过程中若发现key已经存在,直接覆盖value,进入第6步;
  6. 插入成功后,判断size是否超过了阈值(当前容量*负载因子),如果超过,进行扩容。
Hashmap put操作

4.4、HashMap、HashTable、ConcurrentHashMap对比

  • HashMap是线程不安全的:HashMap在Java 1.7中是采用的头插法,这种方式会导致多线程put操作时形成环形链表,get操作陷入死循环。而且在put操作中最后会有一个size++的操作,并发时可能导致不准确,丢失数据。在Java 1.8中改成了尾插法,这种方式避免了环形链表。但是还是可能造成数据丢失。所以HashMap是线程不安全的。
  • HashTable是线程安全的:底层结构与HashMap一样都是散列表结构,差别在于HashTable的put方法通过synchronized方法修饰,每次只能有一个线程可以新增元素,其他的线程会被阻塞在同步方法外。
  • ConcurrentHashMap是线程安全的。在Java 1.7中采用了segment分段锁即为每一个桶都分配一个segment分段锁, 这样在put操作时只需要锁住对应桶的segment即可。在Java 1.8中segment分段锁改成了 cas + synchronized。每次put操作时,先通过cas的方式设置,失败在用synchronized锁住操作。并发效率更高。

5、双亲委派机制

        类加载阶段分为加载、连接、初始化三个阶段,而加载阶段需要通过类的全限定名来获取定义了此类的二进制字节流。Java特意把这一步抽出来用类加载器来实现。在Java中任意一个类都是由这个类本身和加载这个类的类加载器来确定这个类在JVM中的唯一性。

  1、启动类加载器(Bootstrap ClassLoader), 它是属于虚拟机自身的一部分,用C++实现的,主要负责加载<JAVA_HOME>\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。它等于是所有类加载器的爸爸。加载核心类库

        2、扩展类加载器(Extension ClassLoader),它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。加载外部类库

        3、应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那他就是我们程序中的默认加载器。

        双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载

        通过这种方式可以保证每一个类的唯一性

6、Java垃圾回收

参考:Java垃圾回收垃圾回收G1详解

6.1、Java内存结构

        根据JVM规范主要有:

  • 堆:存档对象和数组。从结构上可以分为新生代和老年代。新生代又可以分为Eden区、From Survivor区(S0)、To Survivor区(S1)。所有新生成的对象都是先放在新生代,S0和S1之间通过标记复制筛选幸存对象,超过15次进入老年代。S0和S1之间没有先后关系,大小一致,总有一个是空的。也是垃圾回收的主要区域,如果堆内没有内存完成实例分配,报oom 。
  • 方法区:与堆一样,是各个线程共享的区域,主要存储已经被虚拟机加载的类信息、常量、静态变量信息。很少对这个区域垃圾回收,回收效率也很低。会报oom 。java7中叫永生代,java8移除了永生代,常量信息存在堆内,出现了元空间,不在虚拟机内,使用的是本地内存。永生代和元空间都是对jvm 规范中方法区的实现。java8移除永生代的主要原因是:字符串存在永生代容易内存溢出;类及方法信息比较难确定大小。不好分配永生代空间;永生代回收效率低。在Java8中元空间也不需要垃圾回收
  • 虚拟机栈:每个线程会有一个私有的栈,每个线程调用一个方法会创建一个栈桢。会报oom 和stackOverFlow。
  • 本地方法栈:与虚拟机栈类似,区别在于虚拟机栈执行的是字节码服务,本地方法栈是虚拟机使用Native 方法的。也会报oom和stackOverFlow 。
  • 程序计数器:不存在oom,记录的是当前线程执行到了哪一行字节码。
Java1.7和Java1.8 JVM内存结构差异

        程序计数器、虚拟机栈、本地方法栈3个区域是随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

        堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。GC 关注的也就是这部分的内存区域。

6.2、哪些对象需要回收

  • 引用计数法:简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收。
String ref = new String("Java");

        如上述,ref 对象引用了右侧对象,所以引用次数是 1。如果在上述代码后面添加一个 ref = null,则由于对象没被引用,引用次数置为 0。但是引用计数法无法解决循环引用的问题。

  • 可达性分析:从GC Root 的对象出发,引出他们指向的下一个结点。然后在以此为起点继续引出下一个结点...(这样通过 GC Root 串成的一条线就叫引用链)。与引用链不相连的对象就可以视为垃圾,可以被回收。

        如图a,b就可以回收。

QA:不可达的对象一定会被回收吗?

不是。当对象不可达(可回收)时,GC回收过程中会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法。我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!

注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!

QA:GC Root 对象主要指的是哪些对象?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  • 方法区类静态属性引用的对象
  • 方法区常量引用的对象

6.3、垃圾回收算法

  • 标记清除算法:顾名思义,先标记垃圾然后清除
    • 优点:效率高
    • 缺点:清除的垃圾可能在不同的位置,会产生很多内存碎片

  • 标记复制算法:将一块内存分成大小相等的两份,每次只使用其中一份。GC时将存活的对象复制到另一份未用的内存块,GC时将整个内存块全部回收。
    • 优点:能提供规整的内存
    • 缺点:有一半的内存空间未使用

  • 标记整理算法:标记需要回收的垃圾,然后将存活的对象整理到一起,将内存地址末尾到垃圾全部清除。
    • 优点:能提供规整的内存,且不会像标记复制一样有一半内存未使用
    • 缺点:整理的过程比较耗时

  • 分代收集算法 -【新生代使用复制算法,老年代使用标记整理算法]:根据统计,98%的对象都是朝生夕死。所以将堆空间分为新生代和老年代(新生代:老年代=1:2)。其中新生代分为Eden区,from Survivor区(S0),to Survivor区(S1),其中 Eden:S0:S1 == 8:1:1。将新生代的GC称为Young GC,老年代的GC 称为FUll GC【会清理整个堆中的不可用对象,一般要花较长的时间】。

        新生的对象都在Eden区,一次GC后仍然存活的对象转移到S区。当年龄>15时,转移到老年代。[注:部分超大对象,或者Eden区和S区的空间不够分配下一次GC,也会直接转移到老年代]。

        一次GC的流程图:在Safe Point(一般是循环末尾,方法返回前,抛出异常的位置) 开始GC。然后stop the world(阻塞用户线程) 通过可达性分析标记垃圾,并采用垃圾回收算法对垃圾进行回收。

6.4、 常用的垃圾回收器

        其中Serial和Serial Old 都是单线程垃圾回收器,使用的是分代收集算法。其中ParNew是serial 的多线程版本,Parallel Old是ParNew的多线程版本,可以并发的标记。parallel scavenge是关注于吞吐量的垃圾回收器。CMS(实现最短 STW 时间为目标)使用的是标记清除算法。CMS收集器工作流程图:

 6.5、G1垃圾回收器

        鉴于 CMS 的一些不足之外,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1(Garbage First) 就横空出世了,它对于堆区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。

6.5.1、G1结构

        G1 将连续的Java堆划分为多个大小相等的独立区域(Region)每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。Humongous,简称 H 区,是专用于存放超大对象的区域,通常 >= 1/2 Region Size,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

G1内存划分结构图

        G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾)的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。这也是 "Garbage First" 得名的由来。G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现。

        G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次GC

一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析??

答案是不需要,每个 Region 都有一个 Remembered Set (记忆集),用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。再提一个概念,Collection Set :简称 CSet,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)

G1垃圾回收器一次GC只会回收一个Region对吗?

答案:不对,G1命名由来,G表示"Garbage First",即垃圾优先。1表示这是第一代实验性质的产品。并不是每次只回收一个Region。实际回收Region数量会随着应用程序内存使用情况变化,一次GC可能会回收数十上百个Region都有可能。

6.5.2、G1运行原理

如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:

初始标记(Initial Marking):仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。会阻塞其他的线程

并发标记(Concurrent Marking):使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。

最终标记(Final Marking):使用多条标记线程并发执行。会阻塞其他的线程

筛选回收(Live Data Counting and Evacuation):回收废弃对象,此时也要 阻塞其他的线程,并使用多条筛选回收线程并发执行。(还会更新Region的统计数据,对各个Region的回收价值和成本进行排序)

G1垃圾回收器运行时序图

        G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的。

注:G1 一般来说是没有FGC的概念的。因为它本身不提供FGC的功能。如果 Mixed GC 仍然效果不理想,跟不上新对象分配内存的需求,会使用 Serial Old GC 进行 Full GC强制收集整个 Heap。

7、Java基础

7.1、equals和==的区别

==:是操作符,比较基本数据类型时比较的是值,比较引用类型时比较的是地址

equals:是超类Object的方法,用来检测两个对象是否相等(即对象的内容是否相等)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值