JVM —— Java Virtual Machine的简称,Java虚拟机。
虚拟机:
JVM是一台被定制过的现实当中不存在的计算机。
运行时数据区共有5个区域,这5个区域又分为两类:线程私有区域、线程共享区域
程序计数器是一块比较小的内存空间,记录当前线程执行的位置。
描述的是Java方法执行的内存模型。虚拟机栈就是方法调用栈,记录方法调用的入跟出。
2.3.1 Java虚拟机栈会产生的两种异常
StackOverFlowError —— 栈溢出
方法不停地嵌套调用,不停地入栈。
方法递归没有出口,会产生栈溢出。
OOM:OutOfMemoryError —— 内存溢出
在方法中不停地开辟空间,创建对象。栈溢出之前,内存空间不够用 -> 内存溢出,无法创建对象。
本地方法栈的作用同虚拟机栈,不同的是:
本地方法栈针对虚拟机使用的Native方法;
虚拟机栈针对的是JVM执行的Java方法。
在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。
JVM管理的最大的内存区域,垃圾回收器主要管理的就是堆。
存储类信息(JVM要把Class文件加载进去,需要占用一定的空间)、常量、静态变量等数据。
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK1.7后移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
不断地创建对象,并且保证避免GC来清除这些对象,则在对象数量达到最大堆容量后就会产生内存溢出。
2.8.1 参数
-Xms:设置初始堆的最小值
-Xmx:设置初始堆的最大值
2.8.2 内存泄漏与内存溢出
Memory Leak 内存泄漏:泄漏对象无法被GC(无人使用也无法被回收)
Memory Overflow 内存溢出:内存对象扔或者,只是内存空间不够。可以将JVM堆内存空间调大或检查对象的生命周期是否过长(明明能在方法作用域中创建的对象,放在属性中去创建,该对象就会伴随类的实例化对象所引用,导致生命周期过长)
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 data=new ArrayList<>();
//一直创建对象
while(true){
data.add(new OOMObject());
}
}
}
栈容量由-Xss参数来设置
2.9.1 两种异常
/**
* 栈溢出
* 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;
}
}
}
由于程序计数器、虚拟机栈、本地方法栈这三个区域的生命周期随线程而生,随线程而亡,他们的内存分配与回收具有确定性。因此,此处研究的是共享区域,主要是堆和方法区。
这一部分,单独写了一篇博客,有兴趣的小伙伴可以戳这个链接哦!
https://blog.csdn.net/qq_42142477/article/details/88773397
强引用、软引用、弱引用、虚引用,这四种引用的强度依次递减。
想知道finalize()是如何成为一块免死金牌的? 戳 https://blog.csdn.net/qq_42142477/article/details/88773409
方法区(永久代)的垃圾回收主要收集: 废弃常量和无用的类。
垃圾回收算法有:
标记-清除算法
复制算法(新生代)
标记-整理算法(老年代)
分代收集算法
具体怎么回收? 喏 -> https://blog.csdn.net/qq_42142477/article/details/88773419
垃圾收集器就是内存回收的具体实现
每种收集起的日志格式都可不同,但都维持一定的共性
eg:
大多数情况下,对象在新生代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();
}
}
大对象 -> 需要大量连续空间的对象
虚拟机提供了一个-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();
}
}
虚拟机给每个对象定义了一个对象年龄计数器,新生代对象每在Survivor中逃过一次Minor GC,年龄+1,当年龄增长到年龄阈值时,晋升为老年代,年龄阈值通过参数-XX:MaxTenuringThreshold设置
JVM定义了一种Java内存模型屏蔽掉各种硬件和操作系统的内存访问差异
Java内存模型主要定义了JVM中变量(实例字段、静态字段和构成数组对象的元素)如何存取。
一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存等,由以下8种操作完成。
操作 | 作用于 | 功能 |
---|---|---|
lock(锁定) | 主内存的变量 | 把一个变量标识为一条线程独占的状态,一旦锁定其他线程无法访问 |
unlock(解锁) | 主内存的变量 | 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 |
read(读取) | 主内存的变量 | 把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用 |
load(载入) | 工作内存的变量 | 把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
use(使用) | 工作内存的变量 | 把工作内存中一个变量的值传递给执行引擎 |
assign(赋值) | 工作内存的变量 | 把一个从执行引擎接收到的值赋给工作内存的变量 |
store(存储) | 工作内存的变量 | 把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用 |
write(写入) | 主内存的变量 | 把store操作从工作内存中得到的变量的值放入主内存的变量中 |
当一个变量定义为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