JAVA 知识体系(第四版)
2021-09-10 12:42:38 2 举报
AI智能生成
JavaSE,面试题,虚拟机
作者其他创作
大纲/内容
字符串
String
要素
1、引用类型
2、final 修饰,不可继承
主要属性
value
用来存储字符串中的字符。
hash
用来缓存计算之后的 hascode。这样就不用每次都重新计算哈希值了。
创建字符串的两种方式
通过 new String 创建
通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然字符串内容
相同,但是地址值不同
相同,但是地址值不同
字面量直接赋值
以“”方式给出的字符串,此时字符串值声明在字符串常量池中,只要字符序列相同(顺序和大小写),无论在程序代码中出现几
次,JVM 都只会建立一个 String 对象,并在字符串池中维护,且字符串常量池不会存储相同的字符串
次,JVM 都只会建立一个 String 对象,并在字符串池中维护,且字符串常量池不会存储相同的字符串
二者之间的内存图
特点
String类的字符串不可变,它们的值在创建后不能被更改
String s1 = "abc";
s1 += "d";
System.out.println(s1); // "abcd"
// 内存中有"abc","abcd","d",3个对象,s1从指向"abc",改变指向,指向了"abcd"。
s1 += "d";
System.out.println(s1); // "abcd"
// 内存中有"abc","abcd","d",3个对象,s1从指向"abc",改变指向,指向了"abcd"。
String 的值是不可变的,但是它们可以被共享
String s1 = "abc";
String s2 = "abc";
// 内存中只有一个"abc"对象被创建,同时被s1和s2共享。
String s2 = "abc";
// 内存中只有一个"abc"对象被创建,同时被s1和s2共享。
字符串效果上相当于字符数组( char[] )
String str = "abc";
相当于:
char[] data = {'a', 'b', 'c'};
String str = new String(data);
// String底层是靠字符数组实现的,jdk9底层是字节数组。
byte[] bys = {97,98,99};
String str = new String(bys);
相当于:
char[] data = {'a', 'b', 'c'};
String str = new String(data);
// String底层是靠字符数组实现的,jdk9底层是字节数组。
byte[] bys = {97,98,99};
String str = new String(bys);
String 不可变如何理解
在 java 中,String 是对 char 数组的封装,String 类中一个重要属性 value 就是一个字符数组,被 private final 所修饰,且 String 内部没有提供对 value 这个char 数组进行修改的任何接口,外部无法访问且无法直接进行修改,所以当 String 初始化后值就已经被固定,因此,可以认为 String 是不可变得。但是,String 是可以通过反射进行修改
表现
当字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
当对现有字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
当使用 String 类提供的修改方法时,都是重新 new 了一个String 对象,并未对原有的内存区域进行修改
AbstractStringBuilder
StringBuilder
工作流程
1、new一个 StringBuilder,传入一个 string 类型参数,调用有参构造方法
2、在构造方法中,调用 appennd 方法
3、判断传入的 string 是否为null,然后获取 string 长度
4、做扩容判断,是否扩容,如果需要扩容,则扩容至原来数组长度的2倍再加 2
5、调用 String 的 getChars 方法,将传入参数 String 的 value 数组 拷贝到原有的 StringBuilder 的 value 数组中
StringBuffer
工作流程
与 StringBuilder 一致,自己的 append 方法里都是显示调用父类 AbstractBuilder 的 append 方法,super.append(),只不过在调用 StringBuffer 的 append 方法时,StringBuffer 会在自己的 append 方法加上 synchronized 做同步
主要属性
value
用来存储字符序列中的字符。value是一个动态的数组,当存储容量不足时,会对它进行扩容。
count
表示value数组中已存储的字符数。
为什么 StringBuilder 和 StringBuffer 可变
StringBuilder 和 StringBuffer 都继承自 AbstractStringBuilder,AbstractStringBuilder 与 String 一样,都包含了一个value 属性(char 数组),不同的是 AbstractStringBuilder 提供了对 char 数组的修改方法,
为什么大量频繁的做字符串拼接 StringBuilder 比 String 更快?
因为 String 每拼接一次就会创建一个 String 对象,而 StringBuilder 做拼接只是不停地调用 append 方法做一个数组的拷贝 (System.arraycopy),最后调用一个 toString 方法,实际只创建了两个对象,一个StringBuilder,一个 String 结果,效率会比反复创建 String 对象更快
String 做字符串拼接后,原始字符串是否改变?
不会,因为 String 本身是不可继承的类,且存储字符的 value 加了 final 是不可变的属性,String 的拼接实际是新创建一个 String 对象,将原始字符串对象中的 value char数组中的值 copy 到 新的字符串对象的 value 字符数组中,再把指针指向新的对象,原始字符串对象并未改变
面向对象
面向对象和面向过程编程
面向过程
强调的是过程,开发者必须清楚每一个步骤,按步骤实现
面向对象
通过调用对象的属性和行为来实现目的,开发者不必清楚每一个步骤的实现细节也可以完成
类
概述
类是用来描述一类具有共同属性和行为事物的统称。是抽象的,只是用来描述具有共性的数据信息。
组成
属性
描述数据具有的共性
行为
描述类具有的行为,表现形式即方法,方法的调用即执行类的行为
对象
概述
对象是具体存在的,具备该类事物的属性和行为
调用方法
根据对象的引用找到堆中对象存储地址,再找到地址中方法的地址,方法是存储在元空间,根据找到的方法地址在元空间中找到对应的方法,将其代码加载到虚拟机栈,成为一个栈帧,方法执行完成后,栈帧弹出,回收方法中的局部变量
对象内存图
单个对象内存图
多个对象内存图
多个变量指向同一个对象
局部变量和成员变量区别
类中位置不同
成员变量(类中方法外)局部变量(方法内部或方法声明的参数上)
内存中位置不同
成员变量在虚拟机的堆内存中,而局部变量存储在虚拟机栈中的局部变量表里
生命周期不同
成员变量是随着对象的存在而存在,随着对象的消失而消失,局部变量是随着方法的调用而存在,随着方法的调用完毕而消失
初始化值不同
成员变量有默认初始化值,局部变量没有默认初始化值,必须先定义,赋值才能使用,如果只定义了但没有赋值,直接使用就会报错
线程安全性
成员变量如果没有加 synchronized 或 volatile 等线程安全相关的关键字,则线程不安全;局部变量存储在虚拟机栈中栈帧的的局部变量表里,而每个线程都存在自己的调用栈,彼此之间不共享,所以不存在资源争夺,天然线程安全
封装
将类的信息隐藏在内部,外界无法直接修改或访问,只能通过创建对象,使用对象的方法进行读写操作,隐藏实现细节,对外提供访问接口,提高了安全性和复用性
继承
概述
在java中指的是“一个类”可以“继承自”“另一个类”。 "被继承的类"叫做: 父类/超类/基类,"继承其
他类的类"叫做:子类。继承后,“子类”中就“拥有”了“父类”中所有的成员(成员变量、成员方法)。 “子类就
不需要再定义了”。
他类的类"叫做:子类。继承后,“子类”中就“拥有”了“父类”中所有的成员(成员变量、成员方法)。 “子类就
不需要再定义了”。
继承规则
子类可以继承父类所有成员,包括 static 关键字修饰的静态成员
好处
提高代码的复用性(减少代码冗余,相同代码重复利用)。
使类与类之间产生了关系。
继承内存关系图
多态
概述
在继承和派生关系中,父类引用指向子类对象即多态
重载
方法名相同
方法的形参列表不同
具体的不同表现为:类型、个数、顺序的不同才可以构成重载
与方法的返回值类型与访问权限无关
重写
两同两小一大原则
相同
方法名相同
参数类型相同
两小
子类方法返回类型小于等于父类方法返回类型(子类方法返回值必须与父类方法返回值相同或者是父类方法返回值的子类)
子类重写方法抛出异常小于等于父类方法抛出异常(这里指的是子类方法抛出的异常是父类异常的子类,并非个数,子类抛出的异常可以大于父类异常个数,只要抛出的异常都是父类异常的子类,或者是 RuntimeExcetion 的子类)
一大
子类访问权限大于等于父类方法访问权限。
访问成员特点
成员变量
编译和运行都看父类
成员方法
编译看父类,运行看子类
静态方法
编译和运行都看父类
静态变量
编译和运行都看父类
多态内存图
多态应用场景
变量多态
如果变量的类型为父类类型,该变量就可以接收该父类类型的对象或者其所有子
类对象
类对象
形参多态
如果参数类型为父类类型,该参数就可以接收该父类类型的对象或者其所有子类对象
返回值多态
如果返回值类型为父类类型,那么就可以返回该父类类型的对象或者其所有子类对象
多态的优缺点
优点
提高代码的可扩展性
缺点
多态的情况下,只能调用父类的共性内容,不能调用子类的特有内容。
this
this可以访问本类的成员变量
this.成员变量 (一般用来区分同名的成员变量和局部变量)
this可以访问本类的成员访问
this.成员方法名(实参)
this可以访问本类的构造方法
无参构造
this()
有参构造
this(实参)
PS
只能在本类的构造方法中使用 this 调用其他构造方法
在本类的构造方法中使用 this 调用其他构造方法,必须放在该构造方法的第一行,否则会报错
两个构造方法不能使用 this 同时相互调用
super
super可以访问父类的成员变量
super.成员变量(一般用来区分父子类中同名的成员变量)
super可以访问父类的成员方法
super.成员方法(实参)(一般用来在子类中访问父类的成员方法)
super可以访问父类的构造方法
无参构造
super()
有参构造
super(实参)
PS
如果没有显示调用父类的有参构造,那么子类的构造方法默认会在构造方法第一行调用父类的空参构造方法
super访问父类的构造方法,可以用来初始化从父类继承过来的属性
在子类的构造方法中,使用 super 调用父类的构造方法,必须放在子类构造方法的第一行
父类私有成员,子类无法通过 super 关键字直接调用
static 关键字
用法
修饰成员变量、成员方法、内部类,不可以修饰局部变量(静态成员属于类,不属于方法),Class对象是存放在堆区的,不是方法区,类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。而static 变量是存在 Class 对象中,所以 static存储在虚拟机的堆中(非元空间)
特点
1、静态成员被所在类的对象共享
2、静态成员可通过类名直接调用
3、静态成员随着类加载而加载
4、静态成员优先于对象存在
static 方法
访问特点
1、静态方法只能调用静态成员
static 方法属于类方法,随着类的加载而加载,所以静态方法可以直接通过类名来调用,静态方法也可以访问其他静态方法,而非静态成员属于对象,类会优先于对象存在,静态成员随着类加载而加载,所以当静态方法加载到内存时,非静态成员还未被加载到内存中,所以静态方法无法访问非静态成员
2、非静态方法可以调用任意成员
非静态方法调用时·,对象已经存在,而静态成员也早于对象(一般情况)被加载,所以非静态方法可以访问调用任意成员
继承问题
静态成员可以被继承,无论是 子类.属性 或者 子类对象.属性 都可以访问到父类的 static成员。不能被重写。注意:接口的静态方法实现类即使是 子类.属性 也无法继承调用。如果父类中有一个静态方法,子类也有一个同名且参数列表相同的静态方法,那么该子类方法会将继承过来的父类的 static 方法隐藏,并非重写。简单来讲,父类的静态方法和子类的同名同参静态方法是没有关系的,如果子类存在同名同参静态方法,父类静态方法被隐藏,如果子类不存在,则可以直接通过 子类类名.父类静态方法名 调用父类的静态方法
Java类继承中子类能获得父类的静态方法,但为啥接口继承中子接口为何不能获得父接口中的静态方法?
子主题
抽象方法 与 static
abstract 与 static 不能同时存在,抽象方法是需要子类去重写完善的,而 static 修饰的方法不能被重写
非静态内部类与静态内部类
静态内部类
可以定义静态和非静态成员变量
可以定义静态和非静态方法
外部类可以访问内部的静态和非静态成员,但内部类只能直接访问外部类的静态成员,不能直接访问非静态成员
非静态内部类
只能定义非静态成员
只能定义非静态方法
可以访问内部的非静态成员(静态成员无法定义压根就没有),外部的静态与非静态成员
static 应用场景
工具类
单例模式,静态方法 getInstance
抽象类
概述
使用abstract关键字修饰的类就是抽象类,这种类有构造方法但不能创建对象,它就是用来做父类被子类继承的
定义
抽象类中的成员
成员变量与成员方法
静态成员变量与静态成员方法
构造方法
抽象方法
概述
没有方法体,使用abstract修饰的方法就是抽象方法,但 abstract 不能与 static 共存
作用
强制要求子类实现,如果子类不实现抽象方法就必须加上 abstract 关键字,把子类也变成抽象类
要点
1、抽象类不能被创建对象,就是用来做“父类”,被子类继承的。
2、抽象类不能被创建对象,但可以有“构造方法”——为成员变量初始化。
3、抽象类中可以没有抽象方法,但抽象方法必须定义在抽象类中
4、子类继承抽象类后,必须重写抽象类中所有的抽象方法,否则子类必须也是一个抽象类
5、抽象类中不能有静态的抽象方法。定义抽象方法的目的是重写此方法,但如果定义成静态方法就不能被重写。
6、jdk8 前,抽象类中的抽象方法默认 protected,jdk8 默认 default
接口
概述
接口是Java语言中的一种引用类型,是方法的"集合",所以接口的内部主要就是定义方法。它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。接口不能创建对象,需要使用实现类实现接口(类似于继承),实现接口的类叫做实现类(子类)
接口中的成员
静态常量( jdk7 及以前)
抽象方法(jdk7 及以前)
默认方法,使用public default 关键字修饰(public可省略,default 关键字不能省略,默认方法与默认访问权限不是一个概念,它的访问权限只能是 public)(jdk8 新增)
静态方法(jdk8 新增)
私有方法(jdk9 新增)
接口中成员访问特点
静态常量
主要是供接口直接使用
抽象方法
供实现类重写的
默认方法
供实现类继承的(实现类中可以直接调用,实现类对象也可以直接调用或者重写),必须在接口里实现
静态方法
只供接口直接调用,实现类继承不了,与 abstract 关键字冲突,所以必须在接口里就实现
私有方法
只能在接口中直接调用,实现类继承不了
接口与超类的二义性
实现多个接口的冲突情况(一个类实现多个接口)
公有静态常量的冲突
如果有冲突,则实现类不继承冲突的静态常量,当使用子类访问冲突的静态常量时,编译直接报错
公有抽象方法的冲突
实现类只需要重写一个
公有默认方法的冲突
实现类必须重写一次最终版本
公有静态方法的冲突
接口中的静态方法不能被继承,所以不存在冲突
私有方法的冲突
私有方法只能在本接口中直接使用,不存在冲突
接口多继承时的冲突情况(一个接口继承多个父接口)
公有静态常量的冲突
子接口无法继承父接口中冲突的常量,当使用子接口访问冲突的静态常量时,编译报错
公有抽象方法的冲突
子接口只会继承一个有冲突的抽象方法,子接口的实现类会自己实现
公有默认方法的冲突
子接口中必须重写一次有冲突的默认方法(注意要加default)
公有静态方法和私有方法的冲突
不冲突,因为静态方法是直接属于接口的,只能使用本接口直接访问,而私有方法只能在接口中访问,也没有冲突
实现类继承父类又实现接口时的冲突
公有静态常量的冲突
子类无法继承有冲突的常量,当使用子类访问冲突的静态常量时,编译报错
公有抽象方法的冲突
子类必须重写一次有冲突的抽象方法
公有默认方法的冲突
优先访问父类的
公有静态方法的冲突
只会访问父类的静态方法
私有方法的冲突
不存在冲突
总结
1、静态常量冲突,无论接口或者是类,都无法继承,编译报错
2、公共抽象方法冲突,子类或子接口重写一个即可
3、公有默认方法冲突,如果是接口与接口冲突,则子类或子接口必须重写,如果是超类与接口冲突,类优先
4、公有静态方法冲突,这句话其实不严谨,因为接口的静态方法是无法继承的,所以接口与接口之间不存在冲突,接口与超类,只会继承超类的静态方法
5、私有方法不存在冲突
内部类
概念
内部类是指在一个外部类的内部再定义一个类。
为何使用内部类
1、增强封装,把内部类隐藏在外部类中,不允许其他类来访问内部类
2、内部类能提高代码的可读性和可维护性
分类
成员内部类
定义
即没有使用static修饰的内部类.这说明,成员内部类属于外部类的实例对象,不属于外部类本身(类比字段).
访问方式
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
特点
1、成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括 private 和 静态成员)
2、成员内部类要访问外部类的同名成员,需要以下面的形式进行访问
外部类名.this.成员变量
外部类名.this.成员变量
3、外部类要访问成员内部类的成员,必须先创建一个成员内部类的对象,在通过指向这个对象的引用来访问
4、成员内部类可以拥有四大访问权限
5、成员内部类可以定义常量,但不能定义静态成员
静态内部类
定义
使用static修饰的内部类.所以,该内部类属于外部类本身,而不属于外部类的实例对象
访问方式
OuterClass.InnerStaticClass innerStaticClass = new OuterClass.InnerStaticClass();
import com.qmy.innerClass.OuterClass.InnerStaticClass;
InnerStaticClass innerStaticClass = new InnerStaticClass();
InnerStaticClass innerStaticClass = new InnerStaticClass();
特点
1、在创建内部类的实例时,不必创建外部类的实例,不依赖于外部类实例存在
2、可以定义静态和非静态成员,但只能访问外部类的静态成员,不能直接访问外部类非静态成员,除非持有外部类实例引用
匿名内部类
定义
匿名内部类是一个没有名称的局部内部类,适合于只使用一次的类,匿名内部类必须继承一个父类或者实现一个接口,但最多只能继承一个父类或实现一个接口.
访问方式
Runnable thread = new Runnable() {
@Override
public void run() {
}
};
@Override
public void run() {
}
};
特点
1、匿名内部类是不能有访问修饰符和static修饰符的;
2、匿名内部类是唯一一种没有构造器的类;
3、匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
局部内部类
定义
在方法中定义的内部类,其可见范围是当前方法,和局部变量是同一个级别,所以局部内部类只能在方法中使用.
特点
1、局部内部类和方法里面的局部变量一样,是不能有public、protected、private以及static修饰符的。
2、局部内部类是定义在一个方法或者一个作用域里面的类,局部内部类的访问仅限于方法内或者该作用域内;
3、局部内部类访问的局部变量必须使用 final 修饰,在 Java8 中是自动隐式加上 final (语法糖),如果局部内部类访问到的局部变量自动加上 final 编变成常量
4、局部内部类和成员内部类一样,不能包含静态成员
局部内部类访问的局部变量自动变常量原因分析
当方法被调用运行完毕之后,当前方法的栈帧被销毁,方法内部的局部变量的空间全部销毁。但内部类对象可能还在堆内存中,要直到没有被引用时才会消亡。此时就会出现一种情况:内部类要访问一个不存在的局部变量.为了避免该问题,我们使用final修饰局部变量,从而变成常量;永驻内存空间,即使方法销毁之后,该局部变量也在内存中,对象可以继续持有。
异常机制
1、异常的体系结构和分类
分类
编译时异常
编译期间,编译器检测到某段代码可能会发生异常,需要开发者提前给代码作出错误解决方案,否则编译不通过
运行时异常
编译通过,运行期间出现的异常
体系结构
Throwable
Error
严重性错误
Exception
RunTimeException
运行时异常
非 RuntimeException
都是编译时异常,需抛出或捕获
2、异常产生的原理
java对异常的默认处理方式是抛出异常给上一级,抛储之前,java 会根据错误产生的异常类,创建出该类对象,底层通过 throw 关键字将异常抛出,不断向上抛出,直到抛给 JVM 虚拟机,虚拟机拿到问题后,将错误原因和所在的位置打印在控制台
3、异常的处理方式
1、可以自己处理 try-catch
2、不能自己处理 throws 声明异常并往上抛出(如他人传参或者调用自己提供的接口出现的错误,自己无法处理则抛出错误交给调用者)
throws 是 java 对异常默认处理方式
4、throw 和 throws 区别
throw
将异常抛给调用者
throws
仅对方法进行声明,告知调用者此方法存在异常,如果是 RuntimeExcepton,则不需要 throws 抛出
数据结构
线性表
数组
概述
数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
数组的逻辑解构和物理结构
数组的逻辑结构指的是我们可以用什么的表示方式来描述数组元素,数组的物理结构指的是数组元素实际的存储形式
数组的寻址公式
array[k]_address = base_address + k * type_size
从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。即数组下标 index 就是 k,type_size即数组存储类型的大小。偏移量就是 k*type_size。base_address就是数组的首地址,如果下标 index 为0,即k = 0,array[0] 就是偏移为 0 的位置,也就是首地址,array[k] 就表示偏移 k 个 type_size 的位置,这也是为什么数组必须存储同一类型的原因,如果存储类型不同,会导致偏移量计算有误,寻址公式失效
数组下标为何从 0 开始
如果下标从 1 开始,那么计算 array[k]的内存地址会变成:
array[k]_address = base_address + (k-1)*type_size
array[k]_address = base_address + (k-1)*type_size
对比两个公式,不难发现从数组下标从 1 开始如果根据下标去访问数组元素,对
于 CPU 来说,就多了一次减法指令。
当然另一方面也是由于历史原因,c 语言设计者使用 0 开始作为数组的下标,后来
的高级语言沿用了这一设计。
于 CPU 来说,就多了一次减法指令。
当然另一方面也是由于历史原因,c 语言设计者使用 0 开始作为数组的下标,后来
的高级语言沿用了这一设计。
数组的访问特点
高效的随机访问
数组元素的访问是通过下标来访问的,计算机通过数组的首地址和寻址公式能够很快速的找到想要访问的元素
低效插入和删除
数组是一段连续的内存空间,因此为了保证数组的连续性会使得数组的插入和删除的效率变的很低
链表
概述
链表(Linked list)是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
存储结构
数组
需要一块连续的存储空间,对内存的要求比较高,比如我们要申请一个1000M 的数组,如果内存中没有连续的足够大的存储空间则会申请失败,即便内存的剩余可用空间大于 1000M,仍然会申请失败。
链表
与数组相反,它并不需要一块连续的内存空间,它通过指针将一组零散的内存块串联起来使用,所以如果我们申请一个 1000M 大小的链表,只要内存剩余的可用空间大于 1000M,便不会出现问题。
链表访问特点
执行插入、删除操作效率高,访问任意节点效率低
类型
单链表
概述
链表的最基本的结构,链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。
图示
单链表图示
循环链表
概述
循环链表是一种特殊的单链表。它跟单链表唯一的区别就在尾结点。单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。
图示
循环链表图示
双向循环链表
概述
单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向循环链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。如果存储同样多的数据,双向循环链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性
图示
双向链表图示
栈
概述
栈就是一种操作受限的线性表,只允许在栈的一端进行数据的插入和删除,这两种操作分别叫做入栈和出栈。
栈的实现
1、基于数组的顺序栈的实现
2、支持动态扩容的顺序栈
3、基于链表的链式栈的实现
队列
概述
队列和栈一样都属于一种操作受限的线性表,先进先出
常见队列及实现
顺序队列
链式队列
队列在 java 中的应用 ----- Queue
散列表
概念
散列表又名哈希表,是根据键(Key)直接访问在内存存储位置的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性;
散列函数
是根据键(Key)直接访问在内存存储位置的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性;
散列冲突解决方案
开放寻址法(ThreadLocal)
1、线性探测
如果经过散列函数散列之后,存储位置已经被占用,那么从当前位置开始继续往后查找,直到找到为止
hash(key) +0,hash(key) +1,hash(key) +2,.....
2、二次检测
所谓的二次检测跟线性检测的原理一样,只不过线性检测每次检测的步长是 1,每
次检测的下标依次是: hash(key)+0,hash(key)+1,hash(key)+2,..........,
所谓的二次检测指的是每次检测的步长变为原来的二次方
次检测的下标依次是: hash(key)+0,hash(key)+1,hash(key)+2,..........,
所谓的二次检测指的是每次检测的步长变为原来的二次方
hash(key) +0^2,hash(key) +1^2,hash(key) +2^2,.....
3、双重散列
双重散列,意思就是不仅要使用一个散列函数。我们使用一组散列函数,不同散列函数的散列算法不同,
我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到
找到空闲的存储位置。
我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到
找到空闲的存储位置。
hash1(key),hash2(key),hash3(key)……
链表法(HashMap)
链表法对内存的利⽤率⽐开放寻址法要⾼。因为链表结点可以在需要的时候再创建,并不需要像开放寻址法那样事先申请好。链表法⽐起开放寻址法,对⼤装载因⼦的容忍度更⾼。基于链表的散列冲突处理⽅法⽐较适合存储⼤对象、⼤数据量的散列表,⽽且,⽐起开放寻址法,它更加灵活,⽀持更多的优化策略,⽐如⽤红⿊树代替链表。
树
特点
1、没有父节点的节点称为根节点
2、每一个非根节点有且只有一个父节点
3、每一个非根节点有且只有一个父节点
4、树里面没有环路(cycle)
核心概念
高度
节点到叶子节点的最长路径(边数),所有叶子节点的高度为 0
深度
根节点到这个节点所经历的边的个数,根的深度为 0
层
节点的深度+1
图示
树核心概念释义
二叉树
定义
每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点,
存储结构
链式存储
定义
是基于指针或者引用的二叉链式存储法,存储空间是离散的非连续的,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要找到根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的
图示
二叉链式存储
顺序存储
定义
基于数组的顺序存储法,需要开辟一段连续的内存空间,顺序存储只适用于完全二叉树,非完全二叉树使用顺序存储会有大量空间资源浪费
图示
完全二叉树
分类
满二叉树
完全二叉树
二叉查找树
介绍
二叉查找树又名二叉搜索树,有序二叉树或者排序二叉树,是二叉树中比较常用的
一种类型
一种类型
特点
1、若任意节点的左子树不空,则左子树上所有节点的值均小于它们的根节点的值
2、若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
3、任意节点的左、右子树也分别为二叉查找树
4、没有键值相等的节点
图示
二叉查找树
平衡二叉查找树
介绍
平衡二叉查找树又简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在
1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。
1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。
特点
1、可以是空树
2、假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之
差的绝对值不超过1
差的绝对值不超过1
堆
定义
堆其实是一棵树,并且是一种特殊的树,它满足以下特点,堆是一个完全二叉树,堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值
JAVA集合
Collction 接口
List 接口
概述
存储有序、可重复的数据
主要实现类
ArrayList
概述
一种可以动态增长和缩减的索引序列
特点
1、有序
2、可重复
3、带索引
4、元素增删慢,查找快,线程不安全,由于日常开发中使用最多的功能为查询数据、遍历数据
5、ArrayList底层是数组实现的,数组是一块连续的内存空间,数组存放数据位数是相等的(即类型是一样的)
继承关系
AbstractList类
AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能
Cloneable接口
ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆
Serializable接口
ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输
扩容
1、旧数组没有容量,设置最小容量10
2、旧数组有容量,新数组就扩容到旧数组的1.5倍
源码分析
添加元素
ArrayList add 方法流程分析
获取元素
ArrayList get 方法流程分析
LinkedList
概述
java中 LinkedList 数据存储的结构是双向链表结构。线程不安全,效率高,可存储重复元素。
源码分析
创建 LinkedList 容器并添加元素
创建 LinkedList 容器并添加元素流程图
获取 LinkedList 容器中的元素
获取 LinkedList 容器中的元素流程图
LinkedList 和 ArrayList 的比较
1、ArrayList 的实现基于数组,LinkedList 的实现基于双向链表
2、对于随机访问,ArrayList 优于 LinkedList,ArrayList 底层数据结构是数组,可以根据下标用寻址公式对元素进行随机访问。而 LinkedList 的每一个元素都依靠地址指针和它后一个元素连接在一起,在这种情况下,查找某个元素只能从链表头开始查询直到找到为止
3、对于插入和删除操作,LinkedList 优于 ArrayList,因为当元素被添加到LinkedList 任意位置的时候,不需要像 ArrayList 那样重新计算大小或者是更新索引
4、LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
Vector、Stack
概述
底层数据结构是数组,Stack 是 Vector的子类,线程安全,先进后出(FILO,FIRST IN LAST OUT) 的特性
源码分析
入栈
入栈
出栈
出栈
Set 接口
概述
但凡是实现了Set接口的类都叫做Set集合, 元素无索引,元素不可重复(唯一),存储无序且不重复的数据,Set 判断两个对象是否相同不是使用 == 运算符,而是根据 hashcode 和 equals() 方法
set 集合无序性和不可重复性如何理解
无序性
set 集合存储的数据存储数据在底层数组中并非按照数组索引顺序添加,而是根据数据的 hash 值存储
不可重复性
通过 equals 和 hashCode 方法判断数据是否一样,存入 Set 集合的数据往往需要重写这两个方法
主要实现类
HashSet
原理
set 集合底层数据结构是一个 HashMap(如果是有参构造 HashSet(int initialCapacity, float loadFactor, boolean dummy) 则数据结构为 LinkedHashMap,这个构造方法其实是提供给 LinkedHashSet 使用,访问修饰符为默认,所以开发者无法直接调用到这个有参构造方法),HashMap 的键即为 set 集合存储的数据存储数据,值是一个固定的 new Object(),HashSet 操作数据实际就是对 HashSet 中的 map (HashMap)进行操作
HashSet 集合存储数据的结构
哈希表结构
jdk8以前
数组+链表
jdk8以后
数组+链表+红黑树
保证元素唯一原理
1、当存储元素的时候,就会调用该元素的hashCode()方法计算该元素的哈希值
2、判断该哈希值对应的位置上,是否有元素:
3、如果该哈希值对应的位置上,没有元素,就直接存储
4、如果该哈希值对应的位置上,有元素,说明产生了哈希冲突
5、产生了哈希冲突,就得调用该元素的equals方法,与该位置上的所有元素进行一一比较:
如果比较的时候,有任意一个元素与该元素相同,那么就不存储(Map 覆盖)
如果比较完了,没有一个元素与该元素相同,那么就直接存储
添加操作
进入 HashSet 的 add 方法后,直接调用 map 的put方法,将需要存储的数据作为键,new 一个 Object 作为值存放进map,并对 map.put 方法的返回值进行空值判断,如果 ==null,则添加成功,不等于 null,则添加失败,这是由于 HashMap 的put方法返回值所决定的,如果put 的是一个新的键,则返回一个 null值,如果 HashMap 中已经存在这个键,则覆盖这个键对应的值,并将旧值返回。HashSet 正是利用这个来判断是否添加成功
LinkedHashSet
一种可以举例元素插入顺序的集合,但并不代表它有序,它依旧是一种无序的集合,是 HashSet的子类,底层依旧是有一个数组,在数据存入数组的时候还是无序的,不过他通过链表的形式记录了元素插入顺序。对于频繁的操作遍历,LinkedHashSet 效率优于 HashSet
TreeSet
比较数据是否相同并非是按照 equals 和 hashcode 方法比较,而是通过 定制排序(Comaprator)或自然排序(Comparable)进行比较
Queue 接口
Map
概述
Map接口 与Collection 接口 并列存在。用于保存具有映射关系的数据:key-value
Map 中的 key 和 value 都可以是任何引用类型的数
Map 中的 key 用Set来存放,不允许重复,即同一个对象所对应的类,须重写 hashCode() 和 equals() 方法
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
主要实现类
散列表在java 中的应用---HashMap
基本概念
HashMap 是一个利用散列表(哈希表)原理来存储元素的集合,是根据Key value而直接进行访问的数
据结构
据结构
数据结构
在 JDK1.7 中,HashMap 是由 数组+链表
在 JDK1.8 中,HashMap 是由 数组+链表+红黑树
继承体系
Cloneable 空接口
表示可以克隆
Serializable
可使用 JDK 序列化
AbstractMap
提供Map实现接口
工作流程
1、new HashMap<>()
构造一个空的HashMap ,设置负载因子为0.75,this.loadFactor = DEFAULT_LOAD_FACTOR
2、调用 put 方法往 HashMap 中存储数据
put 一个 key 为 1,value 为 “001” 的数据
3、Hash 扰动- 获取 hash值
调用HashMap 的 hash 方法即 hash 扰动,如果 key 为 null,则存储位置下标为 0,如果不为 null,用 key 的 hashCode 与 hashCode 无符号右移 16 位后的值做 ^ 异或运算,得到一个 hash 值
4、初始化数组长度
第一次调用 put ,首先调用 resize 对数组进行扩容,初始化数组长度,如果没有填初始容量,默认 16,初始化扩容阈值,默认 0.75 即12,如果不是第一次调用 put 方法,直接跳到第 5 步
5、寻址-找到当前数据存放的桶即数组下标
(n-1) & hash,数组的桶的个数 - 1 与 hash 值 做按位与运算,得到存放的桶的位置即数组下标
思考:这里为什么不用取模运算而用按位与?
mod运算是与运算的20倍,效率太低下,&运算为什么这么高?操作的是二进制
6、存放数据
1、如果桶的位置为空,构建 Node<>(hash,key,value,next) 对象放入桶中
2、如果桶里已经存在,hash 值相同 且 调用 equals 方法比较 key 也相同,则覆盖原来的 value 值,并返回旧的 value
3、Hash 冲突,key 不相同但 hash 值一样即桶的位置一样
判断当前 Node 节点是否树化,没有树化,则循环获取 Node 节点,直到 Node 的 next 节点为空,创建一个 Node赋值到 上一个 Node 的next 属性上,然后再判断是否需要树化
扩容与数据迁移
扩容
如果是第一次 put ,就是默认的数组长度 16 和默认的扩容阈值 16 *0.75 = 12
如果不是第一次 put,数组长度 和 扩容阈值默认左移 1 位,即扩容 2 倍
数据迁移
循环旧数组 oldTab,将有数据的桶赋值给临时变量,然后将当前桶(即当期数组位置)置为 null,便于 GC
1、如果当前桶里只有一个Node节点(即e.next =null),那就将这个 Node
2、如果当前桶里的 Node 是红黑树结构
待完善
3、如果桶里有多个节点且不是红黑树
迁移过去的值需要重新计算hashCode,也就是他的存储位置
待完善
底层原理
见笔者另一篇作品 《HashMap 源码分析》
LinkedHashMap
HashMap的子类,存储数据采用的 哈希表结构+链表 结构。通过链表结构可以保证键值对的存取顺序一致;
通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、
通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、
原理简述
LinkedHashMap 作为 HashMap 的 子类,put 方法继承自 HashMap,但在创建 Node 节点的时候,多态调用了LinkedHashMap 自己实现了 newNode 方法,创建的 Node 对象不再是 HashMap 的内部类 Node<K,V>,而是自己内部类 LinkedHashMap.Entry<K,V>,这个类继承了 HashMap 中的Node,同时多了 before ,after 两个Entry<K,V> 成员,通过 before 和 after 来记录前后顺序
TreeMap
TreeMap集合和Map相比没有特有的功能,底层的数据结构是红黑树;
可以对元素的键进行排序,排序方式有两种:自然排序和比较器排序
可以对元素的键进行排序,排序方式有两种:自然排序和比较器排序
HashTable
Properties
泛型
概念
所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
为什么使用泛型而不直接使用 Object
1、Object 没有错误检查,可以向集合中添加任何类型对象。泛型解决元素存储的安全性问题,
2、当获取数据元素时,使用 Object 需要强制类型转换,而是用泛型不再需要类型强制转换
假设一个 List 要存储 String 类型的数据
当集合使用 Object 作为参数类型时
1、将 String 类型对象添加到存储 Object 参数类型的对象的集合中
2、读取集合中的数据时,将数据将转为 String
这样做导致在添加元素到集合中的时候,任意类型都可以添加到集合中。当读取数据时需要强转过程繁琐,且强转可能不成功而导致类型转换错误
当集合使用泛型作为参数类型时
只有指定类型才可以添加到集合中,保证类型安全,读取时不需要强转且不会出现类型转换错误
泛型的使用
泛型类
泛型方法
类型变量的限定
<T extends BoundingType>
表示 T 应该是绑定的子类型,T 和绑定类型可以是类,也可以是接口
PS: 一个类型变量或通配符可以有多个限定,T extends BoundingType & Serializable,限定类型用 & 分隔,类型变量用逗号 ,分隔。可以根据需要用用多几个接口超类型,但限定中至多有一个类,如果用类作为限定,必须是限定列表中的第一个。
类型擦除
概述
定义一个泛型类,都会自动提供一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型。如果无限定,则用 Object 替代
约束与局限性
1、不能使用基本类型实例化类型参数
因为类型擦除的存在,Object 不能存储 double ,int,float...等值
2、运行时类型查询只适用于原始类型
eg
Pair<String> stringPair = .....
Pair<Employee> employeePair=......
stringPair.getClass() == employeePair.getClass() 为 true
3、不能创建参数化类型的数组
eg
Pair<String>[] table = new Pair<String>[10] //ERROR
PS: 声明 Pair<String>[] 类型的变量是合法的,只是不允许创建参数化类型数组
4、不能实例化类型变量
不能使用 new T() 这样的表达式中的类型变量,因为类型擦除会将 T 改为 Object
5、不能构造泛型数组
T[] mm = new T[10] //ERROR
6、泛型类的静态上下文中类型变量无效
在泛型类中,不能在静态域或方法中引用类型变量,禁止使用带有类型变量的静态域和方法。
PS: 泛型方法可以是静态
7、不能抛出或捕获泛型类的实例
IO 流
File类
概述
文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
API
略
IO的顶层父类
字节输入流
顶层父类 InputStream 抽象类
字节输出流
顶层父类 OutputStream 抽象类
字符输入流
顶层父类 Reader 抽象类
字符输出流
顶层父类 Writer 抽象类
分类
根据数据的流向分类
输入流
概述
把数据从 其他设备 上读取到 内存 中的流
字节输入流
以字节为基本单位,读数据
字符输入流
以字符为基本单位,读数据
输出流
概述
把数据从 内存 中写出到 其他设备 上的流。
字节输出流
以字节为基本单位,写出数据
字符输出流
以字符为基本单位,写出数据
根据读写操作的基本单位分类
字节流
以字节为单位,读写数据的流。
字节输入流
以字节为基本单位,读数据
字节输出流
以字节为基本单位,写出数据
字符流
以字符为单位,读写数据的流。
字符输入流
以字符为基本单位,读数据
字符输出流
以字符为基本单位,写出数据
JDK 中后缀是 Stream 是字节流;后缀是 Reader,Writer 是字符流
根据功能分类
节点流
直接与数据源相连(直接和文件打交道),读入或写出
处理流
与节点流一块使用,在节点流的基础上,再套接一层,不与文件直接接触
常用 IO 流
字节流
字节节点流
FileInputStream
以字节的形式对文件进行读取操作,如读取一张图片
FileOutputStream
以字节的方式向文件写入内容
字节缓冲流
BufferedInputStream
字符输入缓冲流
BufferedOutputStream
字符输出缓冲流
问:为什么缓冲流效率高?
缓冲输入流相对于普通输入流的优势是,它提供了一个缓冲数组,每次调用read方法的时候,它首先尝试从缓冲区里读取数据,若读取失败(缓冲区无可读数据),则选择从物理数据源(譬如文件)读取新数据(这里会尝试尽可能读取多的字节)放入到缓冲区中,最后再将缓冲区中的内容部分或全部返回给用户.由于从缓冲区里读取数据远比直接从物理数据源(譬如文件)读取速度快。
对象序列化 、 反序列化
ObjectInputStream
反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
ObjectOutpuStream
将Java对象的原始数据类型写出到文件,实现对象的持久存储。
注意事项
序列化类必须要实现Serializable接口
序列化类中对象属性要求实现Serializable接口
序列化版本号ID,保证序列化的类和反序列化的类是同一个类
使用transient(瞬间的)修饰属性,这个属性不能序列化
静态属性不能序列化
字符流
字符节点流
FileReader
FileWriter
并发
1、进程和线程
1、进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
2、线程
进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。
守护线程
Java中有两种线程,一种是用户线程,另一种是守护线程。
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
守护线程当进程不存在或主线程停止,守护线程也会被停止。
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
守护线程当进程不存在或主线程停止,守护线程也会被停止。
2、并发与并行
1、并发
多个事情在同一时间段交替发生(cpu 时间片切换)
2、并行
多个事情在同一时刻同时发生(多个CPU)
3、线程安全
1、线程安全概述
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的,反之则是线程不安全的。
2、线程安全三要素
1、原子性问题
所谓的原子性是指在一次操作或者多次操作中,
要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,
要么所有的操作都不执行,多个操作是一个不可以分割的整体。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,
要么所有的操作都不执行,多个操作是一个不可以分割的整体。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
图示
2、可见性问题
是指多个线程访问一个共享资源时,该资源的状态、值信息的修改对于其他线程不是立即可见的。如果其他线程处于循环中,且循环体代码执行速度非常快,快到工作线程来不及读取主存中的值,那么会造成死循环,如图所示。
图示
3、有序性问题
指程序中代码的执行顺序 (编译器会重排)
图示
3、变量线程安全分析
成员变量和静态变量
如果它们没有共享,则线程安全
如果它们被共享了
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量
局部变量是线程安全的
但局部变量引用的对象则未必安全
4、java 内存模型 JMM
概述
JMM 是抽象模型,一种规范,一种设计,一种思想,JVM 内存区域 就是按照 Java 内存模型 去划分的,其中 栈区和PC 都是属于工作空间内存,
JMM 内存区域
1、主内存
存放共享数据
2、工作内存空间
线程私有数据,如果是基本数据类型,直接分配到工作空间内存(就是栈)如果是引用数据类型,引用 地址存放在工作内存,引用的对象放在堆中
工作方式
所有的共享变量都存在主内存中,线程在执行的时候,有单独的工作内存,会把共享变量拷贝一份到现成单独的工作内存中,并对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值,线程对拷贝到工作内存的共享变量副本进行修改后,在之后的某一刻(什么时间不确定)会将修改后的值同步到主内存中,更新主内存的值。而后,其他线程会在不确定的某一时刻读取主存中的共享变量值
4、锁
java 对象结构
对象头(object header)
概述
括了关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和vm内部对象都有一个共同的对象头格式。
MarkWord
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
32bit
64bit
Klass Pointer
即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据(Instance Data)
实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
对齐填充(Padding)
为了字节对齐,填充的数据,不是必须的。
图示
synchronized 关键字
工作原理
被synchronized修饰的代码块,多了两个指令
monitorenter
1、概述
1、每一个对象都可以和一个监视器monitor关联。
2、监视器被占用时会被锁住,其他线程无法来获取该monitor。
3、当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。
2、流程
1、若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为
monitor的owner(所有者)
monitor的owner(所有者)
2、若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
3、 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直
到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
monitorexit
1、能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
2、 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出
monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个
monitor的所有权
monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个
monitor的所有权
3、monitorexit释放锁。monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。
被synchronized修饰的方法,多了一个指令
采用 ACC_SYNCHRONIZED 标记,表示这是一个互斥方法,底层隐式调用了 monitor
锁重入
sync 拥有锁重入的功能,即在使用 sync 时,当一个线程得到锁后,再次请求此对象的锁时,即获取对象关联的 monitor 对象,monitor 进入数加 1
问题:正常退出时计数器减1,抛异常时计数器也是减1。那如果两次重入,在内层抛出异常,会释放锁吗?还是只会计数器减1,锁并不会释放?
会
总结
1、synchronized是靠Monitor关联拿到锁的
2、如果竞争的时候拿不到锁,线程就去竞争队列
3、如果拿到锁了,第二次拿,它又拿到锁,其他线程进入阻塞队列
4、如果拿到锁的线程调用了wait方法,其他线程进入等待队列
5、释放锁,需要将计数器减减操作
6、出现异常,释放锁。锁重入后内层出现异常,也释放锁(Exception table)
偏向锁
概述
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0 来禁用延迟
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0 来禁用延迟
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值
图示
-
偏向锁获取过程
访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
如果为可偏向状态,则测试线程ID是否指向当前线程
如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码。
如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
偏向锁的释放
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
轻量级锁
使用场景
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以
使用轻量级锁来优化。
使用轻量级锁来优化。
使用方法
轻量级锁对使用者是透明的,即语法仍然是 synchronized
加锁过程
案例
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
拷贝对象头中的Mark Word复制到锁记录中。拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
入计数减一。如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
入计数减一。如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
解锁
锁升级
概述
线程如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁,即让 Object 对象的MarkWord 指向Monitor,当前线程阻塞等待。待到持有轻量级锁的线程执行完后解锁,发现 Markword 的指针已经指向 Monitor ,解锁失败,然后执行重量级锁的解锁流程
案例分析
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,避免上下文切换消耗性能。当多次自旋仍然无法获得锁时,再进入 Monitor 的 EntryList 阻塞
PS
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能
ReentrantLock
5、volatile 关键字
概述
是一个变量修饰符,只能修饰成员变量,强制线程每次从主内存中取值,让其他线程能立即马上感知到某一线程对某个变量的修改 ,并能保证此变量不会被编译器优化,禁止指令重排(但其他线程不一定能读到最新修改后的变量的值,因为 volatile 不具有原子性)
具体表现
1、保证可见性
对共享变量修改其他变量马上能感知,即执行共享变量写操作的时候,发出信号通知其他 CPU 将该变量的 Cache line 置为无效,但最终其他线程能不能从主存中读到更新后的数据还不一定,看读的时机(也就是说不能满足原子性)
2、保证有序性
禁止指令重排;在编译阶段和指令优化阶段会重排,输入代码的顺序并不是最终执行的顺序,执行时间短的会被放到前面执行,提高CPU的吞吐量。重排序后对单线程没有影响,但是对多线程有影响
3、不能保证原子性
6、原子类
1、原子类
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicLongArray
AtomicReference
AtomicReferenceArray
........
2、工作原理 - CAS 算法
介绍
Compare and Swap即比较交换,CAS 有三个操作数,内存值V,旧的预期值A,将要修改的值B,当且仅当预期值A 和内存值V相同时(即工作内存中的值和主内存中的值一致),将内存值修改为 B 并返回 true,如果不相同则证明内存值在并发的情况下已经被其他线程修改过了,则不作任何修改,返回 false,循环重复操作
疑问?如果多条线程同时获取到旧值,compare结果为 true
这是硬件层面保证的。读并且交换值,虽然是两个动作,但cpu层面保证了原子性。CAS 的 C(比较)和 S(交换)两个操作是原子的,多个线程同时执行时只有一个线程能获取到旧值,其他看到的都是old+1。
所以CAS有单核和多核两个版本,多核时,硬件 CPU 会对 bus 加锁,所以会看到汇编指令前有 lock 的 prefix 。单核时就不需要锁了。
流程
1、工作内存从主内存中获取共享变量的值
2、再拿工作内存中获取到的共享变量的值与当前主内存中的值进行比较
3、如果相同,就做写操作,然后写入主内存;如果不相同,就循环重新获取,判断,做写操作,再写入主内存
图示
cas 工作原理
CAS 源码
7、并发包
并发集合
List
线程不安全
ArrayList
线程安全
CopyOnWriteArrayList
简介
CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。
原理
底层实现添加的原理是先copy出一个容器(可以简称副本,利用 Arrays.copyOf),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。且修改数据期间其他线程无法去写入数据,写操作会利用 ReentrantLock 加锁
Set
线程不安全
HashSet
线程安全
CopyOnWriteArraySet
原理
内部维护的是一个 private final CopyOnWriteArrayList<E> al;, 所以 CopyOnWriteArraySet 的实现原理是通过 CopyOnWriteArrayList 的机制来实现的;它的 set、add、remove方法实际上都是调用 CopyOnWriteArrayList 的set,add,remove 方法
Map
线程不安全
HashMap
线程安全
HashTable
public synchronized V put(K key, V value)
public synchronized V get(Object key)
public synchronized V get(Object key)
HashTable容器使用synchronized来保证线程安全,锁了整张 Hash表,在线程竞争激烈的情况下 HashTable 的效率非常低下。因为当一个线程访问 HashTable 的同步方法,其他线程也访问 HashTable 的同步方法时,会进入阻塞状态。如线程1 使用 put 进行元素添加,线程2 不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap
锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
新类库构件
1、CountDownLatch
能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
2、CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到
达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障
拦截的线程才会继续运行。例如:公司召集5名员工开会,等5名员工都到了,会议开始。
达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障
拦截的线程才会继续运行。例如:公司召集5名员工开会,等5名员工都到了,会议开始。
3、Semaphore
是一个计数信号量,必须由获取它的线程释放。常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。 Semaphore的主要作用是控制线程的并发数量。
synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore可以设置同时允许几个线程执行。
Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore可以设置同时允许几个线程执行。
Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
4、Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二
个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生
产出来的数据传递给对方。
A线程 exchange方法 把数据传递B线程
B线程 exchange方法 把数据传递A线程
这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二
个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生
产出来的数据传递给对方。
A线程 exchange方法 把数据传递B线程
B线程 exchange方法 把数据传递A线程
8、线程池
1、概述
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象
的操作,无需反复创建线程而消耗过多资源。
的操作,无需反复创建线程而消耗过多资源。
2、使用线程池好处,为什么使用线程池
1、降低资源消耗
通过重复利用已创建好的线程降低线程创建和销毁带来的损耗
通过重复利用已创建好的线程降低线程创建和销毁带来的损耗
2、提高响应速度
线程池中线程数没有达到最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
线程池中线程数没有达到最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
3、提高线程的可管理性
线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁带来的系统开销,无限的创建和销毁线程不仅小号系统资源,还降低系统的稳定性,使用线程池进行统一分配。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗
过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机
线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁带来的系统开销,无限的创建和销毁线程不仅小号系统资源,还降低系统的稳定性,使用线程池进行统一分配。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗
过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机
3、线程池 7 大参数
1、corePoolSize
核心线程数,线程池创建好后就初始化的线程数量,等待接收异步任务去执行 相当于 new 了 corePoolSize 个 Thread,只要线程池不销毁,这些线程一直存在,除非设置了 allowCoreThreadTimeOut
2、maximumPoolSize
最大线程数量,即线程池里允许存放的最大线程数量,控制资源
3、keepAliveTime
存活时间,当前正在运行的线程数量大于 corePoolSize,且线程空闲时间大于了指定的 keepAliveTime, 当前正在运行的线程 - corePoolSize 的线程数会被释放掉,核心线程不会释放
4、unit
时间单位,是 KeepAliveTime 参数的时间单位
5、workQueue
阻塞队列,如果任务很多,就会将多的任务放入队列,只要有现成空闲,就会从队列中取出新的任务继续执行
6、threadFactory
线程的创建工厂
7、RejectedExecutionHandler
如果队列满了,按照指定的拒绝策略拒绝执行任务
1、AbortPolicy
该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
2、DiscardPolicy
如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
3、DiscardOldestPolicy
这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。
因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。
4、CallerRunsPolicy
使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。
5、自定义策略
如果以上策略都不符合业务场景,那么可以自己定义一个拒绝策略,只要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法就可以了。具体的逻辑就在rejectedExecution方法里去定义就OK了。
4、线程池创建流程
1、线程池创建,初始化好corePoolSize 数量的核心线程,准备接受任务
2、corePoolSize 满了没有空闲线程,就将新来的任务放入阻塞队列,空闲下来的 corePoolSize 线程会自己去阻塞队列获取任务执行
3、如果阻塞队列也满了,就继续创建线程,直到线程数达到 maximumPoolSize
4、如果达到了 maximumPoolSize,还有任务进来,执行 RejectedExecutionHandler 拒绝任务
如果 maximumPoolSize 未满或者都顺利执行完成,在指定的 keepAliveTime +unit时间后,释放掉 maximumPoolSize - corePoolSize 的线程数,即非核心线程都释放掉
如果 maximumPoolSize 未满或者都顺利执行完成,在指定的 keepAliveTime +unit时间后,释放掉 maximumPoolSize - corePoolSize 的线程数,即非核心线程都释放掉
函数式编程
lambda 表达式
在 JAVA 中,lambda 表达式所能做得也只是能转换为函数式接口
省略规则
1、小括号内参数的类型可以省略
2、如果小括号内有且仅有一个参数,则小括号可以省略
3、如果大括号内有且仅有一条语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
Lambda的表现形式
变量的形式
变量的类型为函数式接口类型,那么可以赋值一个Lambda表达式
Runnable r = ()->{
System.out.println("任务代码");
};
Comparator<Integer> com = (Integer i1,Integer i2)->{return i2 - i1;};
System.out.println("任务代码");
};
Comparator<Integer> com = (Integer i1,Integer i2)->{return i2 - i1;};
参数的形式
方法的形参类型为函数式接口类型,那么就可以传入一个Lambda表达式
Collections.sort(list,(Integer i1,Integer i2)->{
return i2 - i1;
});
return i2 - i1;
});
返回值的形式
方法的返回值类型为函数式接口类型,那么就可以返回一个Lambda表
达式
达式
public static Comparator<Integer> getComparator(){
return (Integer i1 , Integer i2)->{return i1 - i2;};
}
return (Integer i1 , Integer i2)->{return i1 - i2;};
}
函数式接口
概述
只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口
如果是函数式接口,那么就可以使用@FunctionalInterface注解来标识检查
接口中有且仅有一个抽象方法的接口(别的方法无影响),才可以使用Lambda表达式
接口中只有一个抽象方法的接口,叫做函数式接口
Java 内置四大核心函数式接口
Consumer<T> 消费型接口
对类型为T的对象应用操作,包含方法: void accept(T t)
Supplier<T> 供给型接口
返回类型为T的对象,包含方法:T get()
Function<T, R> 函数型接口
对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
Predicate<T> 断定型接口
确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t)
方法引用
概述
方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
要求
实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!使用操作符 双冒号“::” 将类(或对象) 与 方法名分隔开来。
使用场景
如果一个Lambda表达式大括号中的代码和另一个方法中的代码一模一样,那么就可以使用方
法引用把该方法引过来,从而替换Lambda表达式
法引用把该方法引过来,从而替换Lambda表达式
如果一个Lambda表达式大括号中的代码就是调用另一方法,那么就可以使用方法引用把该方
法引过来,从而替换Lambda表达式
法引过来,从而替换Lambda表达式
使用方法引用的步骤
1.分析要写的Lambda表达式的大括号中是否就是调用另一个方法
2.如果是,就可以使用方法引用替换,如果不是,就不能使用方法引用
3.确定引用的方法类型(构造方法,成员方法,静态方法,类的成员方法)
4.按照对应的格式去引用
构造方法
类名::new
与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。
成员方法(有参数)
对象名::方法名
静态方法
类名::方法名
静态方法\成员方法(无参数)
类名::方法名
eg
Comparator<Integer> integerComparator3 = (o1, o2) -> Integer.compare(o1, o2);
Comparator<Integer> integerComparator4 = Integer::compare;
Stream 流
Stream 和 Collection 集合的区别
Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中,
后者主要是面向 CPU,通过 CPU 实现计算。
后者主要是面向 CPU,通过 CPU 实现计算。
特性
1、Stream流不是一种数据结构,不保存数据,它只是在原数据集上定义了一组操作。
2、这些操作是惰性的,即每当访问到流中的一个元素,才会在此元素上执行这一系列操作。
3、Stream不保存数据,故每个Stream流只能使用一次。
4、Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
Stream 的操作三个步骤
创建 Stream
一个数据源(如:集合、数组),获取一个流
中间操作
一个中间操作链,对数据源的数据进行处理
终止操作(终端操作)
一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
流式操作概述
1、中间操作
中间操作的返回结果都是Stream,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”
2、终止操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如: List、 Integer、Double、String等等,甚至是 void 。只能有一个终止操作
操作
Intermediate(中间操作)
筛选与切片
filter(Predicate p)
Stream<T> filter(Predicate<? super T> predicate)
filter()方法是根据Predicate接口的test()方法的返回结果来过滤数据的,如果test()方法的返回结果为true,符合规则;如果test()方法的返回结果为false,则不符合规则被过滤。
distinct()
Stream<T> distinct()
distinct 需要实体中重写hashCode()和 equals()方法才可以使用,通过流所生成元素的 hashCode() 和 equals() 去 除重复元素。
skip(long n)
Stream<T> skip(long n)
跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素 不足 n 个,则返回一个空流。与 limit(n) 互补。
limit(long maxSize)
Stream<T> limit(long maxSize)
截断流,使其元素不超过给定数量 maxSize
映射
map(Function f)
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
接收一个函数作为参数,该函数会被应用到每个元 素上,并将其映射成一个新的元素
flatMap(Function f)
<R> Stream<R> flatMap(Function< ? super T, ? extends Stream<? extends R> > mapper)
接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流。
排序
sorted()
Stream<T> sorted()
产生一个新流,其中按自然顺序排序
sorted(Comparator comp)
Stream<T> sorted(Comparator<? super T> comparator);
产生一个新流,其中按比较器 comparator 的规则定制排序
Terminal(终止操作)
查找与匹配
allMatch()
boolean allMatch(Predicate<? super T> predicate)
检查是否匹配所有元素,只有所有的元素都匹配条件时,allMatch()方法才会返回true。
anyMatch()
boolean anyMatch(Predicate<? super T> predicate)
检查是否至少匹配一个元素
noneMatch()
boolean noneMatch(Predicate<? super T> predicate)
noneMatch()方法表示检查是否没有匹配所有元素
findFirst()
Optional<T> findFirst()
表示返回第一个元素,返回一个 Optional对象,通过该对象的 get 方法获取真实对象
findAny()
Optional<T> findAny()
表示返回当前流中的任意元素,返回一个 Optional对象,通过该对象的 get 方法获取真实对象
count()
long count();
表示返回流中元素总数
max()
Optional<T> max(Comparator<? super T> comparator)
表示返回流中最大值,返回一个 Optional对象,通过该对象的 get 方法获取真实对象
min()
Optional<T> min(Comparator<? super T> comparator)
返回流中最小值,返回一个 Optional对象,通过该对象的 get 方法获取真实对象
forEach()
void forEach(Consumer<? super T> action)
内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反, Stream API 使用内部迭代)。
规约
reduce(T iden, BinaryOperator b)
可以将流中元素反复结合起来,得到一个值。 返回 T
reduce(BinaryOperator b)
可以将流中元素反复结合起来,得到一个值。 返回 Optional
收集
collect(Collector c)
将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
Collector
Collector接口中方法的实现决定了如何对流执行收集操作
toList
返回 List,把流中元素收集到List
toSet
返回 Set,把流中元素收集到Set
toCollection
返回 Collection,把流中元素收集到创建的集合
counting
返回 Long,计算流中元素的个数
summingInt
返回 Integer,对流中元素的整数属性求和
......
JVM
类加载机制
什么是类加载
类加载的描述
当程序要使用某各类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的连接、类的初始化这三个步骤来对类进行初始化,如果不出现意外情况,JVM将会连续完成这三个步骤,所以将这三步骤统称为类的初始化
类加载过程
1、加载
将 class 文件读入内存,并创建一个 java.lang.Class 对象,任何类被使用时,系统都会建立一个 java.lang.Class 对象
2、连接
1、验证阶段
用于检验被加载的类是有正确的内部结构,并和其他类协调一致
2、准备阶段
负责为类的静态变量 static 分配内存,并设置默认初始化值,0 或 null,真实值要等到类初始化阶段才会被赋值
3、解析阶段
将类的二进制数据中的符号引用替换为直接引用
符号引用
符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。个人理解为:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
直接引用
直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。
3、初始化
类加载过程的最后一个步骤,在这之前类变量以在准备阶段初始化了默认值,这里会将类变量的初始化值修改为开发者定义的真实变量值
类加载时机
1、使用 new 关键字实例化对象时
2、读取或设置一个类型的静态字段(被 final 修饰,已在编译期把结果放入常量池的静态字段除外)
3、调用一个类的静态方法
4、使用 java.lang.reflect 包的方法对类型进行反射调用时,如果类型还未初始化,则需要先触发其初始化
5、当初始化类时,如果父类还未初始化,先触发父类初始化
6、当虚拟机启动,用户需要制定一个要执行的主类( main 方法的类),虚拟机会先初始化这个主类
7、当一个接口定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口实现类发生了初始化,那么接口要在其之前初始化
类加载器的分类
全盘负责
当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和应用的其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
父类委托
当一个类加载器负责加载 Class 时,先让父类加载器尝试加载该 Class,只有父类加载器无法加载该类时才尝试从自己的类路径中加载该类
缓存机制
保证所有加载过的 Class 都会被缓存,当程序需要使用某个 Class 对象时,类加载器先从缓存区中搜索该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存储到缓存区
类加载器的作用
负责将 class 文件加载到内存中,并为之生成对应的 java.lang.Class 对象。
java 中的内置类加载器
java 8
Bootstrap Class loader
它是虚拟机的内置类加载器,通常表示为 null,是一个 根加载器
Extension Class loader
扩展类加载器,负责加载 JAVA_HOME/lib/ext/ 目录中的类库
APP Class loader
应用类加载器,与平台类加载器不同,系统类加载器负责加载用户类路径 Classpath 上的所有类库
类加载方式
当类加载器收到类加载的请求,他首先不会尝试自己去加载这个类,而是把请求交给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到最顶层的启动类加载器 Bootstrap class loader 去完成,只有当父类加载器无法完成这个加载请求(它的搜索范围里没有找到所需的类),子类加载器才会尝试自己去完成加载
java 9
Bootstrap Class loader
它是虚拟机的内置类加载器,通常表示为 null,是一个 根加载器
Platform Class loader
平台类加载器可以看到所有平台类,平台类包括由平台类加载器或其祖先定义的 JAVA SE 平台 API,其实现类和 JDK 特定的运行时类
APP Class loader
应用类加载器,与平台类加载器不同,系统类加载器负责加载用户类路径 Classpath 上的所有类库
类加载方式
JAVA9移除了扩展类加载器,引入了平台类加载器,并且加载方式引入了模块化的概念,当进行加载时,先加载平台类加载器,去模块化中找寻类,如果没有就在向上寻找,一直到引导类加载器,如果还是没有找到,那么就往系统类加载器去加载。
垃圾回收机制
判断对象是否“死亡”
引用计数
在对象中添加一个引用计数器,当存在一个引用时,计数器加 1,当引用失效时,计数器就会减 1,当计数器为 0 时,对象就可以进行垃圾回收
可达性分析(java进行垃圾回收的算法)
通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 间没有任何引用链相连,则证明此对象不可能再被使用
作为 GC Roots 的对象包含下面几种
虚拟机栈中引用的对象(本地变量表)
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
java 中的四种引用
强引用
指代码中普遍存在的引用赋值,只要存在强引用,垃圾回收器永远不会回收被引用的对象
软引用
用来描述一些还有用但非必须的对象,被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行二次回收。如果一个对象只具有软引用,若内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存
弱引用
不管内存是否足够,弱引用的对象都会在下一次垃圾回收时回收该对象
虚引用
那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
判断对象死亡过程
在可达性分析算法中,被判定为不可达对象,也并非就一定会被垃圾回收,要真正宣告一个对象成为垃圾,需要经历两次标记过程。可达性分析算法判断对象没有与 GC Roots 相连接的引用链,此时被第一次标记。之后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,加入对象没有覆盖 finalize 方法 或者 finalize 方法 已经被虚拟机调用过,那么虚拟机将这两种情况视为没有必要再执行如果有必要执行 finalize 方法,那么该对象会被放置在一个 F-Queue 队列之中,之后虚拟机会开起一条线程 去执行他们的 finalize 方法,但并不承诺一定会等待 finalize 方法执行完毕。在这之后垃圾收集器会对 F-Queue 中的对象进行第二次小规模标记,如果此时对象还未逃脱就真的会被回收了
垃圾回收算法
标记-清除算法
概述
最基础的收集算法是 “标记-清除” 算法,如同它的名字一样,算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
回收前的状态
回收后的状态
缺点
效率偏低,标记和清除两个过程的效率都不高。
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-整理算法
概述
“标记-整理” 算法是建立在 “标记-清理” 算法基础之上的,其标记过程仍然与 “标记-清理” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,从而解决内存碎片化问题。
回收前状态
回收后状态
缺点
与 “标记-清理” 算法一样,”标记-整理“ 算法也需要进行标记和清理,并且还多了一步整理过程,所以效率也比较低。
标记-复制算法
概述
为了解决效率问题,一种称为 “复制” 的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
图示
缺点
这种算法的代价是将内存缩小为了原来的一半,每次只使用一半
分代收集算法
概述
根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对象进行分配担保,就必须使用 “标记-清理” 或者 “标记-整理” 算法来进行回收。新生代中 98% 的对象都是 "朝生夕死" 的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活者的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被 “浪费”。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被 “浪费”。
图示
新生代一般使用 标记-复制算法,老年代使用 标记-整理算法
清理过程
1、创建新对象,放在Eden区
2、Eden满了(或达到一定比例),触发Minor GC, 把可达对象复制到Survivor1, 同时清空Eden区。
3、Eden区第二次满了,触发Minor GC, 把Eden和Survivor1中可达对象,复制到Survivor2, 同时清空Eden,Survivor1。
4、Eden区第三次满了,触发Minor GC, 把 Eden 和 Survivor2 中可达对象,复制到Survivor1, 同时清空Eden,Survivor2。
如此形成循环,Survoivor1和Survivor中来回清空、复制,过程中有一个Survivor处于空的状态用于下次复制的。
如此形成循环,Survoivor1和Survivor中来回清空、复制,过程中有一个Survivor处于空的状态用于下次复制的。
5、重复多次(默认15),没有被Survivor清理的对象,复制到Old(Tenuerd)区.
6、当Old满了,触发Full GC。注意,Full GC清理代价大,系统资源消耗高。
内存分配与回收策略
对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
对象进入老年代的条件
所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是很长的字符串以及数组(例如 new byte[1024 * 1024 * 10] //10MB)。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时,就提前除触发 GC 以获得足够的连续空间来 “安置” 它们。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数(只对Serial和ParNew两个新生代收集器有用),大于这个设置值的对象直接在老年代分配。这样做的目的时避免 Eden 区及两个 Survivor 区之间发生大量的内存复制。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数(只对Serial和ParNew两个新生代收集器有用),大于这个设置值的对象直接在老年代分配。这样做的目的时避免 Eden 区及两个 Survivor 区之间发生大量的内存复制。
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为1.对象在 Survivor 区每 “熬过” 一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
对象年龄判断
为了能更好的适应不同程序的内存情况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
minor gc后survivor放不下
在MinorGC之后存活的对象超过了survivor区的大小,会将这些对象直接转移到老年代
Minor GC 和 Full GC 的区别
新生代 GC(Minor GC)
指发生在新生代的垃圾收集动作,因为 Java 对象大多数都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC,Full GC)
指发生在老年代的 GC,出现了 Full GC,经常会伴随着至少一次的 Minor GC(并非绝对,Parallel Scavenge 收集器的策略就可以单独进行 Full GC)。Full GC 的速度一般会比 Minor GC 慢 10 倍以上,效率很低,需要避免频繁 full GC
垃圾收集器
概述
前面我们提到了垃圾收集的算法,但还需要具体的实现,在 JVM 中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS 垃圾收集器、G1 垃圾收集器。
图示
图中展示了七种不同的分代垃圾收集器,如果两个收集器之间存在连线,那么它们可以搭配使用,图中收集器所处的区域表示它们属于新生代或老年代收集器。
JVM 默认的垃圾收集器
JDK1.7
Parallel Scavenge (新生代)+ Parallel Old (老年代)
JDK1.8
Parallel Scavenge (新生代)+ Parallel Old (老年代)
JDK1.9
G1
新生代垃圾收集器
Serial 串行收集器
概述
Serial 收集器是最基础、历史最悠久的收集器,曾经是 HotSpot 虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,当收集器工作时,必须暂停其他所有的工作线程,直到它完成收集工作。当它工作时,我们的应用程序处于暂停状态,所以我们也将这个时刻称之为 “Stop The World”。
Serial 收集器会暂停应用程序,这对交互性强的应用是不可接受的,所以一般 Web 应用不会使用该收集器。
Serial 收集器会暂停应用程序,这对交互性强的应用是不可接受的,所以一般 Web 应用不会使用该收集器。
图示
ParNew 并行收集器
概述
PartNew 收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一致。
PartNew 收集器与 Serial 收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK7 之前的遗留系统中首选的新生代收集器。
PartNew 收集器与 Serial 收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK7 之前的遗留系统中首选的新生代收集器。
图示
Parallel 收集器
概述
Parallel 收集器是 JDK8 默认的新生代垃圾收集器,它的工作机制与 ParNew 垃圾收集器是一样的,只是在此基础之上,增加了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。所谓的吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,例如用户代码加上垃圾回收总耗时100秒,其中垃圾收集花掉了1秒钟,那吞吐量就是 99%。
高吞吐量能最大效率的利用处理器资源,低停顿时间能以更快的响应速度来提升用户的体验。
高吞吐量能最大效率的利用处理器资源,低停顿时间能以更快的响应速度来提升用户的体验。
-XX:MaxGCPauseMillis
设置垃圾收集时最大的停顿时间,单位为毫秒。 > > 需要注意的是,Parallel 收集器为了达到设置的停顿时间,可能会调整堆大小或其他参数,如果堆设置的比较小,可能会导致 GC 工作变得更加的频繁。
-XX:UseAdaptiveSizePolicy
自适应 GC 模式,垃圾收集器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。
图示
工作机制与 ParNew 一样
老年代垃圾收集器
Serial Old 收集器
概述
Serial Old 收集器是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 标记-整理 算法。这个收集器主要意义也是供客户端模式下的 HotSpot 虚拟机使用。
图示
Parallel Old 收集器
概述
Parallel Old 收集器是 Parallel 收集器的老年代版本,jdk1.7 和 jdk 1.8 老年代默认收集器。支持多线程并发收集,基于 标记-整理 算法实现。
Parallel Old 收集器直到 JDK6 时才开始提供,在此之前,新生代的 Parallel 收集器只能与 Serial Old 收集器配合工作,由于 Serial Old 收集器在服务端在性能上的拖累(单线程无法充分利用服务器多处理器的并行处理能力),这种组合的总吞吐量甚至没有 ParNew + CMS 的组合来的优秀。
直到 Parallel Old 收集器 出现后,“吞吐量优先” 收集器终于有了名副其实的搭配组合,在注重吞吐量的场景可以有限考虑使用 Parallel 收集器 + Parallel Old 收集器这个组合。
Parallel Old 收集器直到 JDK6 时才开始提供,在此之前,新生代的 Parallel 收集器只能与 Serial Old 收集器配合工作,由于 Serial Old 收集器在服务端在性能上的拖累(单线程无法充分利用服务器多处理器的并行处理能力),这种组合的总吞吐量甚至没有 ParNew + CMS 的组合来的优秀。
直到 Parallel Old 收集器 出现后,“吞吐量优先” 收集器终于有了名副其实的搭配组合,在注重吞吐量的场景可以有限考虑使用 Parallel 收集器 + Parallel Old 收集器这个组合。
图示
CMS 收集器
概述
CMS(Concurrent Mark Sweep)收集器是一种老年代垃圾收集器,CMS 收集器的目标是获取最短的停顿时间,目前很大一部分互联网应用使用的就是 CMS 收集器,因为这些应用更注重给用户带来良好的交互体验。CMS 收集器是基于 标记-清除 算法实现的,它的运作过程相对复杂
清理过程
1、初始标记(CMS initial mark)
初始标记仅仅只是标记一下 GC Roots 能够直接关联到的对象,速度很快;需要 “Stop The World”
2、并发标记(CMS concurrent mark)
从 CG Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程。
3、重新标记(CMS remark)
为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。需要 “Stop The World”,这个阶段的停顿时间比初始标记阶段的耗时会稍微长一些。
4、并发清除(CMS concurrent sweep)
清理掉标记阶段已经判定为死亡的对象。
图示
G1 垃圾收集器(jdk1.9默认使用)
概述
对于垃圾回收器来说,前面收集器要么是一次性回收新生代,要么一次性回收老年代,但现在的服务器的空间已经很大了,为了优化垃圾收集操作,出现了 G1 收集器。
G1 收集器是一款软实时、低延迟、可设定目标(最大 STW 停顿时间)的垃圾收集器,用于替
代 CMS,适用于较大的堆(大于4~6G),JDK9 之后默认使用 G1 收集器,其垃圾收集的时间能控制在 10 毫秒以内。
G1 收集器的设计原则就是简化 Java 虚拟机调优,开发人员只需要简单的三步即可完成调优:
1、开启 G1 垃圾收集器
2、设置堆的最大内存
3、设置最大停顿时间(STW)
G1 收集器是一款软实时、低延迟、可设定目标(最大 STW 停顿时间)的垃圾收集器,用于替
代 CMS,适用于较大的堆(大于4~6G),JDK9 之后默认使用 G1 收集器,其垃圾收集的时间能控制在 10 毫秒以内。
G1 收集器的设计原则就是简化 Java 虚拟机调优,开发人员只需要简单的三步即可完成调优:
1、开启 G1 垃圾收集器
2、设置堆的最大内存
3、设置最大停顿时间(STW)
G1 的内存布局
G1 垃圾收集器相比于其他收集器而言,最大的不同就是不再使用固定的年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。G1 将这些区域作为单次回收的最小单元,这样即可避免在整个堆中进行全区域的垃圾收集。
G1 能够跟踪每个区域里面的垃圾堆积价值,并在后台维护一个优先级列表,根据用户设定的允许停顿时间,优先处理回收价值收益最大的那些区域。
G1 能够跟踪每个区域里面的垃圾堆积价值,并在后台维护一个优先级列表,根据用户设定的允许停顿时间,优先处理回收价值收益最大的那些区域。
图示
可以看到出现了一个新的区域 Humongous,这个区域属于老年代区,专门用来存储大对象,当某个对象的大小超过了 Region 容量的一般则直接存储到这个区域。如果一个 H 区装步下这个大对象,G1 会寻找连续的 H 区来存储,为了能够找到连续的 H 区,有时候不得不启动一次 Full GC。
G1 能够自动的给 Region 分配最优的大小,如果需要手动设置,只能设置 1m, 2m, 4m, 8m, 16m, 32m 这几个值,最小是1m,最大是 32m,且必须是 2 的幂次方。
在 G1 的划分区域中,新生代的垃圾收集依然采用暂停所有用户线程的方式,将存活的对象拷贝到老年代或 Survivor 空间,G1 收集器通过将对象从一个区域复制到另一个区域完成了清理工作,这意味着 G1 在复制对象的过程中就可以完成堆内存的整理工作。
在 G1 的划分区域中,新生代的垃圾收集依然采用暂停所有用户线程的方式,将存活的对象拷贝到老年代或 Survivor 空间,G1 收集器通过将对象从一个区域复制到另一个区域完成了清理工作,这意味着 G1 在复制对象的过程中就可以完成堆内存的整理工作。
垃圾收集模式
Young GC
发生在新生代的 GC 算法,一般对象(除了大对象)都是在 Eden Region 中分配内存,当所有的 Eden Region 被耗尽,就会触发一次 Young GC。 > > 这种机制和之前的 Young GC 差不多,执行完一次 Young GC,活跃对象会被拷贝到 Survivor Region 或晋升到 Old Region 中。
Mixed GC
当越来越多的对象晋升到 Old Region 时,为了避免堆内存被耗尽,虚拟机会触发一次混合的垃圾收集,即 Mixed GC,该算法除了回收整个 Young Region 之外,还会回收一部分的 Old Region。只回收一部分 Old Region 的原因要要对垃圾回收的耗时进行控制。Mixed GC CMS 相同的 标记-清除 算法,运作过程也是相同的。
Full GC
如果对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法是单线程执行 Serial Old GC,所以开发人员需要根据情况适当对虚拟机进行调优,尽可能的避免 Full GC。
使用 G1 注意点
断地对期望最大停顿时间进行调优
通过 -XX:MaxGCPauseMillis=x 来设置期望的最大 GC 停顿时间,G1 在运行的时候会根据这个参数的设置在不同的应用场景中取得吞吐量和延迟之间的最佳平衡,一般设置在 200ms 左右,不能设置的太低,比如设置为 20ms 就不行,因为过低的期望停顿时间意味着收集器的处理速度可能会跟不上垃圾产生的速度,导致垃圾的慢慢堆积,最终因占满堆而引发 Full GC,反而降低了性能。
不要设置新生代和老年代的大小
G1 收集器在运行的时候会自动的调整新生代和老年代的大小,通过改变代的大小来调整对象的晋升速度和晋升年龄,从而达到我们为收集器设置的期望暂停时间的目标。设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们只需要设置整个堆内存的大小,剩下的交给 G1 自己区分配各个代的大小。
0 条评论
下一页