JAVA学习小结

JAVA篇

本篇主要为JAVA(算法和原理)、集合、单例模式、多线程、高并发和新特性相关内容

基础知识

数据类型

  • 基本数据类型
    • 数值型(整数类型byte\short\int\long,浮点类型float\double)
    • 字符型(char)
    • 布尔型(boolean)
  • 引用数据类型
    • 类(class)
    • 接口(interface)
    • 数组(Array)
数据类型长度(字节)
byte1
short2
int4
long8
float4
double8
char2(C语言中为1)
boolean1

面向对象

抽象类和接口
抽象类接口
可以有默认的方法实现java8之前,不存在方法实现
extendsimplements
可以有构造器不可以有构造器
不能被实例化不是类,与实例化无关
public,protected,defaultpublic
单继承多实现
添加新方法,子类不必修改添加新方法,实现接口的类必须也实现
创建对象方式
  • new创建对象
  • 反射机制
  • clone机制(object类方法)
  • 序列化机制

注意:前两种都需要显式地调用构造方法;clone分深拷贝和浅拷贝;序列化可使用Externalizable或Serializable关键字。

深拷贝和浅拷贝
  1. 深拷贝实现方式
  • JSON.parse(JSON.stringify())
  • 手动递归拷贝对象
  • 函数库lodash ==> 方法cloneDeep()
  1. 浅拷贝实现方式
  • Object.assign() ⇒ 当object只有一层时,和深拷贝相同
  • Array.prototype.concat()
  • Array.prototype.slice()

集合

Collection

Collec分支结构

Set

特点:无序,不可重复

类型底层结构特点其他
HashSethash表无序,不可重复重写equals()时需要重写hashcode()
LinkedHashSet链表+hash表有序,不可重复-
TreeSet红黑树有序,不可重复-
List

特点:有序,可重复

类型底层结构特点其他
ArrayList数组有序,可重复线程不安全
LinkedList双向链表有序,可重复-
Vector数组有序,可重复线程安全

其中ArrayList查询访问快,修改慢;LinkedList修改快,查询访问慢,内部维护了链表的长度。

Map

在这里插入图片描述

HashMap

在这里插入图片描述
HashMap的Put机制

参考:https://blog.csdn.net/woshimaxiao1/article/details/83661464
在这里插入图片描述

CurrentHashMap

在这里插入图片描述

参考:https://blog.csdn.net/qq_22343483/article/details/98510619
JDK 1.7
在这里插入图片描述
JDK 1.8
在这里插入图片描述

CAS
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

基础算法

递归实现斐波那契序列

public class Fiber{
	public static int fiber(int location){
		if(location < 1 ) {return 1;}
		if(location == 1 || location == 2) {return 1;}
		else{
			return fiber(location - 1) + fiber(location - 2) ;
		}
	}
}

冒泡排序

public class BubbleSort4Int{
	public static void sort(int[] arr){
		if(arr.length() <= 1 ) {return ;}
		for(int i = 0;i < arr.length() - 1; i++){
			for(int j = 0;j<arr.length() - i - 1; j++){
				if(arr[j] < arr[j+1]){//降序,每次将最小元素放至数组尾部
					int temp = arr[j];
					arr[j] = arr[j+1];
					arr[j+1] = temp;
				}
			}
		}
	}
}

选择排序

public class SelectSort4Int{
	public static void sort(int[] arr){
		int size = arr.length();
		if(size <= 1 ) {return ;}
		int temp = 0;
		for(int i = 0;i < size -1 ; i++){
			int k = i;
			for(int j = i + 1 ;j <= size -1 ; j++){
				if(arr[j] < arr[k]) {k = j;}//升序,每次找到剩余元素中最小元素的下标放在首部
			}
			//arr[i],arr[k] = arr[k],arr[i]
			temp = arr[i];
			arr[i] = arr[k]; 
			arr[k] = temp;	
		}
	}
}

快速排序

public class QuickSort4Int{
	public static void sort(int[] arr,int low,int high){//升序
		if(low >= high){return ;}
		//保存每次进入循环时的low和high初始值
		int low_temp = low;
		int high_temp = high;
		//参考值
		int base = arr[low_temp];
		while(low < high){
			while(low < high && arr[high] >= base){//右哨兵,找到第一个比base小的值
				high--;
			}
			while(low < high && arr[high] <= base){//左哨兵,找到第一个比base大的值
				low++;
			}
			if(low < high){//左哨兵下标<右哨兵下标,左哨兵值>base>右哨兵值,进行交换
				int temp = arr[low];
				arr[low] = arr[high];
				arr[high] = temp;	
			}
		}
		if(low_temp != low){//判断是否反生交换,如果发生将左哨兵和参考值互换,此时左哨兵,参考值,右哨兵有序
			arr[low_temp] = arr[low];
			arr[low] = base;	
		}
		//递归
		sort(arr,low_temp,low-1);
		sort(arr,low+1,high_temp);
	}
}

堆排序

public class HeapSort {
    private static void sort(int[] arr) {
        //创建堆
        for (int i = (arr.length - 1) / 2; i >= 0; i--) {
            //从第一个非叶子结点从下至上,从右至左调整结构
            adjustHeap(arr, i, arr.length);
        }
        
        //调整堆结构+交换堆顶元素与末尾元素
        for (int i = arr.length - 1; i > 0; i--) {
            //将堆顶元素与末尾元素进行交换
            int temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;

            //重新对堆进行调整
            adjustHeap(arr, 0, i);
        }
    }

    /**
     * 调整堆
     * @param arr 待排序列
     * @param parent 父节点
     * @param length 待排序列尾元素索引
     */
    private static void adjustHeap(int[] arr, int parent, int length) {
        //将temp作为父节点
        int temp = arr[parent];
        //左孩子
        int lChild = 2 * parent + 1;

        while (lChild < length) {
            //右孩子
            int rChild = lChild + 1;
            // 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
            if (rChild < length && arr[lChild] < arr[rChild]) {
                lChild++;
            }

            // 如果父结点的值已经大于孩子结点的值,则直接结束
            if (temp >= arr[lChild]) {
                break;
            }

            // 把孩子结点的值赋给父结点
            arr[parent] = arr[lChild];

            //选取孩子结点的左孩子结点,继续向下筛选
            parent = lChild;
            lChild = 2 * lChild + 1;
        }
        arr[parent] = temp;
    }
}

二分查找

public class BinarySearch4Int{//有序数组,target为寻找的元素
	public static int search(int[] arr,int low,int high,int target){
		if(low > high) {return -1;}
		int mid = (low + high) >>> 1;//int mid = (low + high)/2
		if(target = arr[mid]) {
		return mid;
		}else if(target > arr[mid]){
			low = high + 1 ;
			return search(arr,low,high,target);
		}else if(target < arr[mid]){
			high = low - 1 ;
			return search(arr,low,high,target);
		} 
	}
}

单例模式

public class Singleton{
	private Singleton(){}
	private volatile static Singleton singleton;
	private static Singleton getInstance(){
		if(singleton == null){
			synchronized(Singleton.class){
				if(singleton == null){
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

多线程

创建线程

  • 实现java.lang.Runnable接口
  • 继承java.lang.Thread类(抽象类)
    区别:java不支持多继承,继承Threa类意味着子类不能再继承其他类;继承Thread类开销较大;

Callable和Runnable

  • 都可实现多线程
  • 实现Callable接口的线程可以返回执行结果,通常需要和Future/FutureTask结合使用,用于获取异步计算结果
  • 实现Runnable接口的线程不能返回执行结果

Thread类的start()方法和run()方法

  • start()方法最终调用了Native方法start0()来启动线程,线程启动后会自动调用run()方法
  • 直接调用run()方法,和调用其他方法一样,不会在新的线程中执行

线程生命周期

  • 新建(New)
    当新建Thread类的一个实例时,进入新建状态
    Thread t = new Thread();
  • 就绪(Runnable)
    线程已被启动,正在等待被分配cpu时间片(队列中排队)
    t.start();
  • 运行(Running)
    线程获得cpu资源正在运行
    运行run()方法
  • 阻塞(Blocked)
    由于某种原因让出cpu并暂停
    Thread.sleep(100); ⇒ 100ms后重新进入就绪状态
    t.wait(); ⇒ 调用notify()方法进入就绪状态
    t.suspend(); ⇒ 调用resume()方法进入就绪状态
  • 死亡(Dead)
    线程执行完毕或被杀死;
    自然终止:run()方法执行完毕
    异常终止:stop()方法

锁机制

死锁
  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不可剥夺
  • 循环等待条件:多个进程互相等待对方释放资源
Lock

在这里插入图片描述

Synchronized

在这里插入图片描述

Synchronized详解

参考 https://blog.csdn.net/mulinsen77/article/details/88635558

sync和Lock的区别

参考 https://blog.csdn.net/mulinsen77/article/details/84560429

ThreadLocal

线程变量,每个线程都会为ThreadLocal变量创建副本,用法:

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
  • 线程间数据隔离
  • 进行事务操作,用于存储线程事务信息
  • 数据库连接,session会话管理

特点:
(1)每个Thread维护着一个ThreadLocalMap的引用。

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中。

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

ThreadLocal内存泄露问题
在这里插入图片描述

1、Thread中有一个map,就是ThreadLocalMap。

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收。

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的 ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

volatile

在这里插入图片描述
当一个变量定义为 volatile 之后,将具备两种特性:
  1.保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(Java内存模型)来完成。

2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

性能
  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

线程池

几种常见线程池

  • newSingleThreadExecutor 单线程化的线程池
  • newFixedThreadPool 定长线程池,可控最大并发数,超出的线程等待
  • newCacheThreadPool 可缓存线程池,可灵活回收空闲线程,若无可回收线程,新建线程
  • newScheduledThreadPool 定长线程池,支持定时及周期性任务执行

线程池的几种工作队列

  • ArrayBlockingQueue
    基于数组的有界阻塞队列(FIFO)
  • LinkedBlockingQueue
    基于链表的无界阻塞队列(FIFO),吞吐量略高于数组
  • SynchronousQueue
    不存储元素的阻塞队列,每个插入操作必须等待另一个线程调用移除操作,否则插入一直阻塞
  • PriorityBlockingQueue
    具有优先级的无限阻塞队列

参数意义

  • 核心线程数
    默认没线程等任务来了才调用,除非调用了预创建线程(一个或者全部)
  • 最大线程数
  • 空闲时间&单位
    没有执行任务多久会终止,当线程池的线程数大于核心线程数时才会起作用,调用allowCoreThreadTimeOut
  • 缓存队列
    使用上述几种工作队列
  • 工厂方法
  • 拒绝策略
    抛异常、丢弃、重试、丢弃最早提交的
  • 使用Hash表维护线程的引用
  • submit
    使用future获取任务的执行结果

JVM

模型

内存模型

参考 https://www.jianshu.com/p/76959115d486
在这里插入图片描述

程序计数器
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器
我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

注意
如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

Java栈(虚拟机栈)
同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型。

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

对于栈帧的解释参考 Java虚拟机运行时栈帧结构

栈帧: 是用来存储数据和部分过程结果的数据结构。
栈帧的位置: 内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]
栈帧大小确定时间: 编译期确定,不受运行期数据影响。
通常有人将java内存区分为栈和堆,实际上java内存比这复杂,这么区分可能是因为我们最关注,与对象内存分配关系最密切的是这两个。

平时说的栈一般指局部变量表部分。

局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址

详细理解 https://www.zhihu.com/question/29056872

在这里插入图片描述

栈帧
需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常

  • 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  • 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。


对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

参考逃逸分析

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

方法区
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池

是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】

jdk1.7开始逐步去永久代,从String.interns()方法可以看出来。
String.interns()
native方法:作用是如果字符串常量池已经包含一个等于这个String对象的字符串,则返回代表池中的这个字符串的String对象,在jdk1.6及以前常量池分配在永久代中。可通过 -XX:PermSize和-XX:MaxPermSize限制方法区大小。

public class StringIntern {
    //运行如下代码探究运行时常量池的位置
    public static void main(String[] args) throws Throwable {
        //用list保持着引用 防止full gc回收常量池
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}

//如果在jdk1.6环境下运行 同时限制方法区大小 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代
//如果是jdk1.7或1.8环境下运行 同时限制堆的大小  将报heap space 即常量池在堆中

idea设置相关内存大小设置

这边不用全局的方式,设置main方法的vm参数。

做相关设置,比如说这边设定堆大小。(-Xmx5m -Xms5m -XX:-UseGCOverheadLimit)

这边如果不设置UseGCOverheadLimit将报java.lang.OutOfMemoryError: GC overhead limit exceeded,这个错是因为GC占用了多余98%(默认值)的CPU时间却只回收了少于2%(默认值)的堆空间。

目的是为了让应用终止,给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。虽然加大内存可以暂时解决这个问题,但是还是强烈建议去优化代码,后者更加有效,也可通过UseGCOverheadLimit避免[不推荐,这里是因为测试用,并不能解决根本问题]

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

oop-klass

参考 【理解HotSpot虚拟机】对象在jvm中的表示:OOP-Klass模型

概述
HotSpot是基于c++实现,而c++是一门面向对象的语言,本身具备面向对象基本特征,所以Java中的对象表示,最简单的做法是为每个Java类生成一个c++类与之对应。

但HotSpot JVM并没有这么做,而是设计了一个OOP-Klass Model。这里的 OOP 指的是 Ordinary Object Pointer (普通对象指针),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象。而 Klass 则包含元数据和方法信息,用来描述Java类。

之所以采用这个模型是因为HotSopt JVM的设计者不想让每个对象中都含有一个vtable(虚函数表),所以就把对象模型拆成klass和oop,其中oop中不含有任何虚函数,而Klass就含有虚函数表,可以进行method dispatch。

klass
Klass简单的说是Java类在HotSpot中的c++对等体,用来描述Java类。

Klass主要有两个功能:

  • 实现语言层面的Java类
  • 实现Java对象的分发功能

Klass是什么时候创建的呢?一般jvm在加载class文件时,会在方法区创建instanceKlass,表示其元数据,包括常量池、字段、方法等。

oop
Klass是在class文件在加载过程中创建的,OOP则是在Java程序运行过程中new对象时创建的。

一个OOP对象包含以下几个部分:

  • 对象头 (header)
    Mark Word,主要存储对象运行时记录信息,如hashcode, GC分代年龄,锁状态标志,线程ID,时间戳等
  • 元数据指针:即指向方法区的instanceKlass实例
  • 实例数据:
    存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
  • 对齐填充:仅仅起到占位符的作用,并非必须。

实例说明

class Model
{
    public static int a = 1;
    public int b;

    public Model(int b) {
        this.b = b;
    }
}

public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}

上述代码得OOP-Klass模型入下所示
在这里插入图片描述

类加载机制

加载 ⇒ 验证 ⇒ 准备 ⇒ 解析 ⇒ 初始化 ⇒ 使用 ⇒ 卸载
待补充

双亲委派原则

待补充

垃圾回收(GC)

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。JVM中垃圾回收最基本的做法是分代回收。

参考 深入理解 JVM 垃圾回收机制及其实现原理
参考 垃圾回收算法详解

判断算法

引用计数法
在这种算法中,假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1,例如将对象 b 赋值给对象 a,那么 b 被引用,则将 b 引用对象的计数器累加 1。

反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。

特别地,当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。

优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。

可达性分析法
可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:

  • 对象是属于根集中的对象
  • 对象被一个可达的对象引用

在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中 JNI(Native 方法)的引用对象
  • 活跃线程(已启动且未停止的 Java 线程)

根集中的对象称之为GC Roots,也就是根对象。
可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。

在这里插入图片描述

如上图所示,形象的展示了可达对象与不可达对象的示例,其中灰色的对象都是不可达对象,表示可以被垃圾收集的对象。在可达性分析法中,对象有两种状态,那么是可达的、要么是不可达的,在判断一个对象的可达性的时候,就需要对对象进行标记。关于标记阶段,有几个关键点是值得我们注意的,分别是:

开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便 JVM 可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次 Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。

安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。对于安全点,另一个需要考虑的问题就是如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都“跑”到最近的安全点上再停顿下来。

两种解决方案
抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应 GC 事件。

主动式中断(Voluntary Suspension):主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志地地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。

在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:

如果对象在进行根搜索后发现没有与根对象相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于 OC 中的dealloc,Swift 中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。

如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后 GC 将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
GC 判断对象是否可达看的是强引用。

当标记阶段完成后,GC 开始进入下一阶段,删除不可达对象。当然,可达性分析法有优点也有缺点,
优点:可以解决循环引用的问题,不需要占用额外的空间
缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用

在上面的介绍中,我们多次提到了“引用”这个概念,在此我们不妨多了解一些引用的知识,在 Java 中有四种引用类型,分别为:

强引用(Strong Reference):如Object obj = new Object(),这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2 之后提供了SoftReference类来实现软引用。

弱引用(Weak Reference):它也是用来描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了WeakReference类来实现弱引用。

虚引用(Phantom Reference):也称为幻引用,最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2 之后提供了PhantomReference类来实现虚引用。

回收算法

标记-清除算法
标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。

优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:标记和清除过程的效率都不高,这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。
下图为“标记-清除”算法的示意图:
在这里插入图片描述

下图为使用“标记-清除”算法回收前后的状态:
在这里插入图片描述

标记-整理算法
标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
下图为“标记-整理”算法的示意图:
在这里插入图片描述

下图为使用“标记-整理”算法回收前后的状态:
在这里插入图片描述

复制算法
复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
下图为复制算法的示意图:
在这里插入图片描述
下图为使用复制算法回收前后的状态:
在这里插入图片描述

分代回收
  • 新生代
    新生成的对象
    Minor GC
    一般采用复制算法,会将本次回收时依旧存活的对象移动(复制)到同一个Survivor中,然后对Eden和另一个Survivor进行回收
  • 老年代
    新生成的大对象,N次回收后存活的对象
    Major GC
    一般采用标记-清除或者标记-整理算法回收
  • 永久代
    方法区,根据JVM不同可回收也可不回收,JAVA8已废弃。
垃圾回收器

Serial 收集器
ParNew 收集器
Parallel Scavenge 收集器
Serial Old 收集器
Parallel Old 收集器
CMS收集器
G1 收集器

调优

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值