jvm结构和垃圾回收机制
Java内存管理机制

运行时区域
Java虚拟机在执行Java程序代码过程中会把它所管理的内存划分为若干个不同的数据区域。Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
方法区,虚拟机栈(线程),本地方法栈,堆,程序计数器
程序计数器
- 属于线程私有内存
- 程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时候就是通过改变这个计数器的值来选取下一条指令需要执行的字节码指令,跳转,循环,异常处理等都需要这个计数器来处理。
- 在任何一个确定的时刻,一个处理器只会执行一条线程中的命令。因此对于多线程而言,每条线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
- 如果线程正在执行Java方法,这个计数器记录的是正在执行的虚拟机节码指令的地址;如果正在执行的是native方法,这个计数器值内空,指向的是唯OutofMemoryError区域
Java虚拟机栈
- 属于线程私有内存,生命周期与线程相同
- 每个方法被执行的时候都会创建一个栈帧,每个方法被调用到执行完成的过程,对应一个栈帧在虚拟机中入到出栈的过程
- 栈,对应的是虚拟机栈,或者说虚拟机栈中的局部变量表
- 局部变量表,所需的内存空间在编译期间完成分配。它存放了编译期间可知的基本数据类型,对象引用类型和returnAddress类型(指向一个字节码指令的地址)
-
栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址
栈帧.png
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.compute();
}
int compute() {
int a = 3;
int b = 6;
int c = (a + b)*2;
return c;
}
}
分析:首先我们会new Test()对象,放到堆内存中,随后会走Test 类中(也就是方法区中的类元信息Test.class)的compute方法,此方法会有个程序计数器,进入之后,首先将3加入到操作数栈中,下一步会将a加入到局部变量表中,并将3赋值给a,以此轮推,最后会将compute这个栈中的变量返回地址给main栈中,而compute方法这个寻找的过程也称为动态链接
本地方法栈
- 内存线程私有
- 虚拟机栈执行的是java方法
- 本地方法栈执行的是native方法
堆
- 所有线程共享的一块内存区域,在虚拟机启动时创建
- 主要存放对象实例,垃圾收集器管理的主要区域
- Java堆可以是一块不连续内存空间
-
堆分代
image.png
所有新生成的对象首先都是放在年轻代。首先它会进入到Eden区,当Eden区满时,还存活的对象将被复制到Survivor区(两个中其中一个,必须有一个是空闲的),只要移动到To区,里面有个值叫做分代年龄,这个值就会+1,当To满了,GC后还活着的会移动到From区,依次循环,当进行分代年龄到15以后,就会加入到老年代。永久代放的是静态变量和静态常量,类信息等
方法区
- 所有线程共享的一块内存区域。
- 用于存储已被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据。
- 回收目标主要针对的是常量池的回收和对类型的卸载
- 运行时常量池,属于方法区的一部分,存放各种字面量和符号引用。
JMM内存模型
概述
- java内存模型简称JMM
-
JMM决定一个线程对共享变量的写入何时对另一个线程可
JMM内存模型.png
AB通信过程:
1.线程A把本地内存A中更新过的共享变量刷新到主内存中去。2. 线程B到主内存中去读取线程A之前已更新过的共享变量。
一些方法
- read (读取) :从主内存读取数据
- load (载入) :将主内存读取到的数据写入工作内存
- use(使用) :从工作内存读取数据来计算
- assign (赋值) :将计算好的值重新赋值到工作内存中
- store (存储) :将工作内存数据写入主内存
- write (写入) :将store过去的变量值赋值给主内存中的变量
- lock (锁定) :将主内存变量加锁,标识为线程独占状态
- unlock (解锁) :将主内存变量解锁,解锁后其他线程可以锁定该变量
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("等待数据...");
while (!initFlag) {
}
System.out.println("======终于成功了");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("准备数据...");
initFlag = true;
System.out.println("准备结束");
}
}).start();
}
这里我加了一个volatile,我们来分析下没有加volatile的过程

首先主内存现在有个变量initFlag值为false,线程会从主内存读到(read)变量initFlag,随后将变量加载到写入线程的工作内存中,再将工作内存变量use进行内存变量的计算,再经过assign将变量赋值到工作内存中,store将工作内存数据写入主内存,write 将store过去的变量值赋值给主内存中的变量,这样主内存的工作变量就发生了变化。
JMM内存缓存不一致
- 总线加锁(性能太低)
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其它cpu才能读取该数据 - MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
volatile实现原理
-
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存并回写到主内存,此操作被称为"缓存锁定”, MESI缓存一致性协议机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存值通过总线回写到内存会导致其他处理器相应的缓存失效。
-
并发编程;可见性,原子性,有序性
-
volatile保证可见性和有序性,却不保证原子性,保证原子性需要借助synchronized
-
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败
public static volatile int num=0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads=new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
add();
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
private static synchronized void add() {
num++;
}
如果不加synchronized,其打印的值会小于10000,原因是volatile不是立即保证可见性,它有个写入主内存的过程,其他线程(B)等不及的会自己去拿原本的数据进行++运算,当这个线程(B)之前的线程(A)修改完主内存的值后,B已经运算完,但是此时失效了,就会导致原本++运算无效
垃圾收集器和内存分配策略
概述
- 程序计数器,虚拟机栈,本地方法区这三个区域都随线程而生,随线程而灭,因此不需要考虑
- Java堆和方法区则不一样,我们只有在运行期间才知道会创建哪些对象和其大小,所以内存分配指的就是这一块
算法
-
目的:判断对象是死是活
-
引用计数器算法
给对象添加一个引用计数器,每当一个地方引用它时候,计数器值+1,引用失效的时候-1,任何时候为0的对象就不可能会再次被使用 -
根搜索算法
GC Roots为起点,从该节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,表示这个对象不可用。
可作为GC Roots对象的有:
虚拟机栈(栈帧的本地变量表)中的引用对象
方法区中的类静态属性引用的对象
方法区中的常量引用对象
本地方法中JNI中的引用
四种引用
-
强引用
只要强引用存在,垃圾回收器永远不会回收掉被引用的对象 -
软引用
当系统将要发生内存溢出异常之前,将会把这些对象列进回收范围并进行第二次回收,Java提供提供SoftReference类来实现软引用 -
弱引用
被弱引用关联的对象只能生存到下一次垃圾收集发生之前,Java提供weakReference来实现弱引用 -
虚引用
最弱的一种引用。无法通过虚引用来取得一个对象实例。其作用就是这个对象被收集器回收的时候收到一个通知
finalize()方法
-
任何一个对象的finalize方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize方法不会再被执行
-
运行代价高昂,不确定性大,无法保证各个对象调用的顺序
-
finalize能做的所有工作,使用try-finally都可以做,而且会更好
回收方法区
- 回收主要对象:废弃常量和无用的类
- 废弃常量:定义的字符串或者常量没有被使用和引用
- 无用的类:
1.该类已经被回收了,Java堆中不存在该类的任何引用
2.加载该类的classloader已经回收
3.该类没有被反射使用
垃圾收集算法
-
标记清除法
1、首先标记所有要回收的对象,在标记完成后统一回收掉所有被标记的对象
2、缺点:标记和清除效率都不高;空间浪费,会产生大量不连续空间 -
复制算法
1、内存按容量划分大小相等两块,每次只使用其中一块。当一块内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
2、主要针对新生代。 -
标记-整理法
1、标记过程和标记清除法一样,但是后续步骤不是对可回收对象直接清除,而是让所有存活的对象都向着一端移动,然后直接清理掉边界以外的内存。
2、主要针对老年代 -
分代收集算法
1、根据对象的存活周期的不同将内存划分为几块。一般是将Java堆分为新生代和老年代
2、新生代使用复制算法
3、老年代使用标记-清除或者标记-整理法