1. 运行时数据区
程序计数器
用于记录正在执行的虚拟机字节码指令的地址,即每个线程都会有自己的一个程序计数器。换句话讲,它就是用来指示当前线程所执行的字节码执行到了第几行。程序计数器是线程隔离的。
Java虚拟机栈
即VM Stack,每个Java方法执行的同时会创建一个栈帧,虚拟机栈描述的是Java方法执行的内存模型,用于存储局部变量,操作数栈,动态链接(???),常量池引用,方法出口等信息。虚拟机栈是线程隔离的。
本地方法栈
本地方法栈与Java虚拟机栈类似,唯一区别在于本地方法栈是为Native本地方法所服务。本地方法栈是线程隔离的。
注: 本地方法,一般是是由其他语言编写,并被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理!
堆
所有new的对象都是在这里分配内存,堆也是垃圾收集的主要区域(“GC堆”)。堆是线程共享的。
方法区
用于存放已被加载的类信息(包括类名称,方法信息,字段信息)、静态变量、常量以及编译器编译后的代码等数据。
注: JDK1.8开始,HotSpot VM取消了永久代,把方法区移至元空间,元空间位于本地内存中,而不是虚拟机内存中。方法区是一个JVM规范,永久代和元空间都是其一种实现方式。在JDK1.8之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,堆中存放静态变量和常量等。
运行时常量池
运行时常量池属于方法区的一部分。
Class文件中的常量池(编译器生成的字面量和符号引用)会在类加载后放入这个区域。
除了编译器生成的常量,也允许动态生成,如String#intern()。
直接内存
//TODO…
Java内存模型(JMM)
主内存和工作内存:
- 处理器上的寄存器的读写的速度比内存(主存)快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
- 加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
- 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量(实体)存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
(图片来源:https://www.cnblogs.com/haoworld/p/java-bing-fa-xian-cheng-an-quan-he-nei-cun-mo-xing.html#toc_19)
线程A与线程B之间如要“成功”通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取由线程A之前更新过的共享变量。
问题:
共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能某线程本地内存中的值没有及时刷新到主内存,导致其他线程读取的值不是最新的,从而发生数据不一致的问题。
解决方案:
- volatile关键字:可以保证共享变量的可见性、有序性,无法保证原子性;
- synchronized同步锁:能够保证可见性、原子性,由于同一时间只有一个线程获得锁,自然就保证了有序性。
2. 垃圾收集
注: 垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域是属于线程私有的,只存在于线程的生命周期内,线程结束后会小时,因此不需要对这三个区域进行垃圾回收。
如何判断一个对象是否可被回收?
1. 引用计数算法
为每一个对象维护一个引用计数器,每当对象增加一个新的引用时计数器加1,反之,引用失效时计数器则减1。于是当计数为0的时候对象即可被回收。
注: 当两个对象出现循环引用时,引用计数器不可能减为0,最终导致对象无法进行回收。正是如此,Java虚拟机不使用引用计数算法。示例代码如下:
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
2. 可达性分析算法
以GC Roots为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java虚拟机使用该算法来判断对象是否可以被回收,GC Roots一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中JNI中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
3. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收的性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件有很多,需要满足以下三个条件,并且满足条件也u不一定会被卸载:
- 该类的所有实例都已经被回收,此时堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法
4. finalize()
类似于C++的析构函数,用于关闭外部资源。但是try…finally…等方式可以做得更好,并且finalize()方法运行代价很高,不确定性打,无法保证各对象的调用顺序,因此最好不要使用!
当一个对象可以被回收时,如果需要执行该对象的finalize()方法,就有可能在该方法中让对象重新被引用,从而实现“自救”。不过“自救”只能进行一次,即如果回收对象之前调用了finalize()方法自救,后面再次回收时不会在调用该方法!
引用类型
//TODO
参考:
[1] Cyc2018
[2] https://www.cnblogs.com/haoworld/p/java-bing-fa-xian-cheng-an-quan-he-nei-cun-mo-xing.html#toc_19