Java
2024-03-18 12:08:25 0 举报
AI智能生成
Java是一种广泛使用的计算机编程语言,拥有跨平台、面向对象、稳健和安全等特性。
作者其他创作
大纲/内容
基础
特点
OOP(封装,继承,多态)。
平台无关性( Java 虚拟机实现平台无关性)。
可靠性(具备异常处理和自动内存管理机制)。
安全性(访问权限修饰符、限制程序直接访问操作系统资源)。
高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的)。
多线程、支持网络编程且很方便、编译与解释并存。
大版本
Java SE(Java Platform,Standard Edition)
适合开发桌面应用程序或简单的服务器应用程序。
Java EE(Java Platform,Enterprise Edition )
适合开发复杂的企业级应用程序或 Web 应用程序。
Java ME(Java Platform,Micro Edition)
主要用于开发嵌入式消费电子设备的应用程序。(淘汰了)
JVM vs JDK vs JRE
JVM(Java Virtual Machine)
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。
JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,都会给出相同的结果。
JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。
JDK(Java Development Kit)
功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。
包含 JRE、码编译器 javac 以及一些其他工具,如 javadoc(文档注释工具)、jdb(调试器)、jconsole(可视化监控⼯具)、javap(反编译工具)等。
JRE(Java Runtime Environment)
Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。
从 JDK 9 开始就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ jlink 工具。JDK 11 不单独提供 JRE 下载。
可以用 jlink 根据自己的需求,创建一个更小的 runtime(运行时),而不是不管什么应用,都是同样的 JRE。
源码编译过程
字节码
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
.class->机器码
JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。
编译模式
JIT
JIT(Just in Time Compilation)属于运行时编译,解决热点代码(经常使用的代码)频繁重复编译问题,从而提高性能。
原理
当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
优点
具备更高的极限处理能力,可以降低请求的最大延迟。
AOT
AOT(Ahead of Time Compilation) 在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。
AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。
AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。
优点
优势在于启动时间、内存占用和打包体积。
缺点
无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。
应用
GraalVM
一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。
GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。
编译与解释并存
编译型
通过编译器将源代码一次性翻译成可被该平台执行的机器码。
编译语言的执行速度比较快,开发效率比较低。
常见的编译性语言有 C、C++、Go、Rust 等等。
解释型
通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。
解释型语言开发效率比较快,执行速度比较慢。
常见的解释性语言有 Python、JavaScript、PHP 等等。
即时编译
为了改善编译语言的效率而发展出的即时编译技术,已经缩小了编译型和解释型语言间的差距。
这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。
Java 与 LLVM 是这种技术的代表产物。
Oracle JDK vs OpenJDK
2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK(完全免费,GPL v2 许可协议)。
2009 年 Oracle 收购了 Sun 公司,在 OpenJDK 的基础上搞了一个 Oracle JDK(部分(短期)免费,BCL/OTN 许可协议)。
建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。
基本语法
注释
单行注释、多行注释、文档注释。
用的比较多的还是单行注释和文档注释,多行注释在实际开发中使用的相对较少。
注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释)。
代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
标识符
标识符就是为程序、类、变量、方法等取的名字 。
关键字
被赋予特殊含义的标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方。
continue、break 和 return
continue:指跳出当前的这一次循环,继续下一次循环。
break:指跳出整个循环体,继续执行循环下面的语句。
return
用于跳出所在方法,结束该方法的运行。
return;:直接使用 return 结束方法执行,用于没有返回值函数的方法。
return value;:return 一个特定值,用于有返回值函数的方法。
注意
default 这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制。
虽然 true, false, 和 null 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。
运算符
自增自减运算符
自增运算符(++)、自减运算符(--)
++ 和 -- 运算符可以放在变量之前或变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
移位运算符
移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。
移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
在 Java 代码里使用 <<、 >> 和 >>> 转换成的指令码运行起来会更高效些。
分类
<< :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
>> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
>>> :无符号右移,忽略符号位,空位都以 0 补齐。
注意
由于 double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。
移位操作符实际上支持的类型只有 int 和 long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作。
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。即 x<<42 等同于 x<<10。
new 运算符
new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
基本数据类型
基本类型
数字类型
整数型
byte、short、int、long
Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
BigInteger
用于超过 long 整型数据的数据类型。
原理
内部使用 int[] 数组来存储任意大小的整形数据。
缺点
相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。
浮点型
float、double
精度丢失
无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
解决
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。
防止精度丢失
BigDecimal(String val)
BigDecimal.valueOf(double val)
equals(),比较精度
compareTo(),忽略精度
字符类型
char
char a = 'h'char :单引号,String a = "hello" :双引号。
布尔型
boolean
依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
默认值及所占空间
字节 vs 位数
位数(bit):最小存储单位。
字节(byte):由8个bit组成。
Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。
包装类型
包装类
Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
缓存范围
Byte,Short,Integer,Long 默认创建了数值 [-128,127] 的相应类型的缓存数据。
Character 创建了数值在 [0,127] 范围的缓存数据。
Boolean 直接返回 True or False。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
比较
范围内==比较相等,范围外==比较不等,推荐equals比较。
自动拆装箱
装箱
将基本类型用它们对应的引用类型包装起来。
原理
调用了包装类的 valueOf() 方法。
拆箱
将包装类型转换为基本数据类型。
原理
调用了包装类的 xxxValue() 方法。
注意
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
基本类型和包装类型
用途:除了定义一些常量和局部变量外,在其他比如方法参数、对象属性中很少会使用基本类型来定义变量。包装类型可用于泛型,而基本类型不可以。
存储方式:基本数据类型的存放在栈中(局部变量)或存放在堆中(成员变量(未被 static 修饰 ))。包装类型属于对象类型,几乎都存在于堆中。
占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
比较方式:对基本数据类型来说,== 比较的是值。对包装数据类型来说,== 比较的是对象的内存地址。包装类对象之间值的比较,全部使用 equals() 方法。
变量
成员变量与局部变量
语法形式:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但成员变量和局部变量都能被 final 所修饰。
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但成员变量和局部变量都能被 final 所修饰。
存储方式:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,
如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
生存时间:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值:成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(被 final 修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值。
静态变量
被 static 关键字修饰的变量,可以被类的所有实例共享。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
字符型常量和字符串常量
形式:字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
含义:字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)。
内存大小:字符常量只占 2 个字节;字符串常量占若干个字节。
方法
方法的返回值
获取到的某个方法体中的代码执行后产生的结果。
静态方法和实例方法
调用方式
在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式(不推荐),而实例方法只有 对象.方法名 这种方式。
访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员,不允许访问实例成员,而实例方法不存在这个限制。
重载和重写
重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
规则
方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
构造方法无法被重写。
构造方法
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
一个类即使没有声明构造方法也会有默认的无参构造方法。但若手动添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参构造方法了。
特点
名字与类名相同。
没有返回值,但不能用 void 声明构造函数。
生成类的对象时自动执行,无需调用。
不能被 override(重写),但可以 overload(重载)。
参数
实参&形参
实参(实际参数,Arguments)
用于传递给函数/方法的参数,必须有确定的值。
形参(形式参数,Parameters)
用于定义函数/方法,接收实参,不需要有确定的值。
值传递&引用传递
值传递
方法接收的是实参值的拷贝,会创建副本。
引用传递
方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递(创建地址副本)。
可变长参数
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。
格式
method(String... args)
原理
Java 的可变参数编译后实际会被转换成一个数组。
注意
优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
OOP
面向对象和面向过程
面向对象:会先抽象出对象,然后用对象执行方法的方式解决问题。
面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
面向对象开发的程序一般更易维护、易复用、易扩展。
特征
封装
把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但提供一些可以被外界访问的方法来操作属性。
继承
使用已存在的类的定义作为基础建立新类,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
特点
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态
一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
特点
对象类型和引用类型之间具有继承(类)/实现(接口)的关系。
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定。
多态不能调用“只在子类存在但在父类不存在”的方法。
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
接口和抽象类
共同点
都不能被实例化。
都可以包含抽象方法。
都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别
接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
一个类只能继承一个类,但是可以实现多个接口。
接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义或被重新赋值。
深拷贝和浅拷贝、引用拷贝
浅拷贝
浅拷贝会在堆上创建一个新对象,但原对象内部的属性是引用类型,浅拷贝会直接复制内部对象的引用地址,也就是拷贝对象和原对象共用同一个内部对象。
深拷贝
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
Object
Object
Object 类是一个特殊的类,是所有类的父类。它主要提供了 11 个方法。
方法
public final native Class<?> getClass();
native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public native int hashCode();
native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
public boolean equals(Object obj);
用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException;
native 方法,用于创建并返回当前对象的一份拷贝。
native 方法,用于创建并返回当前对象的一份拷贝。
public String toString();
返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public final native void notify();
native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll();
native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException;
native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException;
多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
public final void wait() throws InterruptedException;
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
protected void finalize() throws Throwable { }
实例被垃圾回收器回收的时候触发的操作。
实例被垃圾回收器回收的时候触发的操作。
== 和 equals()
==
基本数据类型比较的是值。
引用数据类型比较的是对象的内存地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
没重写:比较的是对象的内存地址。重写:比较属性或自定义内容。自定义类推荐重写此方法。
hashCode()
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的。
hashCode() 和 equals() 都是用于比较两个对象是否相等。(自定义类推荐重写方法)
特点
如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
如果两个对象的hashCode 值相等并且 equals() 方法也返回 true,我们才认为这两个对象相等。
如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
String
不可变性
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
应用
String:操作少量的数据。
StringBuilder:单线程操作字符串缓冲区下操作大量数据。
StringBuffer:多线程操作字符串缓冲区下操作大量数据。
底层实现
Java 9 以前,底层由 char[] 实现。
Java 9 之后,底层由 byte[] 实现。
原因
Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String#intern
一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中。
常量折叠
常量折叠(Constant Folding),把常量表达式的值求出来作为常量嵌在最终生成的代码中的编译优化。
这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以折叠。
可折叠
基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量。
final 修饰的基本数据类型和字符串变量。
字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
注意
在 JDK9 当中,字符串相加 “+” 改为了用动态方法,不再影响性能了。
异常
java.lang.Throwable
Java 异常类顶级父类。
方法
String getMessage():返回异常发生时的简要描述。
String toString():返回异常发生时的详细信息。
String getLocalizedMessage():返回异常对象的本地化信息。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息。
Exception
程序本身可以处理的异常,可以通过 catch 来进行捕获。
分类
Checked Exception
受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就没办法通过编译。
Unchecked Exception
不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常。
常见
NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
Error
Error 属于程序无法处理的错误 ,不建议通过catch捕获 。
Error 发生时,Java 虚拟机(JVM)一般会选择线程终止。
异常捕获
try-catch-finally
try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
catch块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
注意
不要在 finally 语句块中使用 return!
当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行 finally 语句中的 return 之后,本地变量的值就变为 finally 语句中的 return 返回值。
当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行 finally 语句中的 return 之后,本地变量的值就变为 finally 语句中的 return 返回值。
finally 不是一定会执行
程序所在的线程死亡。
关闭 CPU。
try-with-resources
适用
任何实现 java.lang.AutoCloseable 或者 java.io.Closeable 的对象。
关闭资源和 finally 块的执行顺序
在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行。
注意
不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
抛出的异常信息一定要有意义。
建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出 NumberFormatException 而不是其父类 IllegalArgumentException。
使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
泛型(Generics)
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。
使用方式
泛型类
public class Generic<T> { private T key; }
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
泛型接口
public interface Generator<T> { public T method(); }
泛型方法
public static <E> void printArray( E[] inputArray ) { }
作用
使⽤泛型可在编译期间进⾏类型检测。
使⽤ Object 类型需要⼿动添加强制类型转换,降低代码可读性,提⾼出错概率。
泛型可以使⽤⾃限定类型,如 T extends Comparable 。
限制
泛型的限制⼀般是由泛型擦除机制导致的。擦除为 Object 后⽆法进⾏类型判断。
内容
只能声明不能实例化 T 类型变量。
泛型参数不能是基本类型。因为基本类型不是 Object ⼦类,应该⽤基本类型对应的引⽤类型代替。
不能实例化泛型参数的数组。擦除后为 Object 后⽆法进⾏类型判断。
不能实例化泛型数组。
泛型⽆法使⽤ Instance of 和 getClass() 进⾏类型判断。
不能实现两个不同泛型参数的同⼀接⼝,擦除后多个⽗类的桥⽅法将冲突。
不能使⽤ static 修饰泛型变量。
应用
自定义接口通用返回结果 CommonResult<T> 通过参数 T 可根据具体的返回类型动态指定结果的数据类型。
定义 Excel 处理类 ExcelUtil<T> 用于动态指定 Excel 导出的数据类型。
构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
擦除机制
类型擦除:编译期间,所有的泛型信息都会被擦掉。
Java 的泛型是伪泛型,因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是类型擦除 。
编译器会在编译期间会动态地将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx 。
泛型本质上还是编译器的⾏为,为了保证引⼊泛型机制但不创建新的类型,减少虚拟机的运⾏开销,编译器通过擦除将泛型类转化为⼀般类。
桥⽅法( Bridge Method )
⽤于继承泛型类时保证多态,编译器⾃动⽣成。
通配符
通配符可以允许类型参数变化,⽤来解决泛型⽆法协变的问题。
⽆界通配符
⽆界通配符可以接收任何泛型类型数据,⽤于实现不依赖于具体类型参数的简单⽅法,可以捕获参数类型并交由泛型⽅法进⾏处理。
通配符 ?和通配符 T
T 可以⽤于声明变量或常量⽽ ? 不⾏。
T ⼀般⽤于声明泛型类或⽅法,通配符 ? ⼀般⽤于泛型⽅法的调⽤代码和形参。
T 在编译期会被擦除为限定类型或 Object ,通配符 ? ⽤于捕获具体类型。
边界通配符
上边界通配符 extends 可以实现泛型的向上转型即传⼊的类型实参必须是指定类型的⼦类型。
下边界通配符 super 与上边界通配符 extends 刚好相反,它可以实现泛型的向下转型即传⼊的类型实参必须是指定类型的⽗类型。
? extends xxx 和 ? super xxx
使⽤ ? extends xxx 声明的泛型参数只能调⽤ get() ⽅法返回 xxx 类型,调⽤ set() 报错。
使⽤ ? super xxx 声明的泛型参数只能调⽤ set() ⽅法接收 xxx 类型,调⽤ get() 报错。
T extends xxx 和 ? extends xxx
T extends xxx ⽤于定义泛型类和⽅法,擦除后为 xxx 类型。
? extends xxx ⽤于声明⽅法形参,接收 xxx 和其⼦类型。
Class<?> 和 Class
直接使⽤ Class 的话会有⼀个类型警告,使⽤ Class<?> 则没有,因为 Class 是⼀个泛型类,接收原⽣类型会产⽣警告。
反射(Reflection)
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
获取类模版
.class、Class.forName()、instance.getClass()、xxxClassLoader.loadClass()
优点
可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
缺点
让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。
应用
动态代理的实现依赖反射。
注解(Annotation)
JDK 5 新特性,用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
本质
一个继承了Annotation 的特殊接口。
解析方法
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理。
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。
SPI vs API
SPI
SPI(Service Provider Interface)服务提供者的接口,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或替换服务实现并不需要修改调用方。
实现本质上是通过反射完成的
META-INF/services/
优点
能够大大地提高接口设计的灵活性。
缺点
需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
当多个 ServiceLoader 同时 load 时,会有并发问题。
应用
Spring 框架、数据库加载驱动、日志接口SLF4J (Simple Logging Facade for Java)、以及 Dubbo 的扩展实现等等。
API(Application Programming Interface)
API(Application Programming Interface)应用程序接口,对外提供功能服务的一个接口。
序列化
数据结构或对象和二进制字节流互相转换过程。
序列化
将数据结构或对象转换成二进制字节流的过程。
反序列化
将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
目的
通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
协议
常用协议
Hessian、Kryo、Protobuf、ProtoStuff,不推荐JDK自带(不支持跨语言调用、性能差、存在安全问题)。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
序列化协议对应于 TCP/IP 4 层模型的 表示层。
transient
对于不想进行序列化的变量,使用 transient 关键字修饰。
作用
阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
注意
transient 只能修饰变量,不能修饰类和方法。
transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。
static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
应用
对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化。
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化。
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
语法糖(Syntactic sugar)
编程语言为了方便程序员开发程序而设计的一种特殊语法,compile() 中有一个步骤就是调用 desugar()。
常见语法糖
switch 支持 String 与枚举、泛型、自动拆装箱、变长参数、枚举、内部类。
条件编译、断言、数值字面量、for-each、try-with-resources、lambda表达式等。
代理模式
使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
作用
扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
静态代理
定义
实现和应用角度
对目标对象的每个方法的增强都是手动完成的,非常不灵活且麻烦。
JVM 角度
静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
步骤
定义一个接口及其实现类。
创建一个代理类同样实现这个接口。
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
动态代理
定义
实现和应用角度
不需要针对每个目标类都单独创建一个代理类,并且也不需要必须实现接口,可以直接代理实现类(CGLIB 动态代理机制)。
JVM角度
动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
应用
日常开发使用较少,但在框架中使用非常多。
JDK 动态代理
JDK 动态代理的目标对象必须要有接口实现,也就是说:委托类必须要继承接口。
核心
InvocationHandler 接口
在 invoke() 方法中可以自定义处理逻辑。
Proxy 类
Proxy.newProxyInstance()
主要用来生成一个代理对象。
代理对象在调用方法时,实际会调用实现 InvocationHandler 接口的类的 invoke()方法。
步骤
定义一个接口及其实现类。
自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中会调用原生方法(被代理类的方法)并自定义一些处理逻辑。
通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象,再调用接口方法。
CGLIB 动态代理
CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,允许在运行时对字节码进行修改和动态生成。
原理
CGLIB 通过继承方式实现代理。
核心
MethodInterceptor 接口
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。
Enhancer 类
通过 Enhancer 类来动态获取被代理类,当代理类调用方法时,实际调用的是 MethodInterceptor 中的 intercept 方法。
步骤
手动添加 CGLIB(开源项目)相关依赖。
定义一个类。
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似。
通过 Enhancer 类的 create() 创建代理类,再调用接口方法。
应用
Spring 的 AOP 模块
如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
区别
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
效率上,JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
区别
灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
魔法类 Unsafe
依赖本地方法(Native Method)
native 关键字修饰,Java 中使用其他编程语言编写的方法。
初始化
类加载器、反射、VM参数
内存操作
在 Java 中不允许直接对内存进行操作,对象内存的分配和回收都是由 JVM 实现。但是在 Unsafe 中,提供接口可以直接进行内存操作。
释放
这种方式分配的内存属于堆外内存 ,是无法进行垃圾回收的,需要把这些内存当做一种资源去手动调用 freeMemory 方法进行释放,否则会产生内存泄漏。
通用的操作内存方式是在 try 中执行对内存的操作,最终在 finally 块中进行内存的释放。
内存屏障
CPU 为了防止代码进行重排序而提供的指令。
Java 8 中引入了一种锁的新机制:StampedLock,读写锁的一个改进版本。
loadFence()、storeFence()、fullFence()
对象操作
对象属性
put、get
对象实例化
常规对象实例化方式
new
非常规的实例化方式
allocateInstance
数组操作
arrayBaseOffset、arrayIndexScale
java.util.concurrent.atomic 包下的 AtomicIntegerArray
CAS 操作
比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。
三个操作数——内存位置、预期原值及新值。
JUC 包的并发工具类中大量地使用了 CAS 操作。
线程调度
park、unpark、monitorEnter、monitorExit、tryMonitorEnter
Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS)
Class 操作
类加载和静态变量的操作方法。
Lambda 表达式实现需要依赖 Unsafe 的 defineAnonymousClass 方法。
系统信息
addressSize、pageSize
集合
定义
Java 集合, 也叫作容器,主要是由两大接口派生而来。
Collection接口
用于存放单一元素。
子接口
List、Set 和 Queue。
Map 接口
用于存放键值对。
List
List(对付顺序的好帮手):存储的元素是有序的、可重复的。
ArrayList:Object[] 数组,性能好,线程不安全。
LinkedList:双向链表,不常用,线程不安全。
CopyOnWriteArrayList:写时复制COW,Object[] 数组,线程安全。
Vector:Object[] 数组,JDK1.5之前使用,线程安全。
Stack:继承自 Vector,是一个后进先出的栈,JDK1.5之前使用,线程安全。
Set
Set(注重独一无二的性质):存储的元素不可重复的。
HashSet(无序,唯一):基于 HashMap,线程不安全 。
LinkedHashSet:HashSet 的子类,基于 LinkedHashMap,线程不安全 。
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树),线程不安全。
Queue
Queue(实现排队功能的叫号机):按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
PriorityQueue:JDK1.5,二叉堆 + Object[] 数组来实现小顶堆,线程不安全。
ArrayDeque:JDK1.6,可扩容动态双向数组,基于可变长的数组和双指针来实现。
阻塞队列
BlockingQueue:阻塞队列,继承自 Queue。
ArrayBlockingQueue:JDK1.5,基于定长的数组,有界队列。创建时需指定容量,支持公平和非公平锁访问机制。
LinkedBlockingQueue:JDK1.5,基于单向链表,无界队列,近似无界。创建时指定容量或默认 Integer.MAX_VALUE,仅支持非公平锁访问机制。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
SynchronousQueue:JDK1.6,同步队列,一个不存储元素的阻塞队列。
DelayQueue:JDK1.8,基于PriorityQueue,一个支持延迟获取元素的阻塞队列。
TransferQueue:JDK1.7,一个支持更多操作的阻塞队列。
阻塞队列就是典型的生产者-消费者模型,调用 put、take、offfer、poll 等 API 即可实现多线程之间的生产和消费。
Map
Map(用 key 来搜索的专家):使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
HashMap:数组 + 链表(红黑树)组成的,线程不安全。
LinkedHashMap
继承自 HashMap,增加了一条双向链表,线程不安全。
accessOrder:访问顺序,属于构造参数。
LRU 缓存
(Least Recently Used,最近最少使用) 缓存,确保当存放的元素超过容器容量时,将最近最少访问的元素移除。
accessOrder 设置为 true 并重写 removeEldestEntry 方法(移除规则)。
Hashtable:数组(Hashtable 的主体)+ 链表组成的,线程安全。
TreeMap:红黑树(自平衡的排序二叉树),线程不安全。
ConcurrentHashMap:分段的数组 + 链表(红黑树),线程安全。
复杂度
O(1) 常数< O(logn) 对数 < O(n) 线性 < O(n2) < O(n3) < O(2n) 指数
ArrayList
头插 O(n);尾插:容量未达极限 O(1),达到极限 O(n);指定插:O(n);
头删 O(n);尾删 O(1);指定删:O(n);
LinkedList
头插/删 O(1);尾插/删:O(1);指定插/删:O(n);
RandomAccess 接口
一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。
由于 LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess 接口。
Comparable 和 Comparator
Java 中用于排序的接口,在实现类对象之间比较大小、排序等方面发挥了重要作用。
Comparable
java.lang 包,compareTo(Object obj) 方法用来排序。
方式
实现 Comparable 接口并重写 compareTo(Object obj) 方法。
Comparator
java.util 包,compare(Object obj1, Object obj2) 方法用来排序。
方式
new 一个 Comparator 比较器实例并重写其 compare(Object obj1, Object obj2) 方法。
Queue 与 Deque
Queue
单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
扩展了 Collection 的接口,因为容量问题而导致操作失败后处理方式的不同:一种在操作失败后会抛出异常,另一种则会返回特殊值。
Deque
双端队列,在队列的两端均可以插入或删除元素。
Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,失败处理方式与 Queue 相似。
Deque 还提供有 push() 和 pop() 等其他方法,可用于模拟栈。
注意事项
集合判空
使用 isEmpty() 方法,而不是 size()==0 的方式。
集合转 Map
toMap(),value为null,抛NPE。
集合遍历
不要在 foreach 循环里进行元素的 remove/add 操作。
remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
集合去重
利用 Set 元素唯一的特性,避免使用 List 的 contains()。
集合转数组
toArray(T[] array)
数组转集合
Arrays.asList()
不能修改
手动实现工具类
new ArrayList<>(Arrays.asList("a", "b", "c"))
Java8 Stream
Arrays.stream(myArray).collect(Collectors.toList())
Guava
不可变
ImmutableList.of("string", "elements")、ImmutableList.copyOf(aStringArray)
可变
Lists.newArrayList("or", "string", "elements")
Apache Commons Collections
CollectionUtils.addAll(list, str)
Java9 List.of()
List<Integer> list = List.of(array)
Synchronized 性能问题
Synchronized 锁升级后,性能不再是问题。
并发编程
进程 vs 线程
线程是进程划分成的更小的运行单位。
线程分类
绿色线程
JDK 1.2 之前,一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。
用户线程
由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
内核线程
由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
区别
用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。
线程模型:1:1、1:N、N:N
Java 在 Windows 和 Linux 是 1:N。
程序计数器、虚拟机栈和本地方法栈
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务。
本地方法栈则为虚拟机使用到的 Native 方法服务。
保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区
堆存放新创建的对象。
方法区存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并发与并行
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
同步和异步
同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步:调用在发出之后,不用等待返回结果,该调用直接返回。
多线程
为了能提高程序的执行效率提高程序运行速度
问题:内存泄漏、死锁、线程不安全等
线程安全
在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
线程类型
CPU 密集型和 IO 密集型。
线程生命周期和状态
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
线程上下文切换
线程切换需要保存以及恢复相应线程的上下文。
切换过程占用CPU。
线程死锁
多线程中一个或多个等待某个资源释放,而被无限期阻塞,导致程序不能正常终止。
条件
互斥条件;请求与保持条件;不剥夺条件;循环等待条件;
sleep() 方法和 wait() 方法
sleep() 方法没有释放锁,wait() 方法释放了锁 。
sleep()方法操作线程,wait()方法操作对象(对象锁)。
start() 方法和 run() 方法
new一个Thread并调用start(),分到时间片后,start()准备后自动调用run()。
真正多线程
直接调用run()就是main线程下的普通方法。
非多线程
JMM(Java 内存模型)
Java Memory Model
Java Memory Model
主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
CPU 缓存模型(CPU Cache):通常分为三层,分别叫 L1,L2,L3 Cache。
CPU 可以通过制定缓存一致协议(比如 MESI 协议)来解决内存缓存不一致性问题。
指令重排序
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。
编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。
Java源代码重排过程:编译器优化重排 —> 指令并行重排 —> 内存系统重排
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,多线程下可能引发问题。
内存屏障
Memory Barrier,或有时叫做内存栅栏(Memory Fence)
一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。
主内存:所有线程创建的实例对象都存放在主内存中。
本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。
同步操作:锁定(lock);解锁(unlock);read(读取);load(载入);use(使用);assign(赋值);store(存储);write(写入);
同步规则:8种,了解即可。
happens-before 原则
通过逻辑时钟来区分事件发生的前后顺序。
前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
常见规则
程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作。
解锁规则:解锁 happens-before 于加锁。
volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。
传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作。
如果两个操作不满足上述任意一个 happens-before 规则,JVM 就可以对这两个操作进行重排序。
并发编程重要特性
原子性
借助synchronized、各种 Lock 以及各种原子类实现原子性。
可见性
借助synchronized、volatile 以及各种 Lock 实现可见性。
有序性
volatile 关键字可以禁止指令进行重排序优化。
volatile 关键字
保证变量的可见性
标记JVM每次都要到主存中进行读取。
防止 JVM 的指令重排序
插入特定内存屏障
双重检验锁方式实现单例模式
保证变量的可见性,但不能保证对变量的操作是原子性的。
乐观锁和悲观锁
悲观锁
乐观锁
假设最好,提交时验证修改(CAS算法)
java.util.concurrent.atomic包下面的原子变量类
大量失败重试,导致CPU飙升。
多用于写比较少的情况(多读场景,竞争较少)
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。
CAS算法
Compare And Swap(比较与交换)
原子操作,底层依赖于一条 CPU 的原子指令。
三个操作数
V:要更新的变量值(Var)
E:预期值(Expected)
N:拟写入的新值(New)
ABA 问题;循环时间长开销大;只能保证一个共享变量的原子操作(AtomicReference对象原子性);
悲观锁
假设最坏,使用就会上锁
synchronized 和 ReentrantLock 等独占锁
大量阻塞,频繁切换上下文,死锁等
多用于写比较多的情况(多写场景,竞争激烈)
synchronized 关键字
早起属于重量级锁,Java 6 后轻量级锁
自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
偏向锁增加了 JVM 的复杂性,JDK15默认关闭,JDK18彻底废弃
修饰实例方法;修饰静态方法;修饰代码块;
通过获取对象监视器 monitor 实现。
只能是非公平锁。
ReentrantLock
实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
内部类 Sync 继承 AQS(AbstractQueuedSynchronizer)
内部类 Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类,默认非公平锁。
公平锁 : 锁被释放之后,先申请的线程先得到锁。
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。
synchronized 和 ReentrantLock都是可重入锁。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。
ReentrantLock 比 synchronized 多的高级功能
等待可中断;可实现公平锁;可实现选择性通知(锁可以绑定多个条件);
Condition接口:JDK1.5,实现多路通知进行线程调度,可选择性通知。
ReentrantReadWriteLock
使用少,JDK1.8后使用性能更好的读写锁 StampedLock。
锁状态
依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
可中断锁和不可中断锁
可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理,ReentrantLock。
不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理,synchronized。
共享锁和独占锁
共享锁:一把锁可以被多个线程同时获得。
独占锁:一把锁只能被一个线程获得。
写锁可以降级为读锁,但是读锁却不能升级为写锁。
StampedLock
JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Conditon。
提供了三种模式的读写控制模式:读锁、写锁和乐观读。一定条件下进行相互转换。
适合读多写少的业务场景。性能虽好,但使用麻烦,不建议使用高级性能。
基于 CLH 锁 实现的(与AQS一样),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。
ThreadLocal
提供线程局部变量,每个线程 Thread 拥有一份自己的副本变量(ThreadLocalMap),多个线程互不干扰。
ThreadLocalMap
类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
Key
ThreadLocal<?> k ,继承自WeakReference,弱引用
Value
代码中放入的值,强引用
Key被回收,Value仍然存在,容易出现内存泄漏。
Hash 算法
HASH_INCREMENT = 0x61c88647
斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
Hash 冲突
HashMap通过链表结构或红黑树解决。
ThreadLocalMap没有链表结构,采用。。。
过期Key数据清理方式
探测式清理(expungeStaleEntry()):以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理。
启发式清理(cleanSomeSlots()):Heuristically scan some cells looking for stale entries.
扩容机制
先探测式清理再扩容
InheritableThreadLocal
在异步场景下给子线程共享父线程中创建的线程副本数据。
有缺陷,推荐阿里开源的 TransmittableThreadLocal 组件。
内存泄露
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
线程池
为了减少每次获取资源的消耗,提高对资源的利用率。
创建线程池
通过 ThreadPoolExecutor 构造函数来创建(推荐)。
通过 Executor 框架来创建(不推荐)。
FixedThreadPool:返回一个固定线程数量的线程池。
SingleThreadExecutor: 返回一个只有一个线程的线程池。
CachedThreadPool: 返回一个可调整线程数量的线程池。
ScheduledThreadPool:返回一个延迟后运行或者定期执行任务的线程池。
弊端:最大Integer.MAX_VALUE,导致OOM。
线程池的饱和策略
ThreadPoolExecutor.AbortPolicy
抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy
任务回退给调用者,使用调用者的线程来执行任务。
ThreadPoolExecutor.DiscardPolicy
不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
将丢弃最早的未处理的任务请求。
阻塞队列
LinkedBlockingQueue(无界队列):FixedThreadPool 和 SingleThreadExector 。
SynchronousQueue(同步队列):CachedThreadPool 。
DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPool 和 SingleThreadScheduledExecutor 。
设定线程池的大小
CPU 密集型任务(N+1)
I/O 密集型任务(2N)
最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))
动态修改线程池的参数
美团:自定义队列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
Hippo4j:异步线程池框架
Dynamic TP:轻量级动态线程池
优先级任务线程池
使用 PriorityBlockingQueue (优先级阻塞队列)作为任务队列(workQueue 参数)
监测线程池运行状态
SpringBoot 中的 Actuator 组件。
利用 ThreadPoolExecutor 的相关 API 做监控。
坑儿
重复创建线程池的坑
Spring 内部线程池的坑,一定要手动自定义线程池,配置合理的参数
线程池和 ThreadLocal 共用的坑,可能会导致线程从ThreadLocal获取到的是旧值/脏数据。
解决:TransmittableThreadLocal(TTL)
Executor
Executor 框架是 Java5 之后引进来管理线程的框架,比Thread更好,易管理,效率好,避免this逃逸问题。
构成
任务(Runnable /Callable)
任务的执行(Executor)
异步计算的结果(Future)
对比
Runnable vs Callable
Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。
execute() vs submit()
execute()无返回值,submit()有返回值。
shutdown() vs shutdownNow()
shutdown()关闭线程池,线程池的状态变为 SHUTDOWN。
shutdownNow()关闭线程池,线程池的状态变为 STOP。
isTerminated() vs isShutdown()
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true。
Future
异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。
功能
取消任务;判断任务是否被取消;判断任务是否已经执行完成;获取任务执行结果;
FutureTask
Future 接口的基本实现,用来封装 Callable 和 Runnable。
FutureTask 相当于对 Callable 进行了封装,管理着任务执行的情况,存储了 Callable 的 call 方法的任务执行结果。
CompletableFuture
Future 存在一些缺陷,比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。
Java 8 引入 CompletableFuture 类解决 Future 的缺陷。
CompletableFuture 同时实现了 Future 和 CompletionStage 接口。
CompletionStage 接口描述了一个异步计算的阶段。
常用方法
创建
通过 new 关键字。
基于 CompletableFuture 自带的静态工厂方法:runAsync()、supplyAsync() 。
处理异步结算的结果
thenApply()、thenAccept()、thenRun()、whenComplete()
异常处理
handle()、exceptionally()、completeExceptionally()
组合
thenCompose()、thenCombine()
并行
allOf()、anyOf()
建议
建议使用自定义的线程池,而非默认的 ForkJoinPool.commonPool()
尽量避免使用 get()
如果必须要使用的话,需要添加超时时间
正确进行异常处理
合理组合多个异步任务
AQS
抽象队列同步器(AbstractQueuedSynchronizer):主要用来构建锁和同步器。
原理:用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(即不存在队列实例,仅存在结点之间的关联关系)。
资源共享方式
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)
自定义同步器
使用了模板方法模式,自定义同步器时需要重写 AQS 提供的钩子方法
钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰。
Semaphore
构造(信号量)可以用来控制同时访问特定资源的线程数量。
原理:共享锁的一种实现,默认构造 AQS 的 state 值为 permits。
场景:限流,仅限单机,实际推荐Redis + Lua 来做。
CountDownLatch
允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
一次性。
原理:共享锁的一种实现,默认构造 AQS 的 state 值为 count。
场景:多个无顺序依赖任务,需要统一返回结果。-- 阻塞,条件,释放
替代:CompletableFuture,Java 8
CyclicBarrier
与 CountDownLatch 非常类似,但更加复杂和强大。
原理:基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的实现。
虚拟线程(Virtual Thread)
Java 21 重量级更新
由 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。
平台线程(Platform Thread):在引入虚拟线程之前,JVM 调度程序通过平台线程(载体线程)来管理虚拟线程。
主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统除外。
优缺点
非常轻量级;简化异步编程;减少资源开销;
不适用计算密集型任务;依赖于语言或库支持;
创建
Thread.startVirtualThread()、Thread.ofVirtual()、ThreadFactory、Executors.newVirtualThreadPerTaskExecutor()
在密集 IO 的场景下,比平台线程性能更好。
并发容器总结
ConcurrentHashMap
线程安全的 HashMap
另一种线程安全的 HashMap实现:Collections.synchronizedMap()
CopyOnWriteArrayList
程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。
在 JDK1.5 之前,如果想要使用并发安全的 List 只能选择 Vector。
ConcurrentLinkedQueue
高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
BlockingQueue
这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
ArrayBlockingQueue
BlockingQueue 接口的有界队列实现类,底层采用数组来实现。
LinkedBlockingQueue
底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用。
PriorityBlockingQueue
支持优先级的无界阻塞队列。
ConcurrentSkipListMap
跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
跳表是一种可以用来快速查找的数据结构,有点类似于平衡树,是一种利用空间换时间的算法。
Atomic 原子类
具有原子/原子操作特征的类,都存放在java.util.concurrent.atomic下。
基本类型
AtomicInteger:整型原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
数组类型
AtomicIntegerArray:整型数组原子类
AtomicLongArray:长整型数组原子类
AtomicReferenceArray:引用类型数组原子类
引用类型
AtomicReference:引用类型原子类
AtomicMarkableReference:原子更新带有标记的引用类型。
AtomicStampedReference:原子更新带有版本号的引用类型。
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整型字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
IO
定义
IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储的过程即输出。
数据传输过程类似于水流,因此称为 IO 流。
IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
抽象类基类
InputStream/Reader/OutputStream/Writer
字节流和字符流
必要性
字符流是由 Java 虚拟机将字节转换得到的,这个过程比较耗时。
如果不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
字节流
音频文件、图片等媒体文件用字节流
InputStream(字节输入流)
用于从源头(通常是文件)读取数据(字节信息)到内存中
FileInputStream
读取文件单字节数据
DataInputStream
读取指定类型数据,不能单独使用
ObjectInputStream
读取 Java 对象(反序列化),不能单独使用
OutputStream(字节输出流)
用于将数据(字节信息)写入到目的地(通常是文件)
FileOutputStream
输出单字节数据到文件
DataOutputStream
写入指定类型数据,不能单独使用
ObjectOutputStream
写入 Java 对象(反序列化),不能单独使用
字符流
涉及到字符的话使用字符流
字符流默认采用的是 Unicode 编码
Unicode:任何字符都占 2 个字节
Utf8:英文占 1 字节,中文占 3 字节
Gbk:英文占 1 字节,中文占 2 字节
Reader(字符输入流)
用于从源头(通常是文件)读取数据(字符信息)到内存中
InputStreamReader
字节流转换为字符流的桥梁
FileReader
基于InputStreamReader的封装
Writer(字符输出流)
用于将数据(字符信息)写入到目的地(通常是文件)
OutputStreamWriter
字符流转换为字节流的桥梁
FileWriter
基于OutputStreamWriter的封装
字节缓冲流
IO 操作很消耗性能,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,避免频繁操作来提高效率。
原理
装饰器模式
内部维护了一个字节数组作为缓冲区,默认大小为 8192 字节
BufferedInputStream(字节缓冲输入流)、BufferedOutputStream(字节缓冲输出流)
ZipInputStream 和 ZipOutputStream 可以分别增强字节缓冲流
字符缓冲流
类似于字节缓冲流,内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
打印流
System.out 实际是用于获取一个 PrintStream 对象
PrintStream(字节打印流)、PrintWriter (字符打印流)
随机访问流
随意跳转到文件的任意位置进行读写
RandomAccessFile
应用
实现大文件的断点续传
RandomAccessFile 的实现依赖于 FileDescriptor (文件描述符) 和 FileChannel (内存映射文件)。
IO设计模式
装饰器(Decorator)模式
可以在不改变原有对象的情况下拓展其功能。
适配器(Adapter Pattern)模式
用于接口互不兼容的类的协调工作
概念
适配者(Adaptee)
存在被适配的对象或者类
适配器(Adapter)
作用于适配者的对象或者类
分类
类适配器
使用继承关系来实现
对象适配器
使用组合关系来实现
工厂模式
用于创建对象
NIO 中大量用到了工厂模式
观察者模式
NIO 中的文件目录监听服务
IO模型
IO 操作依赖内核空间的能力,应用程序只是IO调用,具体执行是操作系统的内核。
同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O
BIO (Blocking I/O)
属于同步阻塞 IO 模型 。
应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
客户端连接数量不高则可用,但十万甚至百万级连接数则不可用。
NIO (Non-blocking/New I/O)
属于 I/O 多路复用模型或同步非阻塞 IO 模型,Java 1.4 引入
同步非阻塞 IO 模型
应用程序会一直发起 read 调用,只有最后的read会一直阻塞,直到内核把数据拷贝到用户空间。
调用轮询数据,十分消耗 CPU 资源。
I/O 多路复用模型
应用程序先查询内核数据是否准备就绪,就绪后再发起read调用,调用过程中还是阻塞的。
性能优势主要体现在高并发和高延迟的网络环境下。
Buffer(缓冲区)
NIO 读写数据都是通过缓冲区进行操作的。
变量
容量(capacity)、界限(limit)、位置(position)、标记(mark)
0 <= mark <= position <= limit <= capacity
模式
读/写模式分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。
默认写模式,调用 flip() 切换到读模式。调用 clear() 或者 compact() 切换到写模式。切换后 position 都会归零。
读模式
limit 等于 Buffer 中实际写入的数据大小。
写模式
limit 代表最多能写入的数据,一般等于 capacity。
Channel(通道)
Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。
read :读取 Channel 数据并写入到 Buffer 中。
write :将 Buffer 中的数据写入到 Channel 中。
选择器/多路复用器 ( Selector )
允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。
原理
通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。
JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。
事件类型
SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel。
SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel。
SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。
Selector是抽象类,可以通过调用此类的 open() 静态方法来创建 Selector 实例。
NIO 零拷贝
执行 IO 操作时,避免 CPU 存储区域之间的数据复制,从而减少上下文切换以及 CPU 的拷贝时间来提升性能。
应用
ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目
常见实现技术
mmap+write、sendfile和 sendfile + DMA gather copy 。
Java支持
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷⻉⽅式的提供的⼀种实现。
FileChannel 的transferTo()/transferFrom()是 NIO 基于发送文件(sendfile)这种零拷贝方式的提供的一种实现。
不建议直接使用原生 NIO,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。
AIO (Asynchronous I/O)
属于异步 IO 模型,Java 7 引入,也就是 NIO 2
基于事件和回调机制实现的
JVM
Java内存区域
运行时数据区域
概要
JDK 1.7
JDK 1.8
线程私有的
程序计数器、虚拟机栈、本地方法栈
线程共享的
堆、方法区、直接内存 (非运行时数据区的一部分)
程序计数器
一块较小的内存空间,作为当前线程所执行的字节码的行号指示器。
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,以便切换回来后继续执行。
唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期与线程保持一致。
Java 虚拟机栈
JVM 运行时数据区域的一个核心,生命周期与线程保持一致。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
拥有先进后出的数据结构,只支持出栈和入栈两种操作。
局部变量表
主要存放编译期可知的各种数据类型、对象引用。
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。
计算过程中产生的临时变量也会放在操作数栈中。
动态链接
主要服务一个方法需要调用其他方法的场景。
作用
为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。
StackOverFlowError
栈的内存大小不允许动态扩展,栈深度超过最大深度
OutOfMemoryError
虚拟机在动态扩展栈时无法申请到足够的内存空间
本地方法栈
只为虚拟机使用到的 Native 方法服务的Java虚拟机栈。
堆
最大一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例。
注意:随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,并不是所有对象都在堆中分配,JDK 1.7 默认开启逃逸分析。
垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
结构
新生代和老年代
Eden、Survivor、Old
JDK 1.7
新生代(Young Generation)
Eden、S0、S1
老年代(Old Generation)
Tenured
永久代(Permanent Generation)
PermGen
JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。
永久代替换为元空间原因
永久代受 JVM 固定大小限制,元空间使用的本地内存,溢出几率小。
存放类的元数据,加载类数量不由 MaxPermSize 控制,而由系统空间控制,可加载更多类。
JDK8 合并 HotSpot 和 JRockit 的代码,JRockit 没有永久代。
元空间不指定大小时,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
对象在 Eden 区域分配,首次GC后若存活则进入 S0 或 S1,并且对象的年龄还会加 1,年龄超过设置(默认15)则晋升到老年代中。
OutOfMemoryError
GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。
MetaSpace:当元空间溢出时,就会引发此错误。
方法区
JVM 运行时数据区域的一块逻辑区域。
类加载时,存储类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区和永久代以及元空间关系
类似 Java 中接口和类的关系
方法区看作接口,永久代以及元空间为不同实现。
运行时常量池
常量池表(Constant Pool Table)
用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)
字面量包括整数、浮点数和字符串字面量。
符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
常量池表会在类加载后存放到方法区的运行时常量池中。
运行时常量池是方法区的一部分,受到方法区内存的限制,也会抛出 OutOfMemoryError 错误。
字符串常量池
JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
原理
固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 将字符串常量池移动到堆中原因
永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。
Java 程序中通常会有大量的被创建的字符串等待回收,放在堆中,能更高效回收。
直接内存
一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
不属于虚拟机。
堆外内存
把内存对象分配在堆外的内存,这些内存直接受操作系统管理(而不是虚拟机),一定程度上减少垃圾回收对应用程序造成的影响。
HotSpot 虚拟机
对象的创建过程
类加载检查
检查类的符号引用是否能在常量池中定位,再检查类是否已被加载过、解析和初始化过,如没有就需要执行类加载过程。
分配内存
虚拟机将为新生对象分配内存。
对象所需的内存大小在类加载完成后便可确定。
方式
指针碰撞
适用场合:堆内存规整(即没有内存碎片)的情况下。
原理:内存用过和没用过的分两边,中间一个分界指针,分配时,向没用过区域移动指针即可。
使用该分配方式的 GC 收集器:Serial, ParNew
空闲列表
适用场合:堆内存不规整的情况下。
原理:虚拟机维护了会记录哪些内存块是可用的列表,分配时,更新列表即可。
使用该分配方式的 GC 收集器:CMS
Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩")。
内存分配并发问题
虚拟机采用两种方式来保证线程安全。
CAS+失败重试: CAS 是乐观锁的一种实现方式。
TLAB:首先在 TLAB 分配,当 TLAB 的内存不够时,再采用上述的 CAS 进行内存分配。
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
目的:保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置。
例如:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。
执行 init 方法
初始化后,一个真正可用的对象才算完全产生出来。
对象的内存布局
对象头
用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)。
类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充
不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
原因:虚拟机要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。
对象的访问定位
Java 程序通过栈上的 reference 数据来操作堆上的具体对象,对象的访问方式由虚拟机实现而定。
句柄
Java 堆分配句柄池,reference 存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
优势:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
直接指针
使用直接指针访问,reference 中存储的直接就是对象的地址。
优势:速度快,它节省了一次指针定位的时间开销。
JVM垃圾回收
内存分配和回收原则
对象优先在 Eden 区分配
新生代无足够空间时,虚拟机通过 分配担保机制 把新生代的对象提前转移到老年代中。
大对象直接进入老年代
优化策略:减少新生代的垃圾回收频率和成本。
长期存活的对象将进入老年代
对象年龄(Age)计数器,每次GC,年龄就增加1,年龄超过设置(默认15)则晋升到老年代中。
主要进行 GC 的区域
部分收集 (Partial GC)
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
空间分配担保
为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
死亡对象判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
给对象中添加一个引用计数器,引用+1,引用失效-1,0代表无引用。
简单高效,但使用少,原因:很难解决对象之间循环引用的问题。
可达性分析算法
以 “GC Roots” 的对象为起点向下搜索,节点所走过的路径称为引用链,当对象到 GC Roots 没有任何引用链相连则证明此对象不可达。
可以作为 GC Roots 的对象
虚拟机栈(栈帧中的局部变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
JNI(Java Native Interface)引用的对象
不可达对象在死亡前,仍需要经历两次标记才能真正死亡被回收:finalize 方法和队列标记。
引用类型总结
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
强引用(StrongReference)
我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象。
大部分引用实际上都是强引用,这是使用最普遍的引用。
软引用(SoftReference)
使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收。
可用来实现内存敏感的高速缓存。
可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
弱引用(WeakReference)
使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收。
弱引用与软引用的区别:只具有弱引用的对象拥有更短暂的生命周期。
虚引用(PhantomReference)
虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知。
主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别:虚引用必须和引用队列(ReferenceQueue)联合使用。
除了强引用外,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多。
废弃常量
没有任何对象引用的常量就是废弃常量,如果这时发生内存回收的话而且有必要的话,废弃常量就会被系统清理。
无用的类
条件
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足条件的无用的类进行回收,而不是必然被回收。
垃圾收集算法
标记-清除(Mark-and-Sweep)算法
首先标记(Mark)出所有不需要回收的对象,在标记完成后统一清除(Sweep)掉所有没有被标记的对象。
最基础的收集算法,后续的算法都是对其不足进行改进得到。
问题
效率问题:标记和清除两个过程效率都不高。
空间问题:标记清除后会产生大量不连续的内存碎片。
复制(Copying)算法
将内存分为大小相同的两块A和B,只使用A,当A使用完后,就将还存活的对象复制到B,然后再把A一次清理掉。
问题
可用内存变小:可用内存缩小为原来的一半。
不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记-整理(Mark-and-Compact)算法
标记过程与“标记-清除”算法一样,但后续是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
分代收集算法
根据对象存活周期的不同将内存分为几块(新生代和老年代),以便根据各个年代的特点选择合适的垃圾收集算法。
当前虚拟机的垃圾收集都采用分代收集算法。
垃圾收集器
没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。
JDK 默认垃圾收集器
JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
JDK 9 ~ JDK20: G1
Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。
单线程收集器,只会使用一条线程去完成工作同时,必须暂停其他所有的工作线程( "Stop The World" ),直到它工作结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
优点:简单而高效(与其他收集器的单线程相比),运行在 Client 模式下的虚拟机的不错选择。
ParNew 收集器
Serial 收集器的多线程版本,运行在 Server 模式下的虚拟机的首选。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Scavenge 收集器
几乎和 ParNew 收集器一样,但其关注点是吞吐量(高效率的利用 CPU),其它关注更多的是用户线程的停顿时间(提高用户体验)。
吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
新生代采用标记-复制算法,老年代采用标记-整理算法。
JDK1.8 默认使用的垃圾收集器是 Parallel Scavenge + Parallel Old。
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。
用途
在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备方案。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器
以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
步骤
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快。
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。
重新标记: 为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优点
并发收集、低停顿。
缺点
对 CPU 资源敏感。
无法处理浮动垃圾。
使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
从 JDK9 开始,CMS 收集器已被弃用。
G1(Garbage-First) 收集器
面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
特点
并行与并发、分代收集、空间整合、可预测的停顿
步骤
初始标记、并发标记、最终标记、筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。
ZGC 在 Java11 中引入,处于试验阶段,在 Java15 已经可以正式使用了,最大支持 16TB 的堆内存。但默认的垃圾回收器依然是 G1。
类文件结构
魔数(Magic Number)
每个 Class 文件的头 4 个字节称为魔数(Magic Number)。
唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
Java 规范规定魔数为固定值:0xCAFEBABE。非此值,Java 虚拟机将拒绝加载它。
Class 文件版本号(Minor&Major Version)
魔数后的四个字节存储的是 Class 文件的版本号:第 5 ~ 6 个字节是次版本号,第 7 ~ 8 个字节是主版本号。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。可以使用 javap -v 命令来快速查看 Class 文件的版本号信息。
常量池(Constant Pool)
主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1。
主要存放两大常量:字面量和符号引用。
字面量
接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
符号引用
属于编译原理方面的概念。包括 类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 这三类常量。
访问标志(Access Flags)
在常量池之后,紧接着的两个字节代表访问标志。
用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。
字段表集合(Fields)
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
方法表集合(Methods)
方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
属性表集合(Attributes)
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
类加载过程
类的生命周期
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
验证、准备和解析这三个阶段可以统称为连接(Linking)。
类加载过程
系统加载 Class 类型的文件主要三步:加载->连接->初始化。
连接过程又可分为三步:验证->准备->解析。
加载
通过全类名获取定义此类的二进制字节流。
将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
验证
确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求。
组成
文件格式验证(Class 文件格式检查)
元数据验证(字节码语义检查)
字节码验证(程序语义检查)
符号引用验证(类的正确性检查)
准备
正式为类变量分配内存并设置类变量初始值的阶段,在方法区中分配。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
初始化
执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步。
必须对类进行初始化的6种情况
当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时。
使用 java.lang.reflect 包的方法对类进行反射调用时。
初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类)。
MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
就必须先使用 findStaticVarHandle 来初始化要调用的类。
就必须先使用 findStaticVarHandle 来初始化要调用的类。
当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时。
注意
<clinit> ()方法是编译之后自动生成的。
<clinit> () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。
类卸载
卸载类即该类的 Class 对象被 GC。
要求
该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
该类没有在其他任何地方被引用。
该类的类加载器的实例已被 GC。
类加载器
定义
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
每个 Java 类都有一个引用指向加载它的 ClassLoader。
数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
作用
加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。
除了加载类,还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。
加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。
类加载器
BootstrapClassLoader(启动类加载器)
最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级。
作用
加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 jar 包和类)及被 -Xbootclasspath 参数指定的路径下的所有类。
拓展
rt.jar:rt 代表“RunTime”,rt.jar是 Java 基础类库,常用内置库 java.xxx.*都在里面。
ExtensionClassLoader(扩展类加载器)
加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。
AppClassLoader(应用程序类加载器)
面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
自定义类加载器
需要继承 ClassLoader抽象类。
重写
loadClass(String name, boolean resolve) 方法
打破双亲委派模型
findClass(String name)方法
建议,不想打破双亲委派模型
线程上下文类加载器(ThreadContextClassLoader)
将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。
双亲委派模型
定义
ClassLoader 类使用委托模型来搜索类和资源。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。
面向对象编程设计原则:组合优于继承,多用组合少用继承。(非常经典)
执行流程
在类加载的时候,系统会首先判断当前类是否被加载过。
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。
拓展
JVM 判定两个 Java 类是否相同的具体规则
类的全名和加载此类的类加载器都一致。
优点
保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。
打破模型
自定义类加载器并重写 loadClass(String name, boolean resolve) 方法。
单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
应用
Tomcat 服务器为了能够优先加载 Web 应用目录下的类。
SPI 接口的实现(如com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,需要先加载。
JVM参数
堆内存相关
显式指定堆内存–Xms和-Xmx
-Xms<heap size>[unit]
-Xmx<heap size>[unit]
-Xmx<heap size>[unit]
heap size 表示要初始化内存的具体大小。
unit 表示要初始化内存的单位。单位为 “ g” (GB)、“ m”(MB)、“ k”(KB)。
显式新生代内存(Young Generation)
默认情况下,YG 的最小大小为 1310 MB,最大大小为 无限制。
方式
通过-XX:NewSize和-XX:MaxNewSize指定。
通过-Xmn<young size>[unit]指定。
通过 -XX:NewRatio=<int> 来设置老年代与新生代内存的比值。
GC调优策略
最大限度将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC。
显式指定永久代/元空间的大小
永久代
通过-XX:PermSize=N和-XX:MaxPermSize=N指定。
元空间
通过-XX:MetaspaceSize=N和-XX:MaxMetaspaceSize=N指定。
注意
Metaspace 的初始容量并不是 -XX:MetaspaceSize 设置,对于64位 JVM 来说,初始容量都是 21807104(约 20.8m)。
MetaspaceSize 表示 Metaspace 使用过程中触发 Full GC 的阈值,只对触发起作用。
垃圾收集相关
垃圾回收器
通过-XX:+Use<垃圾回收器标识>指定。
串行垃圾收集器(SerialGC)、并行垃圾收集器(ParallelGC)、CMS 垃圾收集器(ParNewGC)、G1 垃圾收集器(G1GC)
GC 日志记录
通过-XX:+Print<打印内容标识>指定。
# 必选
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
处理 OOM
JVM 提供了一些参数,将堆内存转储到一个物理文件中,以后可以用来查找泄漏。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
其他
- -server : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM
- -XX:+UseStringDeduplication : Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 char [] 数组来优化堆内存。
- -XX:+UseLWPSynchronization: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。
- -XX:LargePageSizeInBytes: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。
- -XX:MaxHeapFreeRatio : 设置 GC 后, 堆空闲的最大百分比,以避免收缩。
- -XX:SurvivorRatio : eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6 设置每个 survivor 和 eden 之间的比例为 1:6。
- -XX:+UseLargePages : 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。
- -XX:+UseStringCache : 启用 String 池中可用的常用分配字符串的缓存。
- -XX:+UseCompressedStrings : 对 String 对象使用 byte [] 类型,该类型可以用纯 ASCII 格式表示。
- -XX:+OptimizeStringConcat : 它尽可能优化字符串串联操作。
JDK工具
命令行工具
jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。
jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据。
jinfo (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息。
jmap (Memory Map for Java) : 生成堆转储快照。
jhat (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。
jstack (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
可视化分析工具
JConsole:Java 监视与管理控制台
Visual VM:多合一故障处理工具
JVM问题及调优案例
新特性
Java 8
Oracle 于 2014 发布了 Java8(jdk1.8),使用最多的 jdk 版本。
Interface 的方法可以用 default 或 static 修饰,为了解决接口的修改与现有的实现不兼容的问题。
functional interface 函数式接口,也称 SAM 接口,即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口。
Lambda 表达式,最重要新特性。
Stream,java.util.stream 包下,可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。
Optional,防止 NPE 问题。
Date-Time API,对java.util.Date强有力的补充,解决了 Date 类的大部分痛点。
《Java8 指南》中文翻译
Java 9
发布于 2017 年 9 月 21 日,最重要的改动是 Java 平台模块系统的引入。
JShell,是 Java 9 新增的一个实用工具。为 Java 提供了类似于 Python 的实时命令行交互工具。
模块系统,是Jigsaw Project的一部分,把模块化开发实践引入到了 Java 平台中,可以让我们的代码可重用性更好!
G1 成为默认垃圾回收器。
快速创建不可变集合,增加了List.of()、Set.of()、Map.of() 和 Map.ofEntries()等工厂方法来创建不可变集合。
String 存储结构优化,Java 8 及之前的版本,String 一直是用 char[] 存储。在 Java 9 之后,String 的实现改用 byte[] 数组存储字符串,节省了空间。
接口私有方法,接口的使用就更加灵活了,有点像是一个简化版的抽象类。
try-with-resources 增强,在 try-with-resources 语句中可以使用 effectively-final 变量。
Stream & Optional 增强
Stream 中增加了新的方法 ofNullable()、dropWhile()、takeWhile() 以及 iterate() 方法的重载方法。
Optional 类中新增了 ifPresentOrElse()、or() 和 stream() 等方法。
进程 API,增加了 java.lang.ProcessHandle 接口来实现对原生进程进行管理,尤其适合于管理长时间运行的进程。
响应式流 ( Reactive Streams )
变量句柄,是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等。
平台日志 API 改进,Java 9 允许为 JDK 和应用配置同样的日志实现。
CompletableFuture类增强,新增了几个新的方法(completeAsync ,orTimeout 等)。
I/O 流的新特性,增加了新的方法来读取和复制 InputStream 中包含的数据。
改进应用的安全性能,Java 9 新增了 4 个 SHA- 3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。
Java 10
发布于 2018 年 3 月 20 日,最知名的特性应该是 var 关键字(局部变量类型推断)的引入。
局部变量类型推断(var),var 并不会改变 Java 是一门静态类型语言的事实,编译器负责推断出类型。
垃圾回收器接口,通过引入一套纯净的垃圾收集器接口来将不同垃圾收集器的源代码分隔开。
G1 并行 Full GC,为了最大限度地减少 Full GC 造成的应用停顿的影响。
集合增强,List,Set,Map 提供了静态方法copyOf()返回入参集合的一个不可变拷贝。
Optional 增强,新增了orElseThrow()方法来在没有值时抛出指定的异常。
应用程序类数据共享(扩展 CDS 功能),允许应用类放置在共享存档中。
实验性的基于 Java 的 JIT 编译器、线程-局部管控、备用存储装置上的堆分配等
Java 11
于 2018 年 9 月 25 日正式发布,这是很重要的一个版本!这是据 Java 8 以后支持的首个长期版本,持续至 2026 年 9 月。
HTTP Client 标准化,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。
String 增强,增加了一系列的字符串处理方法。
Optional 增强,新增了isEmpty()方法来判断指定的 Optional 对象是否为空。
ZGC(可伸缩低延迟垃圾收集器),是一个可伸缩的、低延迟的垃圾收集器。处在实验阶段,只支持 Linux/x64 平台。
Lambda 参数的局部变量语法,允许开发者在 Lambda 表达式中使用 var 进行参数声明。
启动单文件源代码程序,允许使用 Java 解释器直接执行 Java 源代码。
新的垃圾回收器 Epsilon、低开销的 Heap Profiling、TLS1.3 协议、飞行记录器(Java Flight Recorder) 等。
Java 12
String 增强,增加了两个的字符串处理方法 indent() 和 transform() 。
Files 增强(文件比较),添加了方法( mismatch() )来比较两个文件。
数字格式化工具类,NumberFormat 新增了对复杂的数字进行格式化的支持。
Shenandoah GC,Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等。落地可能性大。
G1 收集器优化,默认的垃圾收集器 G1 带来了两项更新:可中止的混合收集集合 和 及时返回未使用的已分配内存。
增强 Switch,使用类似 lambda 语法条件匹配成功后的执行块,不需要多写 break 。
instanceof 模式匹配,在判断是否属于具体的类型同时完成转换。
Java 13
增强 ZGC(释放未使用内存),将向操作系统返回被标识为长时间未使用的页面,这样它们将可以被其他进程重用。
SocketAPI 重构,将 Socket API 的底层进行了重写,在 Java 13 中是默认使用新的 Socket 实现。
FileSystems,添加了以下三种新方法,以便更容易地使用将文件内容视为文件系统的文件系统提供程序。
动态 CDS 存档,允许在 Java 应用程序执行结束时动态进行类归档。
预览新特性
文本块,支持两个 """ 符号中间的任何内容都会被解释为字符串的一部分,包括换行符。
增强 Switch(引入 yield 关键字到 Switch 中)
Java 14
空指针异常精准提示,通过 JVM 中添加参数,可以在空指针异常中获取更为详细的调用信息,更快的定位和解决问题。
转正
switch 的增强。
预览新特性
record 关键字,record 关键字可以简化 数据类(一个 Java 类一旦实例化就不能再修改)的定义方式。
文本块,引入了两个新的转义字符。
instanceof 增强。
从 Java11 引入的 ZGC 作为继 G1 过后的下一代 GC 算法,从支持 Linux 平台到 Java14 开始支持 MacOS 和 Windows。
移除了 CMS(Concurrent Mark Sweep) 垃圾收集器(功成而退)。
新增了 jpackage 工具,标配将应用打成 jar 包外,还支持不同平台的特性包。
Java 15
CharSequence,接口添加了一个默认方法 isEmpty() 来判断字符序列为空。
TreeMap,新引入了5个方法。
EdDSA(数字签名算法),新加入了一个安全性和性能都更强的基于 Edwards-Curve Digital Signature Algorithm (EdDSA)实现的数字签名算法。
隐藏类(Hidden Classes),是为框架(frameworks)所设计的,隐藏类不能直接被其他类的字节码使用,只能在运行时生成类并通过反射间接使用它们。
转正
ZGC,但默认的垃圾回收器依然是 G1。
文本块。
预览新特性
密封类,可以对继承或者实现它们的类进行限制,这样这个类就只能被指定的类继承。
instanceof 模式匹配。
Nashorn JavaScript 引擎彻底移除、DatagramSocket API 重构、禁用和废弃偏向锁(Biased Locking)等。
Java 16
在 2021 年 3 月 16 日正式发布,非长期支持(LTS)版本。
启用 C++ 14 语言特性,允许在 JDK 的 C++ 源代码中使用 C++14 语言特性,并提供在 HotSpot 代码中可以使用哪些特性的具体指导。
ZGC 并发线程堆栈处理,消除 ZGC 垃圾收集器中最后一个延迟源可以极大地提高应用程序的性能和效率。
弹性元空间,可将未使用的 HotSpot 类元数据(即元空间,metaspace)内存更快速地返回到操作系统,从而减少元空间的占用空间。
对基于值的类发出警告。
默认强封装 JDK 内部元素,默认强封装 JDK 的所有内部元素,但关键内部 API(例如 sun.misc.Unsafe)除外。
转正
打包工具。
instanceof 模式匹配。
记录类型。
孵化
向量 API(第一次孵化)。
外部内存访问 API(第三次孵化)。
外部链接器 API。
预览新特性
密封类
Unix-Domain 套接字通道、从 Mercurial 迁移到 Git、迁移到 GitHub、移植 Alpine Linux、Windows/AArch64 移植 等。
Java 17
在 2021 年 9 月 14 日正式发布,是一个长期支持(LTS)版本,到 2029 年 9 月份。Spring 6.x 和 Spring Boot 3.x 最低支持的就是 Java 17。
增强的伪随机数生成器,增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。
弃用/删除
弃用 Applet API,用于编写在 Web 浏览器端运行的 Java 小程序。
删除 远程方法调用激活机制。
删除 实验性的 AOT 和 JIT 编译器。
弃用 安全管理器。
转正
密封类。
孵化
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。
向量 API(第二次孵化)。
预览新特性
switch 的类型匹配,增加了类型匹配自动转换功能。
Java 18
在 2022 年 3 月 22 日正式发布,非长期支持版本。新特性少了很多。
默认字符集为 UTF-8,JDK 终于将 UTF-8 设置为默认字符集。
简易的 Web 服务器,可以使用 jwebserver 命令启动一个简易的静态 Web 服务器。
优化 Java API 文档中的代码片段,可以通过 @snippet 标签来引入代码段。
使用方法句柄重新实现反射核心,改进了 java.lang.reflect.Method、Constructor 的实现逻辑,使之性能更好,速度更快。
孵化
向量 API(第三次孵化)。
Foreign Function & Memory API(第二次孵化)。
互联网地址解析 SPI,定义了一个全新的 SPI(service-provider interface),用于主要名称和地址的解析。
Java 19
于 2022 年 9 月 20 日正式发布,非长期支持版本,有比较重要的新特性。
孵化
向量 API(第四次孵化)。
结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent。
预览新特性
外部函数和内存 API,Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。
虚拟线程(Virtual Thread),是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP)。
Java 20
于 2023 年 3 月 21 日发布,非长期支持版本。
孵化
作用域值(Scoped Values),可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
结构化并发(第二次孵化)。
向量 API(第五次孵化)。
预览新特性
记录模式(Record Patterns,第二次预览),可对 record 的值进行解构,也就是更方便地从记录类(Record Class)中提取数据。
switch 模式匹配(第四次预览)。
外部函数和内存 API(第二次预览),Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。
虚拟线程(第二次预览)。
Java 21
于 2023 年 9 月 19 日 发布,是一个长期支持(LTS)版本,这是一个非常重要的版本,里程碑式。
转正
记录模式。
switch 的模式匹配。
虚拟线程。
预览新特性
字符串模板(String Templates),提供了一种更简洁、更直观的方式来动态构建字符串。
外部函数和内存 API(第三次预览)。
未命名模式和变量(预览),可以使用下划线 _ 表示未命名的变量以及模式匹配时不使用的组件,旨在提高代码的可读性和可维护性。
未命名类和实例 main 方法 (预览),主要简化了 main 方法的的声明。
序列化集合,引入了一种新的集合类型:Sequenced Collections(序列化集合,也叫有序集合),这是一种具有确定出现顺序的集合。
分代 ZGC,对 ZGC 进行了功能扩展,增加了分代 GC 功能。不过,默认是关闭的,需要通过配置打开。
0 条评论
下一页
为你推荐
查看更多