第一章JVM概述
第一节 JDK、JRE、JVM关系回顾
- JVM:Java Virtual Machine,翻译过来是Java虚拟机
- JRE:Java Runtime Environment,翻译过来是Java运行时环境
- JRE=JVM+Java程序运行时所需要的类库
- JDK:Java Development Kits,翻译过来是Java开发工具包
- JDK=JRE+Java开发辅助工具
第二节 JVM工作的总体机制
1、Java源程序编译运行过程
Java源程序→编译→字节码文件→放到JVM上运行
2、JVM工作的总体机制
总体机制的粗略描述:
- 第一步:使用类加载器子系统将 *.class 字节码文件加载到 JVM 的内存
- 第二步:在 JVM 的内存空间存储相关数据
- 第三步:在执行引擎中将 *.class 字节码文件翻译成 CPU 能够执行的指令
- 第四步:将指令发送给CPU执行
3、JVM落地产品
- Sun公司的HotSpot
- BEA公司的JRockit
- IBM公司的J9 VM
第二章 类加载机制
第一节 类加载器分类
1、类加载器
类加载器(英文:ClassLoader)负责加载 .class 字节码文件,.class 字节码文件在文件开头有特定的文件标识。ClassLoader 只负责 *.class 字节码文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
2、说明
JVM 中类加载器分为四种:前三种为虚拟机自带的加载器。
-
启动类加载器(Bootstrap):使用 C++ 语言编写的类加载器,在Java环境下看不到
负责加载 $JAVA_HOME/jre/lib/rt.jar 里所有的 class。由 C++ 实现,不是 ClassLoader 子类
-
扩展类加载器(Extension):sun.misc.Launcher.ExtClassLoader
负责加载 Java 平台中扩展功能的一些 jar 包,包括 $JAVA_HOME/jre/lib/*.jar 或 -Djava.ext.dirs 参数指定目录下的 jar 包、以及 $JAVA_HOME/jre/lib/ext/classes 目录下的 class。
-
应用类加载器(AppClassLoader):sun.misc.Launcher.AppClassLoader
也叫系统类加载器,负责加载classpath中指定的 jar 包及目录中的 class
-
自定义类加载器:程序员自己开发一个类继承 java.lang.ClassLoader,定制类加载方式
父子关系1:启动类加载器是扩展类加载器的父加载器
父子关系2:扩展类加载器是应用类加载器的父加载器
3、代码测试
// 1.获取Person类的Class对象
// 2.通过Class对象进一步获取它的类加载器对象
ClassLoader appClassLoader = Person.class.getClassLoader();
// 3.获取appClassLoader的全类名
String appClassLoaderName = appClassLoader.getClass().getName();
// 4.打印appClassLoader的全类名
// sun.misc.Launcher$AppClassLoader
System.out.println("appClassLoaderName = " + appClassLoaderName);
// 5.通过appClassLoader获取扩展类加载器(父加载器)
ClassLoader extClassLoader = appClassLoader.getParent();
// 6.获取extClassLoader的全类名
String extClassLoaderName = extClassLoader.getClass().getName();
// 7.打印extClassLoader的全类名
// sun.misc.Launcher$ExtClassLoader
System.out.println("extClassLoaderName = " + extClassLoaderName);
// 8.通过extClassLoader获取启动类加载器(父加载器)
ClassLoader bootClassLoader = extClassLoader.getParent();
// 9.由于启动类加载器是C语言开发的,在Java代码中无法实例化对象,所以只能返回null值
System.out.println("bootClassLoader = " + bootClassLoader);
第二节 双亲委派机制
- 当我们需要加载任何一个范围内的类时,首先找到这个范围对应的类加载器
- 但是当前这个类加载器不是马上开始查找
- 当前类加载器会将任务交给上一级类加载器
- 上一级类加载器继续上交任务,一直到最顶级的启动类加载器
- 启动类加载器开始在自己负责的范围内查找
- 如果能找到,则直接开始加载
- 如果找不到,则交给下一级的类加载器继续查找
- 一直到应用程序类加载器
- 如果应用程序类加载器同样找不到要加载的类,那么会抛出ClassNotFoundException
第三节 验证双亲委派机制
1、实验1
- 第一步:在与JDK无关的目录下创建Hello.java
public class Hello {
public static void main(String[] args){
System.out.println("AAA");
}
}
- 第二步:编译Hello.java
- 第三步:将Hello.class文件移动到$JAVA_HOME/jre/classes目录下
- 第四步:修改Hello.java
public class Hello {
public static void main(String[] args){
System.out.println("BBB");
}
}
- 第五步:编译Hello.java
- 第六步:将Hello.class文件移动到$JAVA_HOME/jre/lib/ext/classes目录下
- 第七步:修改Hello.java
public class Hello {
public static void main(String[] args){
System.out.println("CCC");
}
}
- 第八步:编译Hello.java
- 第九步:使用java命令运行Hello类,发现打印结果是:AAA
- 说明Hello这个类是被启动类加载器找到的,找到以后就不查找其他位置了
- 第十步:删除$JAVA_HOME/jre/classes目录
- 第十一步:使用java命令运行Hello类,发现打印结果是:BBB
- 说明Hello这个类是被扩展类加载器找到的,找到以后就不查找其他位置了
- 第十二步:删除$JAVA_HOME/jre/lib/ext/classes目录
- 第十三步:使用java命令运行Hello类,发现打印结果是:CCC
- 说明Hello这个类是被应用程序类加载器找到的
2、实验2
- 第一步:创建假的String类
package java.lang;
public class String {
public String() {
System.out.println("嘿嘿,其实我是假的!");
}
}
- 第二步:编写测试程序类
@Test
public void testLoadString() {
// 目标:测试不同范围内全类名相同的两个类JVM如何加装
// 1.创建String对象
java.lang.String testInstance = new java.lang.String();
// 2.获取String对象的类加载器
ClassLoader classLoader = testInstance.getClass().getClassLoader();
System.out.println(classLoader);
}
- 第三步:查看运行结果是null
- 假的String类并没有被创建对象,由于双亲委派机制,启动类加载器加载了真正的String类
第四节 双亲委派机制的好处
- 避免类的重复加载:父加载器加载了一个类,就不必让子加载器再去查找了。同时也保证了在整个 JVM 范围内全类名是类的唯一标识。
- 安全机制:避免恶意替换 JRE 定义的核心 API
第三章 总体机制中不重要的部分
1、本地接口 Native Interface
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序。因为 Java 诞生的时候是 C/C++ 横行的时候,要想立足,必须有能力调用 C/C++。于是就在内存中专门开辟了一块区域处理标记为 native 的代码,它的具体做法是 Native Method Stack 中登记 native 方法,在Execution Engine 执行时加载 native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机或者 Java 系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。
2、本地方法栈 Native Method Stack
专门负责在本地方法运行时,提供栈空间,存放本地方法每一次执行时创建的栈帧。它的具体做法是在 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库。
native 方法举例:
public static native void yield();
2、程序计数器
也叫PC寄存器(Program Counter Register)。用于保存程序执行过程中,下一条即将执行的指令的地址。也就是说能够保存程序当前已经执行到的位置。这个位置由执行引擎读取下一条指令,是一个非常小的内存空间,从内存空间使用优化这个角度来看:几乎可以忽略不记。
3、执行引擎 Execution Engine
作用:用于执行字节码文件中的指令。
执行指令的具体技术:
- 解释执行:第一代JVM。
- 即时编译:JIT,第二代JVM。
- 自适应优化:目前Sun的Hotspot JVM采用这种技术。吸取了第一代JVM和第二代JVM的经验,在一开始的时候对代码进行解释执行, 同时使用一个后台线程监控代码的执行。如果一段代码经常被调用,那么就对这段代码进行编译,编译为本地代码,并进行执行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
- 芯片级直接执行:内嵌在芯片上,用本地方法执行Java字节码。
4、直接内存
作用
提高特定场景下性能。
应用场景
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 本机直接内存的分配不会受到 Java 堆大小的限制,受到本机总内存大小限制。 配置虚拟机参数时,不要忽略直接内存防止出现 OutOfMemoryError 异常。
直接内存(堆外内存)与堆内存比较
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。直接内存 I/O 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。
第四章 方法区
1、不同版本具体实现
- 标准层面:方法区(Method Area)
- 具体实现层面:
- ≤1.6 永久代
- =1.7 永久代仍然存在,但是已经开始提出:去永久代
- ≥1.8元空间(Meta Space)
TIP
永久代概念辨析:
- 从堆空间角度来说
- 新生代:从标准和实现层面都确定属于堆
- 老年代:从标准和实现层面都确定属于堆
- 永久代
- 名义上属于堆
- 实现上不属于堆
- 从方法区角度来说
- 方法区的具体实现:JDK 版本 ≤ 1.7 时,使用永久代作为方法区。
- 方法区的具体实现:JDK 版本 ≥ 1.8 时,使用元空间作为方法区。
2、元
本身含义:万物初始,一件事情的源头或基本组成部分。
举例:元素、元始天尊、每年1月称为元月、1月1日称为元旦、元认知、元无知、元知识
对比类和对象,类相当于是对象的元信息。
3、元空间存储数据说明
- 类信息:类中定义的构造器、接口定义
- 静态变量(类变量)
- 常量
- 运行时常量池
- 类中方法的代码
第五章 Java栈 [重要]
第一节 方法栈
方法栈并不是某一个 JVM 的内存空间,而是我们描述方法被调用过程的一个逻辑概念。
在同一个线程内,method01()调用method02():
- method01()先开始,method02()后开始;
- method02()先结束,method01()后结束。
TIP
『栈』和『堆』这两个字辨析:
1、从英文单词角度来说
- 栈:stack
- 堆:heap
2、从数据结构角度来说
- 栈和堆一样:都是先进后出,后进先出的数据结构
3、从 JVM 内存空间结构角度来说
- 栈:通常指 Java 方法栈,存放方法每一次执行时生成的栈帧。
- 堆:JVM 中存放对象的内存空间。包括新生代、老年代、永久代等组成部分。
第二节 栈帧
1、栈帧存储的数据
方法在本次执行过程中所用到的局部变量、动态链接、方法出口等信息。栈帧中主要保存3 类数据:
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
- 栈操作(Operand Stack):记录出栈、入栈的操作。
- 栈帧数据(Frame Data):包括类文件、方法等等。
2、栈帧的结构
- 局部变量表:方法执行时的参数、方法体内声明的局部变量
- 操作数栈:存储中间运算结果,是一个临时存储空间
- 帧数据区:保存访问常量池指针,异常处理表
3、栈帧工作机制
当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,
A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,
B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈,
……
C 方法执行完毕后,弹出 F3 栈帧;
B 方法执行完毕后,弹出 F2 栈帧;
A 方法执行完毕后,弹出 F1栈帧;
……
遵循“先进后出”或者“后进先出”原则。
图示在一个栈中有两个栈帧:
栈帧 2 是最先被调用的方法,先入栈,
然后方法 2 又调用了方法 1,栈帧 1 处于栈顶的位置,
栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1 和栈帧 2,
线程结束,栈释放。
每执行一个方法都会产生一个栈帧,保存到栈的顶部,顶部栈就是当前方法,该方法执行完毕后会自动将此栈帧出栈。
4、典型案例
请预测下面代码打印的结果:34
int n = 10;
n += (n++) + (++n);
System.out.println(n);
实际执行结果:32
使用 javap 命令查看字节码文件内容:
D:\record-video-original\day03\code>javap -c Demo03JavaStackExample.class
Compiled from “Demo03JavaStackExample.java”
public class Demo03JavaStackExample{
public Demo03JavaStackExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.": ()V
4: returnpublic static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: iload_1
4: iload_1
5: iinc 1, 1
8: iinc 1, 1
11: iload_1
12: iadd
13: iadd
14: istore_1
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
22: return
}
内存执行过程分析:
第三节 栈溢出异常
1、异常名称
java.lang.StackOverflowError
2、异常产生的原因
下面的例子是一个没有退出机制的递归:
public class StackOverFlowTest {
public static void main(String[] args) {
methodInvokeToDie();
}
public static void methodInvokeToDie() {
methodInvokeToDie();
}
}
抛出的异常信息:
Exception in thread “main” java.lang.StackOverflowError at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10)
原因总结:方法每一次调用都会在栈空间中申请一个栈帧,来保存本次方法执行时所需要用到的数据。但是一个没有退出机制的递归调用,会不断申请新的空间,而又不释放空间,这样迟早会把当前线程在栈内存中自己的空间耗尽。
第四节 栈空间的线程私有验证
1、提出问题
某一个线程抛出『栈溢出异常』,会导致其他线程也崩溃吗?从以往的经验中我们判断应该是不会,下面通过代码来实际验证一下。
2、代码
new Thread(()->{
while(true) {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " working");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread-01").start();
new Thread(()->{
while(true) {
try {
TimeUnit.SECONDS.sleep(2);
// 递归调用一个没有退出机制的递归方法
methodInvokeToDie();
System.out.println(Thread.currentThread().getName() + " working");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread-02").start();
new Thread(()->{
while(true) {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " working");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread-03").start();
3、结论
02 线程抛异常终止后,01 和 03 线程仍然能够继续正常运行,说明 02 抛异常并没有影响到 01 和 03,说明线程对栈内存空间的使用方式是彼此隔离的。每个线程都是在自己独享的空间内运行,反过来也可以说,这个空间是当前线程私有的。
第六章 堆 [重要]
第一节 概述
1、堆空间组成部分
2、堆空间工作机制
- 新创建的对象会被放在Eden区
- 当Eden区中已使用的空间达到一定比例,会触发Minor GC
- 每一次在Minor GC中没有被清理掉的对象就成了幸存者
- 幸存者对象会被转移到幸存者区
- 幸存者区分成from区和to区
- from区快满的时候,会将仍然在使用的对象转移到to区
- 然后from和to这两个指针彼此交换位置
口诀:复制必交换,谁空谁为to
- 如果一个对象,经历15次GC仍然幸存,那么它将会被转移到老年代
- 如果幸存者区已经满了,即使某个对象尚不到15岁,仍然会被移动到老年代
- 最终效果:
- Eden区主要是生命周期很短的对象来来往往
- 老年代主要是生命周期很长的对象,例如:IOC容器对象、线程池对象、数据库连接池对象等等
- 幸存者区作为二者之间的过渡地带
- 关于永久代:
- 从理论上来说属于堆
- 从具体实现上来说不属于堆
3、永久代在各个JDK版本之间的演变
永久代 | 常量池 | |
---|---|---|
≤JDK1.6 | 有 | 在方法区 |
=JDK1.7 | 有,但开始逐步“去永久代” | 在堆 |
≥JDK1.8 | 无 | 在元空间 |
4、方法区、元空间、永久代之间关系
5、堆、栈、方法区之间关系
第二节 实验
Java代码:
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
附加 JVM 运行参数:
- 新创建参数设置:
- 修改参数设置:
- 设置参数:
- 运行程序的操作还是和以前一样:
第三节 堆溢出异常
1、异常名称
java.lang.OutOfMemoryError,也往往简称为 OOM。
2、异常信息
- Java heap space:针对新生代、老年代整体进行Full GC后,内存空间还是放不下新产生的对象
- PermGen space:方法区中加载的类太多了(典型情况是框架创建的动态类太多,导致方法区溢出)
我们可以参考下面的控制台日志打印:
[GC (Allocation Failure) 4478364K->4479044K(5161984K), 4.3454766 secs] [Full GC (Ergonomics) 4479044K->3862071K(5416448K), 39.3706285 secs] [Full GC (Ergonomics) 4410423K->4410422K(5416448K), 27.7039534 secs] [Full GC (Ergonomics) 4629575K->4621239K(5416448K), 24.9298221 secs] [Full GC (Allocation Failure) 4621239K->4621186K(5416448K), 29.0616791 secs] Exception in thread “main” java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.atguigu.jvm.test.JavaHeapTest.main(JavaHeapTest.java:16)
第四节 小练习
1、测试代码
查看下面程序在每个步骤中内存的状态:
public class Review {
// 静态变量,类变量
public static Review review = new Review();
public void showMessage() {
// 局部变量
Review reviewLocal = new Review();
}
// 程序入口
public static void main(String[] args) {
// 局部变量
Review reviewMain = new Review();
// 通过局部变量调用对象的方法
reviewMain.showMessage();
// 手动 GC
System.gc();
}
}
2、各状态分析
①状态1:执行到 showMessage() 方法中
②状态2:showMessage() 方法结束
③状态3:执行一次 GC
④状态4:main() 方法结束
⑤状态5:执行一次 GC
⑥状态6:执行过第 15 次 GC 后
第七章 可视化工具
第一节 jconsole
1、所在路径
%JAVA_HOME%/bin/jconsole.exe
2、使用方法
双击打开,选择要监控的进程:
如果想要重新打开新建连接窗口可以点击菜单项:
可以选择内存选项卡,在图表下拉列表中选择想要查看的具体的内存区域:
第二节 jvisualvm
1、所在路径
%JAVA_HOME%/bin/jvisualvm.exe
2、使用方法
第八章 垃圾回收 [重要]
第一节 GC的基本问题
-
什么是GC?
GC 是 garbage collection 的缩写,意思是垃圾回收——把内存(特别是堆内存)中不再使用的空间释放掉;清理不再使用的对象。
-
为什么要GC?
堆内存是各个线程共享的空间,不能无节制的使用。服务器运行的时间通常都很长。累积的对象也会非常多。这些对象如果不做任何清理,任由它们数量不断累加,内存很快就会耗尽。所以GC就是要把不使用的对象都清理掉,把内存空间空出来,让项目可以持续运行下去。
-
什么样的对象是垃圾对象?
不再使用或获取不到的对象是垃圾对象。
-
如何把垃圾对象找出来?
办法1:引用计数法(不采用,不能解决循环引用问题)[了解]
办法2:可达性分析(从GC Roots对象出发,不可达的对象就是要清理的对象)[理解]
-
找到垃圾对象如何执行清理?
具体的GC算法
第二节 标记垃圾对象
1、引用计数法(不采用)
①工作机制
引用计数法是在对象每一次被引用时,都给这个对象专属的『引用计数器』+1。
当前引用被取消时,就给这个『引用计数器』-1。
当前『引用计数器』为零时,表示这个对象不再被引用了,需要让GC回收。
可是当对象之间存在交叉引用的时候,对象即使处于应该被回收的状态,也没法让『引用计数器』归零。
Member member01 = new Member();
Member member02 = new Member();
member01.setFriend(member02);
member02.setFriend(member01);
member01 = null;
member02 = null;
引用计数法的关键问题:该清理的对象清理不掉。
②循环引用举例
[1]一对多关联关系
public class Customer {
private List<Order> orderList;
public List<Order> getOrderList() {
return orderList;
}
public void setOrderList(List<Order> orderList) {
this.orderList = orderList;
}
}
public class Order {
private Customer customer;
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
[2]SpringMVC 组件
- IOC 容器对象的接口类型:WebApplicationContext
- WebApplicationContext 对象初始化过程中:将它自己存入 ServletContext 域
- WebApplicationContext 对象也会把 ServletContext 存入 IOC 容器
- Servlet 上下文对象:ServletContext
2、GC Roots可达性分析
核心原理:判断一个对象,是否存在从**『堆外』到『堆内』**的引用。
请看下面的例子:
①加载Employee类
class Employee {
public static final String SUBJECT = new String("Java");
}
②创建Employee对象并赋值给变量
public static void main(String[] args) {
// 1.创建Employee对象,并赋值给employee变量
Employee employee = new Employee();
}
3、GC Root 对象
GC Root 对象:就是作为根节点出发,顺着引用路径一直查找到堆空间内,找到堆空间中的对象。
- Java 栈中的局部变量
- 本地方法栈中的局部变量
- 方法区中的类变量、常量
第三节 垃圾回收算法
基本垃圾回收算法有四种:引用计数法、标记清除法、标记压缩法、复制算法。现代流行的垃圾收集算法一般是由这四种中的其中几种算法相互组合而成。例如:分代算法、分区算法。
这里又看到一个『引用计数法』,但是和前面提到的不一样:
前面『引用计数法』:是标记垃圾对象的一种方法
这里『引用计数法』:是执行垃圾回收的一种方法
1、基本算法:引用计数法
引用计数算法很简单,它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
引用计数垃圾收集机制,它只是在引用计数变化为0时即刻发生,而且只针对某一个对象以及它所依赖的其它对象。所以,我们一般也称呼引用计数垃圾收集为直接的垃圾收集机制。垃圾收集的开销被分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。
优点:
- 实时性较高,不需要等到内存不够时才回收
- 垃圾回收时不用挂起整个程序,不影响程序正常运行
缺点:
- 回收时不移动对象, 所以会造成内存碎片问题
- 不能解决对象间的循环引用问题
小结:
正是由于引用计数法不能解决对象间的循环引用问题,所以事实上并没有哪一款 JVM 产品采用这个机制。
TIP
Stop-The-World:字面意思让整个世界停止。在 GC 机制中,Stop-The-World 表示挂起整个 JVM 程序,等执行完垃圾回收之后,再继续执行 JVM 程序。Stop-The-World 通常也会简称为 STW。
2、基本算法:标记清除法
它的做法是当堆中的有效内存空间被耗尽的时候,就会暂停、挂起整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:标记的过程其实就是,从根对象开始遍历所有的对象,然后将所有存活的对象标记为可达的对象。
- 清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
小结:
- 优点:实现简单
- 缺点:
- 效率低,因为标记和清除两个动作都要遍历所有的对象
- 垃圾收集后有可能会造成大量的内存碎片
- 垃圾回收时会造成应用程序暂停
3、基本算法:标记压缩法
既然叫标记压缩算法,那么它也分为两个阶段,一个是标记(mark),一个是压缩(compact)。所谓压缩就是把存在碎片的空间连起来。
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象移动到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。
标记 : 标记的过程其实就是,从根对象开始遍历所有的对象,然后将所有存活的对象标记为可达的对象。
压缩 : 移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起,通过这样的方式来达到减少内存碎片的目的。
小结
优点:标记压缩算法是对标记清除算法的优化,解决了碎片化的问题
缺点:还是效率问题,在标记清除算法上又多加了一步,效率可想而知了
4、基本算法:复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,并依次排列,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
小结
- 优点1:在垃圾多的情况下(新生代),效率较高
- 优点2:清理后,内存无碎片
- 缺点:浪费了一半的内存空间,在存活对象较多的情况下(老年代),效率较差
执行GC前:
执行标记:
执行复制:
交换指针:
5、综合算法:分代算法
前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的。
分代算法其实就是这样的,根据回收对象的特点进行选择。
- 新生代适合使用复制算法
- 老年代适合使用标记清除或标记压缩算法
6、综合算法:分区算法
上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分,而分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间。在相同条件下,堆空间越大。一次GC耗时就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标停顿时间每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。
第四节 垃圾回收器 [了解]
1、串行垃圾回收器
串行:在一个线程内执行垃圾回收操作。
新生代串行回收器 SerialGC:采用复制算法实现,单线程垃圾回收,独占式垃圾回收器
老年代串行回收器 SerialOldGC:采用标记压缩算法,单线程独占式垃圾回收器
2、并行垃圾回收器
并行:在多个线程中执行垃圾回收操作。
新生代 ParNew 回收器:采用复制算法实现,多线程回收器,独占式垃圾回收器。
新生代 ParallelScavengeGC 回收器:采用复制算法多线程独占式回收器
老年代 ParallelOldGC 回收器: 采用标记压缩算法,多线程独占式回收器
-
CMS回收器
CMS全称 (Concurrent Mark Sweep),是一款并发的、使用标记-清除算法的垃圾回收器。对CPU资源非常敏感。
启用CMS回收器参数 :-XX:+UseConcMarkSweepGC。
使用场景:GC过程短暂停顿,适合对时延要求较高的服务,用户线程不允许长时间的停顿。
优点:最短回收停顿时间为目标的收集器。并发收集,低停顿。
缺点:服务长时间运行,造成严重的内存碎片化。算法实现比较复杂。
-
G1回收器
G1(Garbage-First)是一款面向服务端应用的并发垃圾回收器, 主要目标用于配备多颗CPU的服务器,治理大内存。是JDK1.7提供的一个新收集器,是当今收集器技术发展的最前沿成果之一。
G1计划是并发标记-清除收集器的长期替代品。
启用G1收集器参数:-XX:+UseG1GC启用G1收集器。
G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合。
每块区域既有可能属于Old区、也有可能是Young区,因此不需要一次就对整个老年代/新生代回收。而是当线程并发寻找可回收的对象时,有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程,但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源)。这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率。
特点:
- 一整块堆内存被分成多个独立的区域Regions
- 存活对象被拷贝到新的Survivor区
- 新生代内存由一组不连续的堆heap区组成,使得可以动态调整各个区域
- 多线程并发GC
- young GC会有STW(Stop the world)事件
3、垃圾回收器对比
①新生代回收器
名称 | 串行/并行/并发 | 回收算法 | 适用场景 | 可以与CMS配合 |
---|---|---|---|---|
SerialGC | 串行 | 复制 | 单CPU | 是 |
ParNewGC | 并行 | 复制 | 多CPU | 是 |
ParallelScavengeGC | 并行 | 复制 | 多CPU且关注吞吐量 | 否 |
②老年代回收器
名称 | 串行/并行/并发 | 回收算法 | 适用场景 |
---|---|---|---|
SerialOldGC | 串行 | 标记压缩 | 单CPU |
ParNewOldGC | 并行 | 标记压缩 | 多CPU |
CMS | 并发,几乎不会暂停用户线程 | 标记清除 | 多CPU且与用户线程共存 |
第五节 finalize 机制
1、总体机制介绍
java.lang.Object 类中有一个方法:
protected void finalize() throws Throwable { }
方法体内是空的,说明如果子类不重写这个方法,那么不执行任何逻辑。
- 在执行 GC 操作前,调用 finalize() 方法的是 Finalizer 线程,这个线程优先级很低。
- 在对象的整个生命周期过程中,finalize() 方法只会被调用一次。
2、代码验证
public class FinalizeTest {
// 静态变量
public static FinalizeTest testObj;
@Override
protected void finalize() throws Throwable {
// 重写 finalize() 方法
System.out.println(Thread.currentThread().getName() + " is working");
// 给待回收的对象(this)重新建立引用
testObj = this;
}
public static void main(String[] args) {
// 1、创建 FinalizeTest 对象
FinalizeTest testObj = new FinalizeTest();
// 2、取消引用
testObj = null;
// 3、执行 GC 操作
System.gc();
// ※ 让主线程等待一会儿,以便调用 finalize() 的线程能够执行
try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {}
// 4、判断待回收的对象是否存在
if (FinalizeTest.testObj == null) {
System.out.println("待回收的对象没有获救,还是要被 GC 清理");
} else {
System.out.println("待回收的对象被成功解救");
}
// 5、再次取消引用
FinalizeTest.testObj = null;
// 6、再次执行 GC 操作
System.gc();
// 7、判断待回收的对象是否存在
if (FinalizeTest.testObj == null) {
System.out.println("待回收的对象没有获救,还是要被 GC 清理");
} else {
System.out.println("待回收的对象被成功解救");
}
}
}
执行效果:
Finalizer is working
待回收的对象被成功解救
待回收的对象没有获救,还是要被 GC 清理
第九章 JVM参数设置入门
1、Runtime类使用案例
System.out.print("最大堆大小:");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================================================");
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
执行前配置参数:-Xmx50m -Xms30m -XX:+PrintGCDetails
执行看到如下信息:
新生代和老年代的堆大小之和是Runtime.getRuntime().totalMemory()
2、GC演示
System.out.println("=====================Begin=========================");
System.out.print("最大堆大小:Xmx=");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================First Allocated===================");
byte[] b1 = new byte[5 * 1024 * 1024];
System.out.println("5MB array allocated");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("=================Second Allocated===================");
byte[] b2 = new byte[10 * 1024 * 1024];
System.out.println("10MB array allocated");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("=====================OOM=========================");
System.out.println("OOM!!!");
System.gc();
System.out.println("第一个 40M 数组");
byte[] b3 = new byte[40 * 1024 * 1024];
System.out.println("第二个 40M 数组");
byte[] b4 = new byte[40 * 1024 * 1024];
System.out.println("第三个 40M 数组");
byte[] b5 = new byte[40 * 1024 * 1024];
JVM参数设置成最大堆内存100M,当前堆内存10M:-Xmx100m -Xms10m -XX:+PrintGCDetails
再次运行,可以看到minor GC和full GC日志:
第十章 相关面试题
1、面试回答问题过程
2、各个问题思路
①请谈谈你对 JVM 的理解?
- JVM 技术的作用:提升、优化项目性能
- JVM 结构
- 类加载机制
- Java栈
- 堆
- 新生代
- Eden 区
- 幸存者区:S0+S1
- 老年代
- 新生代
- 元空间
- GC
- 垃圾对象的标记
- 引用技术法
- GC Roots:概念→原理(堆外指向堆内)→GC Root 对象
- 垃圾回收算法
- 垃圾对象的标记
- JVM 常用参数
②Java 8 的虚拟机有什么更新?
- 永久代取消,变成元空间;
- 常量池放入到元空间,但是一直都是在方法区中。
③什么是OOM?
- 概念:堆内存溢出错误
- 全类名:java.lang.OutofMemoryError
- 产生原因:经过多轮 GC,连老年代都已经没了空间,还要继续创建新的对象。
- 详细过程:堆空间对象存储、GC 流程。
- 相关拓展:StackOverFlowError
④什么是StackOverflowError?
- 概念:栈内存溢出错误
- 产生原因:在一个线程中,栈内存分配给它的空间被不断新增的栈帧填满,没有及时释放栈帧。
- 详细过程:栈帧概念,栈帧中存储的数据,典型情况(无法退出的递归)
- 相关拓展:OOM
⑤ JVM 的常用参数调优你知道哪些?
参数名 | 参数作用 |
---|---|
-Xms | ★堆内存的初始大小 |
-Xmx | ★堆内存的最大值(最多能够向操作系统申请多少) |
-Xmn | 新生代大小 |
-XX:PermSize | 设置永久代(perm gen)初始值 |
-XX:MaxPermSize | 设置持久代最大值 |
-Xss | 每个线程的堆栈大小 |
-XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值 |
TIP
-Xss 设置每个线程在Java 栈中占用空间的大小。
线程数量 = Java 栈空间 / 每个线程空间(-Xss 参数设置的值)
线程深度 = 每个线程空间 / 每个栈帧空间
如果项目希望线程深度更大,那么应该给每个线程设置较大空间;
如果项目线程深度不大,线程数量很多,那么应该给每个线程设置较小空间。
线程深度:在一个线程中能够连续调用的方法的数量。
⑥谈谈 JVM 中,对类加载器你的认识?
双亲委派机制
- 类加载器分类
- 启动类加载器
- 扩展类加载器
- 应用类加载器
- 自定义加载器
- 父子关系
- 关系一:启动类加载器是扩展类加载器的父亲
- 关系二:扩展类加载器是应用类加载器的父亲
- 查找机制:先让父加载器查找,如果找不到再让子加载器加载
- 好处
- 好处1:避免重复操作
- 好处2:避免恶意替换核心 API
⑦堆内存空间大小设置的建议。
- 最大堆内存和初始化堆内存设置为一致:直接一步到位设置为能够使用的最大值,免除频繁申请空间的操作。
- 空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
- 空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
- 新生代大小:Sun官方推荐配置为整个堆的 3/8
TIP
如果咱妈给咱们设置的每个月零花钱的额度是 1000 元,那么为了少去烦她老人家,咱们一开始就是申请 1000 元全部。
⑧Java 栈每个线程占用空间大小如何设置。
JDK5.0 以后每个线程所占用的栈内存大小为 1M,以前是 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。一般小的应用, 如果栈不是很深, 应该是128k够用的。大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
⑨关于 finalize 机制,下面两种说法正确的是:
A 调用 finalize() 方法的效果是把一个已经被 GC 销毁的对象恢复过来。 B 调用 finalize() 方法的效果是针对一个即将被 GC 销毁的对象,在销毁之前调用 finalize() 方法。
|
| -Xss | 每个线程的堆栈大小 |
| -XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) |
| -XX:SurvivorRatio | Eden区与Survivor区的大小比值 |
TIP
-Xss 设置每个线程在Java 栈中占用空间的大小。
线程数量 = Java 栈空间 / 每个线程空间(-Xss 参数设置的值)
线程深度 = 每个线程空间 / 每个栈帧空间
如果项目希望线程深度更大,那么应该给每个线程设置较大空间;
如果项目线程深度不大,线程数量很多,那么应该给每个线程设置较小空间。
线程深度:在一个线程中能够连续调用的方法的数量。
⑥谈谈 JVM 中,对类加载器你的认识?
双亲委派机制
- 类加载器分类
- 启动类加载器
- 扩展类加载器
- 应用类加载器
- 自定义加载器
- 父子关系
- 关系一:启动类加载器是扩展类加载器的父亲
- 关系二:扩展类加载器是应用类加载器的父亲
- 查找机制:先让父加载器查找,如果找不到再让子加载器加载
- 好处
- 好处1:避免重复操作
- 好处2:避免恶意替换核心 API
⑦堆内存空间大小设置的建议。
- 最大堆内存和初始化堆内存设置为一致:直接一步到位设置为能够使用的最大值,免除频繁申请空间的操作。
- 空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
- 空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
- 新生代大小:Sun官方推荐配置为整个堆的 3/8
TIP
如果咱妈给咱们设置的每个月零花钱的额度是 1000 元,那么为了少去烦她老人家,咱们一开始就是申请 1000 元全部。
⑧Java 栈每个线程占用空间大小如何设置。
JDK5.0 以后每个线程所占用的栈内存大小为 1M,以前是 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。一般小的应用, 如果栈不是很深, 应该是128k够用的。大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
⑨关于 finalize 机制,下面两种说法正确的是:
A 调用 finalize() 方法的效果是把一个已经被 GC 销毁的对象恢复过来。 B 调用 finalize() 方法的效果是针对一个即将被 GC 销毁的对象,在销毁之前调用 finalize() 方法。