1. JVM简介
JVM —— Java Virtual Machine的简称,Java虚拟机。
-
虚拟机:
- 通过软件模拟的具有完整硬件功能的、运行在一个完全独立的环境中的完整计算机系统。
-
JVM是一台被定制过的现实当中不存在的计算机。
2. Java内存区域与内存溢出异常
-
2.1 运行时数据区域
运行时数据区共有5个区域,这5个区域又分为两类:线程私有区域、线程共享区域
- 堆为什么是共享的?
- 对象在堆上,多线程访问同一个对象时,会产生资源竞争
- -> 为了防止竞争,导致资源的不安全与不一致,加锁或同步块。
- -> 堆是多线程共享的
- -> 为了防止竞争,导致资源的不安全与不一致,加锁或同步块。
- 对象在堆上,多线程访问同一个对象时,会产生资源竞争
-
2.2 程序计数器
程序计数器是一块比较小的内存空间,记录当前线程执行的位置。
- 程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(内存溢出)情况的区域。
- 因为程序计数器是个非常小的内存空间,只记录内存地址,4G内存用32位(4个字节)就可以表示。
- 2.2.1 程序计数器存在意义?
- 单核CPU上同时有多个线程执行,同一时间只有一个线程运行。只有当分到CPU的时间片时,线程从就绪变为运行。当CPU时间片用完后又回到就绪,这时就需要找到程序计数器所记录的当前线程执行的位置来继续执行。
- 2.2.2 计数器值
- 若当前线程执行的是Java方法,计数器记录当前线程执行的位置;若执行的是Native方法,计数器值为空。
- 2.2.3 线程私有
- 每个线程执行时都是独立的,在单核中同一时间只有一个线程能获得执行CPU的时间,为了记录执行的位置,才存在计数器。各个线程之间计数器互不影响,独立存储。类似这样的区域称之为“线程私有”内存。
-
2.3 Java虚拟机栈
描述的是Java方法执行的内存模型。虚拟机栈就是方法调用栈,记录方法调用的入跟出。
-
2.3.1 Java虚拟机栈会产生的两种异常
-
StackOverFlowError —— 栈溢出
-
方法不停地嵌套调用,不停地入栈。
-
方法递归没有出口,会产生栈溢出。
-
-
OOM:OutOfMemoryError —— 内存溢出
-
在方法中不停地开辟空间,创建对象。栈溢出之前,内存空间不够用 -> 内存溢出,无法创建对象。
-
-
-
2.4 本地方法栈
本地方法栈的作用同虚拟机栈,不同的是:
本地方法栈针对虚拟机使用的Native方法;
虚拟机栈针对的是JVM执行的Java方法。
-
在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。
-
2.5 Java堆
JVM管理的最大的内存区域,垃圾回收器主要管理的就是堆。
- Java堆在JVM启动时创建,堆上存放对象实例。
- JVM在堆上申请好用来存放对象的内存空间,当对象不被使用时,对象被放在堆上,等待回收。
- Java堆可以处于物理上不连续的内存空间中
- Java堆在虚拟机中是可扩展的,利用-Xms、-Xmx设置对的最小、最大值
-
2.6 方法区(元空间)
存储类信息(JVM要把Class文件加载进去,需要占用一定的空间)、常量、静态变量等数据。
- 当方法区无法满足内存分配需求时,抛出OOM异常。
-
2.7 运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
-
2.8 Java堆溢出
不断地创建对象,并且保证避免GC来清除这些对象,则在对象数量达到最大堆容量后就会产生内存溢出。
-
2.8.1 参数
-Xms:设置初始堆的最小值
-Xmx:设置初始堆的最大值
-
2.8.2 内存泄漏与内存溢出
Memory Leak 内存泄漏:泄漏对象无法被GC(无人使用也无法被回收)
Memory Overflow 内存溢出:内存对象扔或者,只是内存空间不够。可以将JVM堆内存空间调大或检查对象的生命周期是否过长(明明能在方法作用域中创建的对象,放在属性中去创建,该对象就会伴随类的实例化对象所引用,导致生命周期过长)
- 通过程序复现OOM
import java.util.ArrayList;
import java.util.List;
/**
* 堆溢出
* JVM参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
* 命令行中 :java -Xmx20m TestOOM
* Author:qqy
*/
public class TestOOM {
//定义一个类
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> data=new ArrayList<>();
//一直创建对象
while(true){
data.add(new OOMObject());
}
}
}
-
2.9 Java栈溢出
栈容量由-Xss参数来设置
-
2.9.1 两种异常
- 栈溢出
- 线程请求的栈深度大于虚拟机所允许的最大深度
- 内存溢出
- 虚拟机在拓展栈时无法申请到足够的内存空间
- 栈溢出
- 算法规定递归必须有出口。但即使有出口,程序也不一定能正常运行。只有栈空间够用时才可正常运行。
- 解决:优化程序或修改运行时参数
- 通过程序复现StackOverFlow
/**
* 栈溢出
* Author:qqy
*/
public class TestStack {
private int depth = 1;
public void test() {
depth++;
this.test();
}
public static void main(String[] args) {
TestStack test = new TestStack();
try {
test.test();
//栈溢出异常最终继承了Throwable,所以可以捕获Throwable
//不是受查异常
//继承runtime异常和error的子类,成为运行时异常
} catch (Throwable e) {
System.out.println("Stack Length: " + test.depth);
throw e;
}
}
}
3. 垃圾回收器与内存分配策略
由于程序计数器、虚拟机栈、本地方法栈这三个区域的生命周期随线程而生,随线程而亡,他们的内存分配与回收具有确定性。因此,此处研究的是共享区域,主要是堆和方法区。
-
3.1 判断对象是否已死
这一部分,单独写了一篇博客,有兴趣的小伙伴可以戳这个链接哦!
https://blog.csdn.net/qq_42142477/article/details/88773397
-
3.2 Java引用关系
强引用、软引用、弱引用、虚引用,这四种引用的强度依次递减。
- 强引用:只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
- 软引用:一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
- 弱引用:描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾回收发生之前
- 虚引用:也被称为幽灵引用、幻影引用,最弱的引用关系。设置虚引用的唯一目的:能在这个对象被回收时收到一个系统通知。
-
3.3 免死金牌 —— finalize()
想知道finalize()是如何成为一块免死金牌的? 戳 https://blog.csdn.net/qq_42142477/article/details/88773409
-
3.4 回收方法区
方法区(永久代)的垃圾回收主要收集: 废弃常量和无用的类。
- 回收方法区中的一个类,首先要回收这个类的所有实例化对象,也要回收类加载器(类是通过类加载器加载的)
- 无引用关系:类似于代码中有可能通过反射的方式传了一个类的字符串(反射出一个class)的关系
-
3.5 垃圾回收算法
垃圾回收算法有:
-
标记-清除算法
-
复制算法(新生代)
-
标记-整理算法(老年代)
-
分代收集算法
具体怎么回收? 喏 -> https://blog.csdn.net/qq_42142477/article/details/88773419
-
3.6 垃圾收集器
垃圾收集器就是内存回收的具体实现
- 了解三个概念
- 并行:同一时间多个线程同时执行(多核CPU上)
- 并发:同一时间多线程在同一个CPU上交替执行
- 吞吐量:用来计算当前程序的执行效率,垃圾收集时间越短(单核 -> 多核),吞吐量越大
- 吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
- HotSpot虚拟机包含的所有收集器,两个收集器之间存在的连线说明他们之间可以搭配使用
-
3.7 GC日志
每种收集起的日志格式都可不同,但都维持一定的共性
eg:
- 2015-05-26T14:45:37.987-0200 – GC开始时间
- 151.126 – GC发生的时间相对虚拟机启动的时间
- GC –并不是区分新生代GC还是老年代GC,而是说明停顿类型 -> 如果“Full”,说明这次GC时其他工作线程暂停
- Allocation Failure 分配失败 – 回收的原因,内存无法分配对象 -> 触发
- DefNew – 垃圾收集器名称
- 629119K->69888K – 新生代回收前后的使用情况
- (629120K) – 新生代总大小
- 1619346K->1273247K – Total used heap before and after collection.
- (2027264K) – 堆上收集前后的使用情况
- 0.0585007 secs – GC发生时间间隔
- [Times: user=0.06 sys=0.00, real=0.06 secs] – user :收集期间CPU时间 sys :操作系统等待花费的时间 real:程序真实停止的时间 (单核:user+sys=real 多核:user+sys>real (多线程操作会叠加CPU时间))
-
3.8 内存分配与回收策略
- 3.8.1 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发生一次Minor GC
/**
* JVM参数如下:
* -XX:+PrintGCDetails
* -XX:+UseSerialGC(使用Serial+Serial Old收集器组合)
* -Xms20M -Xmx20M -Xmn10M(设置新生代大小)
* -XX:SurvivorRatio=8(Eden:Survivor = 8 : 1)
* Author:qqy
*/
public class TestYGC {
private static final int _1MB = 1024 * 1024;
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
//1:1的部分放不下2MB,放入老年代,最大20M,老年代能放下
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
// 出现Minor GC
allocation4 = new byte[4 * _1MB];
}
public static void main(String[] args) throws Exception {
testAllocation();
}
}
- 3.8.2 大对象直接进入老年代
大对象 -> 需要大量连续空间的对象
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。避免Eden区以及两个Survivor区之间发生大量的内存复制。
/**
* JVM参数如下:
* -XX:+PrintGCDetails
* -XX:+UseSerialGC(使用Serial+Serial Old收集器组合)
* -Xms20M -Xmx20M -Xmn10M(设置新生代大小)
* -XX:SurvivorRatio=8(Eden:Survivor = 8 : 1)
* -XX:PretenureSizeThreshold = 3145728(此时不能写3MB)
* Author:qqy
*/
public class Test1 {
private static final int _1MB = 1024 * 1024;
public static void testAllocation() {
byte[] allocation;
allocation = new byte[4 * _1MB];
}
public static void main(String[] args) throws Exception {
testAllocation();
}
}
- 3.8.3 长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,新生代对象每在Survivor中逃过一次Minor GC,年龄+1,当年龄增长到年龄阈值时,晋升为老年代,年龄阈值通过参数-XX:MaxTenuringThreshold设置
4. Java内存模型
JVM定义了一种Java内存模型屏蔽掉各种硬件和操作系统的内存访问差异
Java内存模型主要定义了JVM中变量(实例字段、静态字段和构成数组对象的元素)如何存取。
-
4.1 主内存和工作内存
- 所有变量都存储在主内存
- 每条线程有自己的工作内存
- 线程对变量的所有操作必须在工作内存中进行
- 线程间变量值得传递军需要通过主内存完成
-
4.2 内存间交互操作
一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存等,由以下8种操作完成。
操作 | 作用于 | 功能 |
---|---|---|
lock(锁定) | 主内存的变量 | 把一个变量标识为一条线程独占的状态,一旦锁定其他线程无法访问 |
unlock(解锁) | 主内存的变量 | 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 |
read(读取) | 主内存的变量 | 把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用 |
load(载入) | 工作内存的变量 | 把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
use(使用) | 工作内存的变量 | 把工作内存中一个变量的值传递给执行引擎 |
assign(赋值) | 工作内存的变量 | 把一个从执行引擎接收到的值赋给工作内存的变量 |
store(存储) | 工作内存的变量 | 把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用 |
write(写入) | 主内存的变量 | 把store操作从工作内存中得到的变量的值放入主内存的变量中 |
- 内存模型三大特性:
- 原子性
- 基本数据类型的访问读写是具备原子性的
- 可见性
- 当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
- volatile、 synchronized、final可实现可见性
- 有序性
- 线程内表现为串行(有序),本线程中观察其他线程的操作都是无序的。
- 原子性
- 要想并发程序正确地执行,必须要保证原子性、可见性以及有序性
-
4.3 volatile型变量的特殊规则
当一个变量定义为volatile之后,它具备两种特性:保证此变量对所有线程的可见性、此变量禁止指令重排序。
如果在两个线程之间要通过一个变量来标识状态,就用volatile修饰变量,既能保证线程可见性,又能防止线程重排。
- 若变量本身是可见的,被volatile修饰的变量如果通过原子操作被修改了,仍可见。但不是原子性操作,则不可见。
/**
* volatile()
* Author:qqy
*/
public class TestVolatile {
private static volatile int num=0;
//num被volatile修饰,num内存可见
//但是num++不可见,因为不是原子性操作,分为两步 num=num+1,做运算再赋值
public static void add(){
num++;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
Thread thread=new Thread(()->{
add();
System.out.println(Thread.currentThread().getName()+" "+num);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.setName("Thread_Volatile_"+i);
thread.start();
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 禁止重排序
-
语句3一定是第三个执行的,但是不能保证语句1、2和语句4、5两只之间的执行顺序。
-
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5