1. Java内存区域(运行时数据区)详解

以下指的都是目前使用范围最广的HotSpot VM(java 虚拟机)
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
https://blog.csdn.net/qq_37740841/article/details/102948711?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0.readhide&spm=1001.2101.3001.4242
https://www.imooc.com/article/42913

JDK1.7和JDK1.8主要差异移除了方法区(永久代(在堆中)来实现),增加了元空间(Metaspace,其实本质上也是对方法区的实现。但一般也会用方法区特指永久代,新的叫元空间加以区分
方法区被移除后,里面的东西大部分被移到堆里,剩下元数据保存在元空间
永久代和元空间最大区别:元空间不在虚拟机中,而是使用本地内存(直接内存)

永久代弃用原因:永久代内存经常不够用或者发生内存泄露,爆出OutOfMemoryError,改用元空间使用本地内存空间

JDK1.7 JVM示意图:
image.png

JDK1.8 JVM示意图:
image.png image.png
image.png

1.1 程序计数器(Program Counter Register)

较小的内存空间,指向当前线程所执行的字节码的行号指示器 线程私有,生命周期随线程创建到死亡
唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
执行java方法:记录的是正在执行的虚拟机字节码指令的地址
执行Native方法:值为空(Undefined) (因为方法底层是C++这种写的,所以没法计数)
两个作用:
1.字节码解释器通过改变程序计数器来依次读取指令——实现代码流程控制,如顺序执行、选择、循环、异常处理
2.多线程下,记录当前线程执行的位置——线程切换回来时可继续运行上次的

1.2 虚拟机栈(平时说的栈就是其中的局部变量表部分)

栈中存放栈帧——每次调用一个1.方法,就把他作为栈帧压入栈,里面包含局部变量(2.基本+3.对象引用)等信息
栈包括:局部变量表、操作数栈、动态链接、方法出口信息
线程私有、生命周期同线程
局部变量表
存放编译期可知的各种数据类型(8基本+对象引用),注意:对象引用≠对象本身,指的是指向对象地址的指针或代表对象的句柄
存储空间32位,刚好放int类型,长度为64的long和double占两个局部变量空间(Slot)
两种错误
StackOverFlowError:线程深度超过JVM最大深度
OutOfMemoryError:堆中无空闲内存且垃圾回收器无法提供更多内存
方法调用
类比数据结构的栈,Java栈保存的内容是栈帧,函数调用时把栈帧压入Java栈,调用结束弹出栈帧
方法的两种返回方式(都会使栈帧弹出):
1.return
2.抛出异常

1.3 本地方法栈

和虚拟机栈类似,但是服务于Native方法,也有相应内容和错误
线程私有、生命周期同线程

1.4 堆(Heap)也被称为GC堆(Garbage Collected Heap)

内存最大,所有线程共享,虚拟机启动时创建
唯一目的:存放对象实例,几乎所有对象实例和数组(JDK1.7默认开启逃逸分析,若对象引用没返回或为被未界使用,那么对象直接在栈上分配内存) 静态变量也放在堆中
垃圾收集器管理的主要区域,收集器基本采用分代垃圾收集算法
分为:新生代(Eden、From Survivor(S0)、To Survivor(S1))、老年代(Old Gen)、永生代(1.8以前)
堆内存占比示意图:
image.png
分区目的:为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收

1.5 方法区(Method Area)

线程共享,存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码数据
包括运行时常量池(Runtime Constant Pool)(里面包含字符串常量池、1.8后移至堆中)
image.png

方法区的大小在启动时设置好,默认或手动(MaxPermSize),即大小有限制(触发永久代 OOM 后该大小会动态调整)
常用参数:

  1. 1.8之前
  2. -XX:PermSize=N //方法区 (永久代) 初始大小
  3. -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError

1.6 元空间(MetaSpace)

保存元数据描述数据的数据,如方法、字段、类、包的描述信息,这些信息可用于创建文档、跟踪代码中的依赖性、执行编译时检查
意义:因为使用直接内存,受本机可用内存限制,溢出几率变小。
常用参数:

  1. 1.8
  2. -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
  3. -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

1.7 直接内存

不是运行时数据区的一部分,但频繁使用,也会出现OutOfMemoryError
JDK1.4后新加入NIO(New Input/Output)类,引入基于通道(Channel)与缓存区(Buffer)的I/O方式,可以直接使用Native函数直接分配对外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。提高了性能,避免了Java堆和Native堆之间来回复制数据。

image.png

运行时常量池在元空间中
基本类型的字面值在栈中
final修饰的常量属于字面量,在常量池表中,即在元空间中
static修饰的变量在堆中

2. Java类加载全过程

2.1 类加载器

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22162061/1627021077337-d065e361-9c12-4004-a583-6e742a4b40a6.png#height=149&id=C6nSg&margin=%5Bobject%20Object%5D&name=image.png&originHeight=198&originWidth=933&originalType=binary&ratio=1&size=197401&status=done&style=none&width=700)<br />**类加载器:通过类的全限定名(或者说 绝对路径)找到一个class文件**<br />启动类加载器、扩展类加载器、系统(应用)类加载器、自定义类加载器<br />**BootstrapClassLoader、ExtClassLoader、AppClassLoader**<br />注意:说是**父类,但不是直接的继承关系,它们是通过一个parent的属性,指明父加载器是谁**。其实是三种,从**Java继承机制**看,他们都继承于**UrlClassLoader**,Urlxxx继承于SecureClassLoader,再往上ClassLoader。<br />和程序员最接近的是**AppClassLoader**加载器,自定义的类加载器都要继承这个,且要满足双亲委派。自己的代码还有底层的jar包,都是**AppClassLoader**加载。**main方法由他开始**。<br />自定义类加载器的实现:继承**ClassLoader**,覆盖**findClass()**方法。<br />它不仅是**系统类加载器**,还是**线程上下文类加载器**<br />**线程上下文类加载器:**有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作(即,父类加载器加载的类,使用线程上下文加载器去加载其无法加载的类),这种行为实际上就是打通了双亲委派模型的层次结构来**逆向使用类加载器**,实际上已经违背了双亲委派模型的一般性原则。<br />Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

2.2 类加载过程(面试问:new一个class对象会发生什么)

每个类加载器对他加载过的类都有一个缓存
类加载过程:加载->验证->准备->解析->初始化
image.png
通俗解释:
准备:指的是静态变量的内存分配和赋初值,因为这能随类一同加载,不是实例变量(将在4的对象创建过程)。赋初值指的是JVM将其设成零值,而不是其赋值语句private static int size = 12。这阶段size=0!
解析:虚拟机将常量池表内的符号引用替换为直接引用的过程。在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在解析阶段就是为了把这个符号引用转化成真正的地址的阶段。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
直接引用可以是:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
初始化:指类的初始化(不要和对象的初始化搞混)准备阶段中已经初始化一次,这个阶段是按照程序里写的代码计划性地去初始化类中变量和其他资源。包括:static{}块,构造函数,父类的初始化。

理解简记

加载:字节码文件加载到JVM
验证:验证字节码文件是否符合JVM规范
准备:创建静态变量(内存分配),赋初值(设成0)
解析:符号引用转为直接引用
初始化:类的初始化,static代码块、构造函数、父类的初始化

2.3 类的生命周期

image.png

3. 双亲委派模型

示意图:
image.png
双亲委派:
向上委派到顶层加载器为止 查找缓存 之前各个类加载器会把加载过的类放到缓存中,先找自己的缓存,没有就往上找,看有没有这个类。 查到顶还是没有,则开始向下查找
向下查找到发起加载的加载器为止 查找加载路径 每个类加载器都有自己的加载路径,路径里看有没有这个类,有的话加载,返回 发起的类加载器不一定是AppClassLoader,还可能是自定义类加载器

好处:
安全性 保护底层基础类,避免用户编写的类动态替换Java的一些核心类,比如String
(假设自己定义了一个String类,根据双亲委派模型,会向上委托查找缓存,在启动加载器缓存中找到,就返回了,这样实际使用的还是Java写好的底层类String。)
同时避免类的重复加载 因为JVM中区分不同类,不仅仅根据类名,相同的class文件被不同类加载器加载就是不同的两个类

4. Java对象的创建过程(五步)

示意图:
image.png
image.png

1.类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

2.分配内存:类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。

(扩展)
分配方式“指针碰撞”“空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定(GC算法“标记-清除”或“标记-整理/压缩”)
JVM - 图13
内存分配并发问题——创建对象要保证线程安全,两种方式:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

3.初始化零值:虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
保证对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.设置对象头:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。
另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

5.执行init方法:从虚拟机的视角来看,一个新的对象已经产生了。但从 Java 程序的视角来看,对象创建才刚开始,init方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

(扩展)
对象的内存布局:3块区域:对象头实例数据对齐填充
对象头:两部分信息:存储对象自身的自身运行时数据、类型指针(确定对象是哪个类)
实例数据:对象真正存储的有效信息
对齐填充:非必要,起占位作用

5. 对象的访问定位的两种方式

Java 程序通过栈上的 reference (引用)数据来操作堆上的具体对象,访问方式有①使用句柄②直接指针两种:
1.句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的具体地址信息
优点: reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改
image.png
2.直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址
优点:速度快,它节省了一次指针定位的时间开销
image.png

6. String和常量池

String对象的两种创建方式

  1. String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
  2. String str2 = new String("abcd");//堆中创建一个新的对象
  3. String str3 = new String("abcd");//堆中创建一个新的对象
  4. System.out.println(str1==str2);//false
  5. System.out.println(str2==str3);//false

第一种方式是在常量池中拿对象
第二种方式是直接在堆内存空间创建一个新的对象
记住一点:只要使用 new 方法,便需要创建新的对象
image.png
注意:
1.直接使用双引号声明出来的 String 对象会直接存储在常量池
2.如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。
String类的intern()方法:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象(注意是常量池中的对象,不是堆中的对象)的引用。
string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
具体看下例:

  1. String s1 = new String("计算机");
  2. String s2 = s1.intern();
  3. String s3 = "计算机";
  4. System.out.println(s2);//计算机
  5. System.out.println(s1 == s2); //false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
  6. System.out.println(s3 == s2); //true,因为两个都是常量池中的 String 对象

字符串拼接:

  1. String str1 = "str";
  2. String str2 = "ing";
  3. String str3 = "str" + "ing";//常量池中的对象
  4. String str4 = str1 + str2; //在堆上创建的新的对象
  5. String str5 = "string";//常量池中的对象
  6. System.out.println(str3 == str4);//false
  7. System.out.println(str3 == str5);//true
  8. System.out.println(str4 == str5);//false

图解:
image.png
所以:尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

String s1 = new String(“abc”); 这句话创建了几个字符串对象
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
注意:不是0或1个,如果池中没有,会在池中和堆空间中各创一个!

(一个不影响上述理解的知识点:字符串常量池中并非直接将字符串对象本身存储,而是存储其字符串对象的引用,实际的字符串对象存储于堆中,注意这个对象和new出来的对象不是同一个。)
例1:
String s = new String(“1”);
String s2 = “1”;
解释:如图,s指向的是堆中new出的String对象,发现常量池中没有“1”对象,于是在堆中创建一个“1”对象,常量池中保存了指向“1”对象的引用。 -> 创建了两个对象
第二句,此时发现常量池中已经存在堆中对象“1”的引用,返回此对象引用,如图
显然 s 不等于 s2
JVM - 图18
JVM - 图19

例2. intern()方法:String s = new String(“ab”),在调用s.intern()方法的时候,这个方法会首先查询字符串池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用(需要接收返回值,不直接改变s指向),否则就将这个字符串添加到字符串池中,然后返回这个字符串的引用(修改s的指向)。

  1. String s3 = new String("1") + new String("1");
  2. String s4 = "11";
  3. System.out.println(s3 == s4); // false
  4. 解释:
  5. s3是在堆中new出了2个对象“1”,常量池中也有1个常量“1”的引用,s3指向堆中“11”对象
  6. s4指向字符串常量池中的引用,引用指向堆中常量“11
  7. 所以s3s4指的不同
  1. String s3 = new String("1") + new String("1");
  2. s3.intern(); // 创建了常量“11”引用,并把s3指向了它
  3. String s4 = "11";
  4. System.out.println(s3 == s4); // true
  5. 解释:
  6. s3在堆中new出对象“11
  7. intern查询字符串常量池未找到常量“11”的引用,创建并返回常量引用,修改了s3
  8. s4指向常量“11”的引用
  9. 所以s3s4都指向了常量“11
  1. String s3 = new String("1") + new String("1");
  2. String s4 = "11";
  3. s3.intern(); // 发现已经有了常量引用,直接返回,如果没有接收就没有改变s3
  4. System.out.println(s3 == s4); // false
  5. 解释:
  6. s3在堆中new出对象“11
  7. s4指向常量池常量“11”的引用
  8. intern查询发现了已经存在常量“11”的引用,返回,但是并未接收,所以s3仍指向堆中对象
  1. String s3 = new String("11");
  2. String s4 = "11";
  3. System.out.println(s3 == s4); // false
  4. String s3 = new String("11");
  5. s3.intern(); // 常量池中已经有了,返回但是并未接收,所以s3仍指向堆中对象
  6. String s4 = "11";
  7. System.out.println(s3 == s4); // false
  8. String s3 = new String("11");
  9. String s4 = "11";
  10. s3.intern(); // 常量池中已经有了,返回,但是并未接收,所以s3仍指向堆对象
  11. System.out.println(s3 == s4); // false
  12. String s3 = new String("11");
  13. String s4 = "11";
  14. String a = s3.intern(); // a接收了返回值,和s4相同
  15. System.out.println(s4 == a); // true

7. 8种基本类型的包装类和常量池

Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean
这 6 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术

  1. Integer i1 = 33;
  2. Integer i2 = 33;
  3. System.out.println(i1 == i2);// 输出 true
  4. Integer i11 = 333;
  5. Integer i22 = 333;
  6. System.out.println(i11 == i22);// 输出 false
  7. Double i3 = 1.2;
  8. Double i4 = 1.2;
  9. System.out.println(i3 == i4);// 输出 false

应用场景:
1.Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40); 从而使用常量池中的对象。
2.Integer i1 = new Integer(40);这种情况下会创建新的对象。

  1. Integer i1 = 40;
  2. Integer i2 = new Integer(40);
  3. Integer i3 = new Integer(40);
  4. System.out.println(i1==i2);//输出 false
  5. System.out.println(i1==i2);//输出 false

举例:

  1. Integer i4 = new Integer(40);
  2. Integer i5 = new Integer(40);
  3. Integer i6 = new Integer(0);
  4. System.out.println(i4 == i5); // false 两个对象比较
  5. System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); // i4=i5+i6 true

解释:
语句 i4 == i5 + i6,因为 + 这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。
自动装箱:相当于程序自动调用了 包装类的valueOf()
自动拆箱:相当于程序自动调用了 包装类的xxValue() xx指基本数据类型

8. JVM内存分配与回收

首先Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则进入S0或S1,且年龄+1(Eden->Survivior区后对象初始年龄变为1
动态对象年龄判定,作为晋升老年代的阈值:遍历所有对象,按年龄从小到大对其占用大小累积,累积到莫i个年龄超S区的一半,取这个年龄和MaxTenuringThreshold中更小的值,作为新的晋升年龄阈值。
(注意,有的说默认晋升年龄是15(因为最大占4bit,所以最大1111,就是15),也有的说这区分垃圾收集器,CMS是6)
经历GC后,Eden和From区已被清空,然后To和From交换角色,新的From就是上次GC前的To。这样保证To一直为空。Minor GC重复此过程,直到To区被填满,将所有对象移到老年代。

对象分配基本策略:
1.对象优先在Eden区分配——不足就发起Minor GC——S区也满了就通过分配担保机制转移到老年代
2.大对象直接进入老年代——因为需要连续内存空间(如字符串、数组),避免分配担保机制带来的复制降低效率
3.长期存活的对象将进入老年代——虚拟机给每个对象一个年龄计数器,前面讲的晋升年龄

主流垃圾收集器采用分代回收算法,根据各年代特点选择合适的垃圾收集算法。

GC分两大类
部分收集(Partial GC):
新生代收集(Minor GC/Young GC)
老年代收集(Major GC/Old GC) Major GC有时也指代整堆收集
混合收集(Mixed GC)对整个新生代和部分老年代
整堆收集(Full GC) 收集整个Java堆和方法区

9. GC如何判断对象可以被回收,什么是GC Root

9.1 定位垃圾的两种方式

image.png
image.png
GCRoot举例:1.静态变量。2.如果线程运行到一个方法,这个方法就作为栈帧在栈中,那么方法中new的对象就是个GC Roots,它使用的局部变量就不能被回收。方法调用完后,栈帧被弹出。
深入一点:可达性分析要两次
注意:可达性算法:一次判断GC Roots不可达,则标记,继续判断对象是否覆盖finalize方法,未覆盖则直接回收。(不建议使用finalize方法,运行代价太大,不确定性大,无法保证对象调用顺序)
通俗解释:
引用计数:无法解决循环引用,存在内存泄漏问题——该回收没回收
根可达:在内存中,从引用根对象向下一直找引用,找得到的是存活对象,找不到的对象就是垃圾
image.png解决了循环引用问题

9.2 什么是GC Root

GC Root包括:(可以理解,但不存在太多理解空间,建议直接背)
遍历的是栈中的,被弹出的不考虑
1.Stack(JVM、Native) 栈中的本地变量表中引用的对象,比如 new User()
2..class类 类静态属性引用的对象 public static String XXX = “XXXXX”
3.runtime constant pool 运行时常量池
4.static reference 静态变量

10. 强引用、软引用、弱引用、虚引用 引用队列

强:最普遍,绝不回收
软:内存够,就不回收 实现内存敏感的高速缓存(提高数据的获取速度)
弱:比软生命周期更短暂 垃圾回收器线程扫描,不管内存是否够,都回收 短时间缓存某些次要数据
虚(PhantomRefernce)/幽灵:若对象只有虚,则在任何时候都可能被垃圾回收 主要用来跟踪对象被垃圾回收的活动能够。使对象进入不可用状态,等待下次JVM垃圾回收,从而使对象进入引用列队中
引用队列:可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中
作用:通过引用队列可以了解JVM垃圾回收情况

  1. // 引用队列
  2. ReferenceQueue<String> rq = new ReferenceQueue<String>();
  3. // 软引用
  4. SoftReference<String> sr = new SoftReference<String>(new String("Soft"), rq);
  5. // 弱引用
  6. WeakReference<String> wr = new WeakReference<String>(new String("Weak"), rq);
  7. // 幽灵引用
  8. PhantomReference<String> pr = new PhantomReference<String>(new String("Phantom"), rq);
  9. // 从引用队列中弹出一个对象引用
  10. Reference<? extends String> ref = rq.poll();

11. 判断废弃常量和无用的类

运行时常量池主要回收废弃常量——没有任何对象引用该常量
无用的类三个条件:符合的可以被回收
1.类的所有实例都已被回收
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法

12. 垃圾收集算法——内存回收的方法论

12.1 标记-清除(Mark Sweep)

1.标记——不需回收的对象
2.清除——统一回收没被标记的对象
存在问题:
1.效率低
2.空间问题——产生大量不连续的碎片

12.2 复制算法(Copying)

解决效率和不连续碎片问题,每次内存回收都是对内存区间一半进行回收
1.将内存分为大小相同的两块,每次使用一块
2.一块内存使用完后,将还存活的对象复制到另一块,再把使用的空间清理
优点:没有内存碎片
缺点:浪费空间,效率和存活对象个数相关

12.3 标记-整理/压缩(Mark Compack)

1.标记
2.让所有存活对象向一端移动,再直接清理掉端边界以外的内存
解决复制算法对空间利用率的问题

以上三种各有适合的应用场景

12.4 分代收集算法

根据对象存活周期的不同将内存分块,分别选择合适的垃圾收集算法
通常:
新生代——复制算法——每次收集都有大量对象死亡,只需付出少量对象的复制成本
老年代——标记-清除/整理——对象存活几率高,没额外空间对其进行分配担保

13. 垃圾回收器——内存回收的具体实现

13.1 Serial收集器

串行收集器
单线程:垃圾收集时,必须暂停其他所有的工作线程(STW——Stop The World)
新生代——复制算法
老年代——标记-整理
缺点:停顿时间长,用户体验不良
优点:简单高效——因为没有线程交互的开销
适用于:运行在Client模式下的虚拟机
image.png

13.2 ParNew收集器

Serial的多线程版本
新生代——复制算法
老年代——标记-整理
适用于:运行在Server模式下的虚拟机
除Serial收集器外,只有它能与CMS收集器配合工作

13.3 Parallel Scavenge(并行清除)收集器

并行(Parallel):多条垃圾收集线程并行工作,此时用户线程仍然处于等待状态
并发(Concurrent):用户线程与垃圾收集线程同时执行,用户程序在继续运行,垃圾收集器运行在另一个CPU
也是使用复制算法的多线程收集器
关注点:吞吐量(高效率的利用CPU) = CPU中用于运行用户代码的时间 / CPU总消耗时间
Parallel Scavenge收集器 + 自适应调节策略——实现内存管理优化
JDK1.8默认收集器
image.png

13.4 Serial Old收集器

Serial收集器的老年代版本

13.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本

13.6 CMS收集器

CMS:Concurrent Mark Sweep Concurrent(同时,并发)
核心思想:将STW打散,让一部分GC线程与用户线程并发执行
目标:获取最短回收停顿时间,适合注重用户体验的应用
并发收集器:实现垃圾收集线程和用户线程同时工作
运作过程4步:
1.初始标记:暂停所有其他线程,SWT只标记出根对象直接引用的对象,速度快
2.并发标记继续标记其他对象,与应用程序并发执行
3.重新标记:STW修正并发标记期间因用户程序继续运行而导致标记产生变动的那部分对象的标记记录
4.并发清除:开启用户线程,同时GC线程对未标记的区域做清除(并行,将产生的垃圾清除

优点:并发收集低停顿
缺点:对CPU资源敏感、无法处理浮动垃圾使用标记-清除,产生大量不连续碎片
浮动垃圾:清除过程中,应用程序又会不断产生的新的垃圾,要留到下次GC过程中清除
image.png

13.7 G1收集器

G1:Garbage-First
有优先级的区域回收方式——在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region
面向服务器,针对配备多颗处理器及大容量内存的机器——多CPU多核
极高概率满足GC停顿时间要求的同时,具备高吞吐量性能特征
四个特点:
1.并行与并发
2.分代收集
3.空间整合——整体看基于标记-整理,局部看基于复制
4.可预测的停顿——相对CMS的一大优势,它在追求低停顿的同时,建立可预测的停顿时间模型

运行步骤:
1.初始标记
2.并发标记
3.最终标记
4.筛选回收
堆化成一个个region

13.8 ZGC收集器


14. 面试题

14.1 一个对象从加载到JVM,再到被GC清除,都经历了什么过程

image.png

14.2 JVM有哪些垃圾回收器?都是怎么工作的?什么是STW?他都发生在哪些阶段?什么是三色标记?如何既觉错标记和漏标记的问题?为什么要设计这么多垃圾回收器?

STW:
image.png
垃圾回收器:
image.png
其中分代算法分三种大的算法:
image.png

14.3 GC 垃圾回收过程(知识点整理)

  1. 判断对象是否已死亡,允许被回收 2种

引用计数法
可达性分析:两次标记——筛选、finalize
GC root:常量引用、静态变量引用、虚拟机栈引用、本地方法栈引用 的对象

  1. 垃圾回收器算法 4种

标记-清除
复制
标记-整理
分代收集

  1. 内存分配原则,不问不说,可省略

优先Eden
大对象、长时间直接老年代
动态对象年龄判定:MaxTenuringThreshold、15
空间分配原则 担保机制

  1. 垃圾收集器 6种

单线程/多线程 x 新生代/老年代 4种
CMS:目的:用户体验,STW最短
G1:面向服务端应用,多核

image.png

14.4 GC 标记的流程

image.png

15. Java内存模型(JMM)

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
image.png

1.主内存(Main Memory)——线程共有
主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

2.工作内存(Working Memory)——线程私有
工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

直接操作主存很慢
工作内存所更新的变量并不会立即同步到主内存——所以线程执行写操作,可能出现另一线程读取到之前的数据