文章目录
- 一、集合
-
- [1.1 List](#1.1 List)
-
- [1.1.1 ArrayList](#1.1.1 ArrayList)
- [1.1.2 Vector](#1.1.2 Vector)
- [1.1.3 LinkedList](#1.1.3 LinkedList)
- [1.2 Deque](#1.2 Deque)
- [1.3 Set](#1.3 Set)
- [1.4 Map](#1.4 Map)
-
- [1.4.1 HashMap](#1.4.1 HashMap)
- [1.4.2 LinkedHashMap](#1.4.2 LinkedHashMap)
- [1.5 注意事项](#1.5 注意事项)
- [二、函数式接口和 Lambda 表达式](#二、函数式接口和 Lambda 表达式)
- 三、方法引用
-
- [3.1 静态方法引用](#3.1 静态方法引用)
- [3.2 实例方法引用](#3.2 实例方法引用)
- [3.2 特定类型的方法引用](#3.2 特定类型的方法引用)
- [3.4 构造器引用](#3.4 构造器引用)
- [四、Stream 流](#四、Stream 流)
-
- [4.1 获取 Stream 流](#4.1 获取 Stream 流)
- [4.2 Stream 流的中间方法](#4.2 Stream 流的中间方法)
- [4.3 Stream 流的终结方法](#4.3 Stream 流的终结方法)
- 五、异常和错误
- 六、泛型
-
- [6.1 泛型类](#6.1 泛型类)
- [6.2 泛型接口](#6.2 泛型接口)
- [6.3 泛型方法](#6.3 泛型方法)
- 七、反射
- 八、注解
- [九、Java 运行时数据区域](#九、Java 运行时数据区域)
-
- [9.1 程序计数器](#9.1 程序计数器)
- [9.2 虚拟机栈](#9.2 虚拟机栈)
- [9.3 本地方法栈](#9.3 本地方法栈)
- [9.4 堆](#9.4 堆)
- [9.5 元空间](#9.5 元空间)
- 十、类的生命周期
- 十一、类加载
-
- [11.1 Java 中默认的类加载器](#11.1 Java 中默认的类加载器)
- [11.2 双亲委派机制](#11.2 双亲委派机制)
- [11.3 打破双亲委派机制的三种方式](#11.3 打破双亲委派机制的三种方式)
- 十二、垃圾回收
-
- [12.1 可达性分析算法](#12.1 可达性分析算法)
- [12.2 垃圾回收算法](#12.2 垃圾回收算法)
-
- [12.2.1 标记-清除算法](#12.2.1 标记-清除算法)
- [12.2.2 复制算法](#12.2.2 复制算法)
- [12.2.3 标记-整理算法](#12.2.3 标记-整理算法)
- [12.2.3 分代垃圾回收算法](#12.2.3 分代垃圾回收算法)
一、集合
1.1 List
1.1.1 ArrayList
ArrayList 内部基于动态数组 Object[]
实现,会根据实际存储的元素数量动态地扩容或缩容 。不过 ArrayList 只能存储对象 ,对于基本数据类型,需要使用其对应的包装类,同时线程不安全。
ArrayList 有三个构造函数,其中以无参数构造方法创建 ArrayList 时,它会初始化一个空数组,但不分配实际容量,只有在添加第一个元素时,数组的容量才会扩展为默认大小(通常为 10)。
java
ArrayList()
ArrayList(int initialCapacity)
ArrayList(Collection<? extends E> c)
ArrayList 在空间不足时会进行动态扩容,扩容时首先会将容量变为原来的 1.5 倍左右(奇数会丢掉小数),然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量。
1.1.2 Vector
Vector 与 ArrayList 类似也采用 Object[]
实现,是 List 的古老实现类,同时线程安全 ,但是在增长时会以固定的幅度增加容量,而不是按倍数增加,这可能导致一些内存浪费,因此通常更倾向于使用 ArrayList,并使用显式的同步措施来确保线程安全性。
1.1.3 LinkedList
LinkedList 底层使用的是双向链表 ,不过需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且性能通常会更好。同时 ArrayList 也不是线程安全的。
1.2 Deque
ArrayDeque 和 LinkedList 都实现了 Deque 接口,ArrayDeque 基于动态数组实现,并且是循环数组,在队满时会扩容为原来的两倍。LinkedList 基于链表实现,速度较慢且存储密度低。因此一般优先考虑 ArrayDeque,不过 ArrayDeque 不支持存储 null
且线程不安全。
ArrayDeque 主要通过 addFirst()
、addLast()
、removeFirst()
和 removeLast()
实现双端队列的相关操作,这四个方法失败时都会抛出异常。
1.3 Set
Set 主要包括 HashSet、LinkedHashSet 和 TreeSet,它们都能保证元素唯一 ,并且都不是线程安全的。
三者主要区别在于底层数据结构不同:
- HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。
- LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。
- TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
1.4 Map
1.4.1 HashMap
HashMap 是一个无序的键值对集合,可以存储 null
的 key 和 value,但 null
作为键只能有一个,null
作为值可以有多个。
创建时如果不指定容量初始值,HashMap 默认的初始化大小为 16,之后每次扩充,容量翻倍。创建时如果给定了初始容量值,HashMap 会将其扩充至 2 n 2^n 2n 大小(主要是为了在减少哈希碰撞同时提高哈希运算的效率,如果数组的长度为 2 n 2^n 2n,那么在映射时只需要直接取键的低 n 位即可,通过位运算即可实现,不需要取余)。
HashMap 基于数组 + 链表 / 红黑树 实现,默认采用数组 + 链表,当链表长度大于 8 时会通过 treeifyBin()
处理哈希冲突,如果此时数组长度小于 64 会优先对数组进行扩容,否则会将链表转化为红黑树以减少搜索时间。
HashMap 是非线程安全的,其并发版本为 ConcurrentHashMap。ConcurrentHashMap 通过 synchronized
锁定链表或红黑二叉树的首节点,从而保证并发安全。不过 ConcurrentHashMap 的 key 和 value 均不能为 null
,因为多线程情况下无法准确判断 null
表示的是不存在还是存在一个空的键或值。同时,ConcurrentMap 保证的只是单次操作的原子性,而不是多次操作 ,因此类似于先通过 containsKey()
判断键是否存在然后再通过 put()
插入元素的操作都是线程不安全的。如果想要执行复合操作,可以通过 putIfAbsent()
等复合函数代替。
1.4.2 LinkedHashMap
LinkedHashMap 和 HashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能 。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效。
LinkedHashMap 提供了两种顺序迭代元素的方式:
- 按照插入顺序迭代元素:按照插入顺序迭代元素是 LinkedHashMap 的默认行为。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。
- 按照访问顺序迭代元素 :可以通过构造函数中的
accessOrder
参数指定按照访问顺序迭代元素。当accessOrder
为true
时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。
1.5 注意事项
-
判断所有集合内部的元素是否为空,使用
isEmpty()
方法,而不是size() == 0
,因为isEmpty()
方法的可读性更好并且效率更高,size() == 0
在某些情况下还要进行类型转换。 -
集合转数组时,使用集合的
collection.toArray(T[] array)
方法,有以下两种使用方式。import java.util.*; class Solution { public static void main(String[] args) { List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3)); Integer[] array = new Integer[3]; // 1. 返回参数 list.toArray(array); for (int i = 0; i < array.length; i++) { System.out.println(i); // 0 1 2 } // 2. 通过 new Integer[0] 说明返回类型,0 是为了节省空间 array = list.toArray(new Integer[0]); for (int i = 0; i < array.length; i++) { System.out.println(i); // 0 1 2 } } }
-
数组转集合时,使用
Arrays.asList(array)
方法。不过该数组必须是对象数组,而不是基本类型数组。同时Arrays.asList()
方法返回的并不是java.util.ArrayList
,而是java.util.Arrays
的一个内部类,因此需要一次附加的转换。import java.util.*; class Solution { public static void main(String[] args) { Integer[] array = new Integer[] {1, 2, 3}; List<Integer> list = Arrays.asList(array); System.out.println(list); // [1, 2, 3] System.out.println(list.getClass()); // class java.util.Arrays$ArrayList List<Integer> trueList = new ArrayList<>(list); trueList.remove(2); System.out.println(trueList); // [1, 2] System.out.println(trueList.getClass()); // class java.util.ArrayList } }
二、函数式接口和 Lambda 表达式
函数式接口和 Lambda 表达式均为 Java8 新特性。函数式接口就是有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,可以被隐式转换为 Lambda 表达式。Lambda 表达式实际是匿名函数,其原型为:(参数列表) -> {函数体};
。
java
import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[]{1, 2, 3};
Arrays.sort(array, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
List<Integer> list = Arrays.asList(array);
list.forEach((i)->{
System.out.println(i); // 3 2 1
});
}
}
三、方法引用
方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 ::
表示,主要分为静态方法引用、实例方法引用、特定类型的方法引用和构造器引用。
3.1 静态方法引用
java
public class MyCompare {
public static int compareFunc(int o1, int o2) {
return o2 - o1;
}
}
java
import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[]{1, 2, 3};
// 静态方法引用
Arrays.sort(array, MyCompare::compareFunc);
List<Integer> list = Arrays.asList(array);
list.forEach(System.out::println); // 3 2 1
}
}
3.2 实例方法引用
java
public class MyCompare {
public int compareFunc(int o1, int o2) {
return o2 - o1;
}
}
java
import java.util.*;
class Solution {
public static void main(String[] args) {
Integer[] array = new Integer[]{1, 2, 3};
MyCompare compare = new MyCompare();
// 实例方法引用
Arrays.sort(array, compare::compareFunc);
List<Integer> list = Arrays.asList(array);
list.forEach(System.out::println); // 3 2 1
}
}
3.2 特定类型的方法引用
java
import java.util.*;
class Solution {
public static void main(String[] args) {
String[] array = new String[]{"b", "A"};
// 特定类型的方法引用
Arrays.sort(array, String::compareToIgnoreCase);
List<String> list = Arrays.asList(array);
list.forEach(System.out::println); // A b
}
}
3.4 构造器引用
java
public class MyCompare {}
java
import java.util.function.Supplier;
class Solution {
public static void main(String[] args) {
Supplier<MyCompare> sup = MyCompare::new;
}
}
四、Stream 流
Stream 在 Java8 中被引入,它提供了一种类似于 SQL 语句的方式来对 Java 集合进行操作和处理。
4.1 获取 Stream 流
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
class Solution {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
String[] array = new String[10];
// 获取集合的 Stream 流
Stream<String> listStream = list.stream();
// 获取数组的 Stream 流
Stream<String> arrayStream = Arrays.stream(array);
}
}
4.2 Stream 流的中间方法
常用中间方法 | 说明 |
---|---|
Stream<T> filter(Predicate<? super T> predicate) |
用于对流中的数据进行过滤 |
Stream<T> sorted(Comparator<? super T> comparator) |
按照指定规则排序 |
Stream<T> limit(long maxSize) |
获取前几个元素 |
Stream<T> skip(long n) |
跳过前几个元素 |
Stream<T> distinct() |
去除流中重复的元素(自定义对象需要重写 equals() 和 hashCode() 方法) |
<R> Stream<R> map(Function<? super T, ? extends R> mapper) |
加工元素并返回新流 |
static <T> Stream<T> concat(stream a, Stream b) |
合并 a 和 b 为一个流 |
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.stream().filter(i -> i > 1).forEach(System.out::print); // 2 3
list.stream().sorted((o1, o2) -> o2 - o1).forEach(System.out::print); // 3 2 1
list.stream().map(i -> i + 1).forEach(System.out::print); // 2 3 4
}
}
4.3 Stream 流的终结方法
常用终结方法 | 说明 |
---|---|
void forEach(Consumer action) |
遍历 |
long count() |
统计元素个数 |
Optional<T> max(Compatator<? super T> comparator) |
获取最大值元素 |
Optional<T> min(Compatator<? super T> comparator) |
获取最小值元素 |
R collect(Collector collector) |
把流中的元素收集到集合 |
Object[] toArray() |
把流中的元素收集到数组 |
五、异常和错误
java.lang.Throwable
是所有异常和错误的父类,它有 Exception 和 Error 两个子类,其中异常又可以分为受检查异常 和不受检查异常。
- 不受检查异常在编译阶段不会出现错误提醒,主要包括 RuntimeException 及其子类,例如
NullPointerException
、IllegalArgumentException
、ArrayIndexOutOfBoundsException
和ClassCastException
等异常。 - 受检查异常在编译阶段就会出现错误提醒,除了 RuntimeException 及其子,其他的 Exception 类及其子类都属于受检查异常,例如
IOException
、SQLException
和ClassNotFoundException
等。处理可检查异常的方式可以是使用try-catch-finally
语句块进行捕获和处理,或者在方法签名中声明抛出该异常。 - Error 类用来表示系统级别的严重错误,例如
OutOfMemoryError
、StackOverflowError
等。对于系统错误,一般不建议进行捕获和处理,而是直接让 JVM 终止。
Throwable 类常用方法:
String getMessage()
:返回异常发生时的简要描述。String toString()
:返回异常发生时的详细信息。void printStackTrace()
:在控制台上打印 Throwable 对象封装的异常信息。
Exception 支持自定义新的异常类型,但是在项目中保持一个合理的异常继承体系是非常重要的,因此可以定义一个 BaseException
根异常继承自 RuntimeException
,其他业务类型的异常再从根异常中派生。
需要注意的是,在通过 try-catch-finally
捕获和处理异常时,不应该在 finally
语句块中使用 return
,因为当 try
语句和 finally
语句中都有 return
语句时,try
语句中的返回值会先被暂存在一个本地变量中,当执行到 finally
语句中的 return
之后,这个本地变量的值就变为了 finally
语句中的 return
返回值,从而导致 try
语句块中的 return
语句会被忽略。
java
class Solution {
public static void main(String[] args) {
System.out.println(func()); // 2
}
private static int func() {
try {
return 1;
} finally {
return 2;
}
}
}
此外,在 JDK7 之后还提供了 try-with-resources
用于管理文件、网络连接、数据库连接等所有实现了 java.lang.AutoCloseable
接口的资源类,它简化了资源管理的代码,并确保资源在使用后被正确关闭,以避免资源泄漏。
六、泛型
泛型是 JDK5 中引入的一个新特性,类似于 C++ 中的模板,泛型编程以一种独立于任何特定类型的方式编写代码。Java 中的泛型主要有泛型类、泛型接口、泛型方法三种使用方式。
6.1 泛型类
java
public class MyClass<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
java
class Solution {
public static void main(String[] args) {
MyClass<Integer> classInt = new MyClass<>();
classInt.setData(1);
System.out.println(classInt.getData()); // 1
MyClass<Double> classDouble = new MyClass<>();
classDouble.setData(1.0);
System.out.println(classDouble.getData()); // 1.0
}
}
6.2 泛型接口
java
public interface MyInterface<T> {
T func();
}
java
public class MyClass implements MyInterface<String> {
@Override
public String func() {
return "str";
}
}
java
class Solution {
public static void main(String[] args) {
MyInterface<String> myClass = new MyClass();
System.out.println(myClass.func()); // str
}
}
6.3 泛型方法
java
class Solution {
public static void main(String[] args) {
Integer i = 1;
Long l = 1L;
func(i); // class java.lang.Integer
func(l); // class java.lang.Long
}
private static <T> void func(T data) {
System.out.println(data.getClass());
}
}
七、反射
反射机制允许我们在运行时获取类的信息、调用类的方法、操作类的属性,而无需在编译时知道类的具体名称,但存在一定的安全问题,同时会影响性能。
获取 Class 对象的四种方式:
java
package atreus.ink;
public class MyClass {}
java
import atreus.ink.MyClass;
class Solution {
public static void main(String[] args) throws ClassNotFoundException {
{
// 1. 使用 .class 获取,不会触发类的初始化
Class<MyClass> clazz = MyClass.class;
System.out.println(clazz); // class atreus.ink.MyClass
}
{
// 2. 通过 Class.forName() 传入类的全路径获取
Class<?> clazz = Class.forName("atreus.ink.MyClass");
System.out.println(clazz); // class atreus.ink.MyClass
}
{
// 3. 通过对象实例的 getClass() 方法获取
MyClass myClass = new MyClass();
Class<? extends MyClass> clazz = myClass.getClass();
System.out.println(clazz); // class atreus.ink.MyClass
}
{
// 4. 通过类加载器的 loadClass() 方法传入类的全路径获取,不会触发类的初始化
ClassLoader classLoader = MyClass.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("atreus.ink.MyClass");
System.out.println(clazz); // class atreus.ink.MyClass
}
}
}
反射的一些基本操作:
java
package atreus.ink;
public class MyClass {
private String data;
public MyClass() {
data = "atreus";
}
private void privateMethod() {
System.out.println("private void privateMethod()");
}
public String publicMethod(String s) {
return s;
}
}
java
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class Solution {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 通过 Class.forName() 传入类的全路径获取 Class 对象
Class<?> clazz = Class.forName("atreus.ink.MyClass");
// 创建类的实例对象
Object instance = clazz.getDeclaredConstructor().newInstance();
// 获取类中定义的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
// 获取指定方法并调用
{
Method method = clazz.getDeclaredMethod("publicMethod", String.class);
Object result = method.invoke(instance, "public String publicMethod(String s)");
System.out.println(result);
}
{
Method method = clazz.getDeclaredMethod("privateMethod");
method.setAccessible(true);
method.invoke(instance);
}
// 获取指定参数并在修改后输出
Field field = clazz.getDeclaredField("data");
field.setAccessible(true);
field.set(instance, "new data");
System.out.println(field.get(instance));
}
}
public java.lang.String atreus.ink.MyClass.publicMethod(java.lang.String)
private void atreus.ink.MyClass.privateMethod()
public String publicMethod(String s)
private void privateMethod()
new data
八、注解
注解可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供了某些信息供程序在编译或者运行时使用。
注解本质上是是一个接口,继承自 Annotation 类,注解属性本质则是抽象方法,使用注解实际上使用的是该接口的实现类 。可以通过 @interface
自定义注解,且注解中如果只有一个 value
属性,使用注解时 value
名称可以不写
java
public @interface MyAnnotation {
public 属性类型 属性名() default 默认值;
}
public interface MyAnnotation extends Annotation {
public abstract 属性类型 属性名();
}
java
public @interface MyAnnotation {
String value();
}
java
class Solution {
@MyAnnotation("str") // 可以省略 value
public static void main(String[] args) {}
}
元注解是修饰注解的注解,主要分为 @Target
和 @Retention
:
@Target
:声明被修饰的注解能在哪些位置使用,如类、接口、成员变量、成员方法等。@Retention
:声明注解的保留周期。SOURCE
表明只作用在源码阶段,字节码文件中不存在。CLASS
为默认值,表明保留到字节码文件中,但运行阶段不存在。RUNTIME
表明一直保留到运行阶段。
AnnotatedElement 接口定义了与注解解析相关的方法。注解一般需要与反射结合使用,所有的类成分 Class、Method、Field 和 Constructor 都实现了 AnnotatedElement 接口,它们都拥有解析注解的能力。
主要解析方法有:
Annotation[] getDeclaredAnnotations()
:获得当前对象上使用的所有注解,返回注解数组。T getDeclaredAnnotation(Class<T> annotationClass)
:根据注解类型获得对应注解对象。boolean isAnnotationPresent(Class<Annotation> annotationClass)
:判断当前对象是否使用了指定的注解。
九、Java 运行时数据区域
Java 运行时数据区域主要分为程序计数器 、虚拟机栈 、本地方法栈 、堆 和元空间 ,其中只有堆和元空间为线程共享。
9.1 程序计数器
每个线程会通过自己的程序计数器记录当前要执行的字节码指令的地址。程序计数器一方面能够控制指令的执行顺序,实现分支、跳转和异常等逻辑,另一方面也能够在多线程执行情况下为当前线程记录 CPU 切换前指令的执行位置。
9.2 虚拟机栈
虚拟机栈的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。它由一个个栈帧组成,调用一个新的函数会在栈上创建一个新的栈帧,当函数返回时这个栈帧会被自动销毁。如果栈的大小不支持动态扩展,发生栈溢出时就会抛出 StackOverFlowError。如果栈的大小支持动态扩展,在扩展过程中无法申请到足够的内存空间时就会抛出 OutOfMemeoryError,可以通过虚拟机参数 -Xss
修改栈的大小。
每个栈帧由局部变量表 、操作数栈 和帧数据三部分组成:
- 局部变量表 :本质是一个数组,数组中从前向后依次保存了实例方法的 this 对象(非静态方法)、方法的参数(有参方法)以及方法体中声明的局部变量,数组的每个位置称之为槽,long 和 double 类型占用两个槽,其他类型(包括 this 等引用类型)占用一个槽。局部变量表的具体内容在编译成字节码文件时就已经确定,不过最大槽位数是固定的,因此为了节省空间,一旦槽中的某个局部变量不再被使用,当前槽就可以被其他局部变量复用。
- 操作数栈 :主要用于存放执行执行过程中的中间数据,在编译期就能确定操作数栈的最大深度,从而在执行时正确分配内存大小。
- 帧数据 :主要包含动态链接 、方法出口 和异常表的引用 :
- 动态链接:如果当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用转换成对应的运行时常量池(位于元空间)中的内存地址,动态链接就保存了符号引用到运行时常量池中的内存地址的映射关系。
- 方法出口:方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址,方法出口存储的就是这条指令的地址。
- 异常表的引用 :异常表存放的是代码中异常的处理信息,包含了
try
代码块的覆盖范围以及出现或未出现异常时需要跳转到的字节码指令的地址。
9.3 本地方法栈
本地方法栈存储的是 native 本地方法的栈帧,不过在 HotSpot 虚拟机中,Java 虚拟机栈和本地方法栈使用的是同一个栈空间。
9.4 堆
堆内存是空间最大的一块内存区域,创建出来的所有对象都保存在堆空间上 。可以通过虚拟机参数 -Xms
修改堆的初始大小(total),通过 -Xmx
修改堆的最大大小(max)。
堆除了存储普通的对象,还存储了字符串常量池 。字符串常量池主要存储了字符串字面值,从而实现字符串的重用。可以通过 intern()
方法手动将堆中字符串的引用放入字符串常量池。
9.5 元空间
元空间是在 JDK8 之后对方法区的具体实现(方法区只是逻辑上的概念,类似于 C++ 中的自由存储区),元空间使用的是直接内存。
元空间主要保存了类的基本信息 (元信息)与运行时常量池:
- 类的元信息:主要包括类的名称、方法和字段的描述、访问修饰符以及继承关系等。
- 运行时常量池:运行时常量池是类文件中的常量池的运行时表示,包括字面量、符号引用、方法和字段引用等。
十、类的生命周期
- 加载阶段 :类加载器会通过全类名获取定义此类的二进制字节流(字节码),然后 JVM 会将字节流中的信息以 InstanceKlass 对象的形式保存到方法区,最后在堆中生成一份与 InstanceKlass 中的数据类似的
java.lang.Class
对象。 - 验证阶段 :验证 Class 文件的字节流中包含的信息是否符合《Java 虚拟机规范》,主要分为文件格式验证 (例如文件是否以
0xCAFEBABE
开头、主次版本号是否符合虚拟机要求)、元数据验证 (例如类必须有父类)、字节码验证 (例如方法内的执行指令跳转位置是否合理)、符号引用验证(例如类是否访问了其他类中的 private 方法)。 - 准备阶段:为静态变量分配内存并设置初始值。一般情况下准备阶段的初始值为 0,代码中的指定值会在初始化阶段再赋给静态变量,但如果这个静态变量还被 final 修饰,由于其常量性,会直接初始化为代码中的指定值。
- 解析阶段:将常量池内的符号引用替换为直接引用,直接引用不再使用编号,而是直接使用内存中的地址访问具体的数据。
- 初始化阶段:执行静态代码块以及静态字段初始化语句,具体执行顺序取决于 Java 中编写的顺序,只有在主动使用类时才会初始化类。
- 使用阶段:使用包括主动引用和被动引用,只有主动引用会引起类的初始化,而被动引用不会引起类的初始化。
- 卸载阶段 :卸载类即将方法区中的类回收,只有同时满足以下三个条件时类才能被卸载:
- 此类的所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类。
- 加载该类的类加载器已经被回收。
- 该类对应的
java.lang.Class
对象没有在任何地方被引用。
主动引用包括:
- 通过
new
创建一个类的对象。 - 访问未被 final 修饰的静态变量或者调用静态方法。
- 调用
Class.forName(String className)
。 - 虚拟机启动时会初始化包含 main 方法的主类。
- 初始化一个类,如果其父类还未初始化,则先触发父类的初始化。
被动引用包括:
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会引起类的初始化。
- 引用类的 static final 常量,不会引起类的初始化。
十一、类加载
11.1 Java 中默认的类加载器
- 启动类加载器 (BootstrapClassLoader):最顶层的加载类,由 Hotspot 虚拟机提供,通过 C++ 实现(JDK 9 之后由 Java 实现,且由按路径查找变为按模块查找)。主要用来加载 JDK 内部的核心类库(如
java.lang
和java.util
等)以及被-Xbootclasspath
参数指定的路径下的所有类。 - 扩展类加载器 (ExtensionClassLoader):由 JDK 提供,主要负责加载 JDK 的扩展类库(即
$JAVA_HOME/jre/lib/ext
目录下的类,它们通用但不重要)以及被-Djava.ext.dirs
参数指定的路径下的所有类。 - 应用程序类加载器(AppClassLoader):由 JDK 提供,是面向用户的加载器,主要负责加载项目中生成的字节码文件以及第三方依赖中的字节码文件。
11.2 双亲委派机制
双亲委派机制保证了类加载的安全性(所有核心类都有顶层类加载器加载,避免了恶意程序篡改核心类库),同时避免了类的重复加载。
双亲委派机制的执行流程:
- 在类加载的时候,系统会首先判断当前类是否被加载过,已经被加载的类会直接返回。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是调用父加载器
loadClass()
方法,把这个请求委派给父类加载器去完成。这样所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会调用自己的
findClass()
方法来尝试自己去加载。 - 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。
11.3 打破双亲委派机制的三种方式
- 自定义类加载器 :自定义类加载器继承自 ClassLoader,重写
loadClass()
方法即可打破双亲委派机制。Tomcat 就通过自定义类加载器为每个 Web 应用指定独立的类加载器,从而实现了应用之间类的隔离。 - 线程上下文类加载器:以 JDBC 为例,启动类加载器在加载 DriverManager 类的过程中,需要同时获取并加载第三方驱动,其中驱动的获取可以通过 SPI 机制实现,但第三方驱动的加载只能由应用程序类加载器实现,因此需要通过线程上下文类加载器(应用程序类加载器)打破双亲委派机制,加载第三方驱动类并创建对象。
- Osig 框架的类加载器:允许同级之间的类加载器委托加载,同时还使用类加载器实现了热部署的功能。不过 Osig 已经废弃,热部署可以通过阿里的 Arthas 实现。
十二、垃圾回收
12.1 可达性分析算法
可达性分析算法的基本思想就是以一系列的 GC 根节点(GC Roots)对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC 根节点没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
可作为 GC 根节点的对象主要有以下几种:
- 线程 Thread 对象。
- 系统类加载器加载的 java.lang.Class 对象。
- 监视器对象,用来保存同步锁 synchronized 关键字持有的对象。
- 本地方法调用时使用的全局对象。
12.2 垃圾回收算法
12.2.1 标记-清除算法
- 标记阶段:将所有存活的对象进行标记。Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
- 清除阶段:从内存中删除没有被标记也就是非存活对象。
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:由于内存是连续的,所以在对象被删除之后内存中会出现很多内存碎片。同时,由于内存碎片的存在,需要维护一个空闲链表对内存空间进行管理。
12.2.2 复制算法
- 准备 From 空间和 To 空间两块空间,只能使用 From 空间进行内存分配。
- 在垃圾回收 GC 阶段,将 From 中存活对象复制到 To 空间。
- 将两块空间的 From 和 To 名字互换。
优点:不会产生内存碎片,复制算法在复制时会将对象按顺序放入 To 空间,因此不存在内存碎片。
缺点:可用内存空间会缩小为总内存空间的一半,内存利用率低。同时,如果待复制的对象过大,复制开销也会增加。
12.2.3 标记-整理算法
- 标记阶段:将所有存活的对象进行标记。Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
- 整理阶段:将存活对象移动到堆的一端依次排列,清理掉其余空间。
优点:不会产生内存碎片,同时内存利用率较高(相对于复制算法)。
缺点:整理阶段会有较大的性能开销。
12.2.3 分代垃圾回收算法
分代垃圾回收将整个内存区域划分为年轻代 (存放存活时间比较短的对象)和老年代 (存放存活时间比较长的对象),其中年轻代还可以再分为 Eden 、Survivor 0 和 Survivor 1 三个区。
- 创建出来的对象,首先会被放入 Eden 伊甸园区。
- 随着 Eden 区的对象越来越多,如果 Eden 区满,继续向年轻代放入对象就会触发年轻代的 GC,称为 Minor GC 或者 Young GC。Minor GC 会把 Eden 区和 From 区中需要回收的对象回收,把没有回收的对象放入 To 区,其中 From 区和 To 区由 Survivor 0 和 Survivor 1 轮流担任,即采用复制算法。
- 每轮 Minor GC 都会为本轮存活对象记录一个年龄,当年龄达到 JVM 阈值时相应对象会被晋升至老年代。
- 当老年代中空间不足时,如果继续向老年代放入对象,首先会尝试 Minor GC(通过回收年轻代尝试避免继续将对象放入老年代),如果还是不足,就会触发 Full GC,Full GC 会对整个堆进行垃圾回收。如果 Full GC 依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出 Out Of Memory 异常。
参考: