JVM内存结构
主流的三种JVM 虚拟机:
| jvm产品名称 | 所属公司 | 应用场景 | 备注 |
|---|---|---|---|
| Hotspot | sun公司 2009年,sun公司被Oracle甲骨文公司收购 | 用途最广,也是我们平时最常用的 服务器、桌面到移动端、嵌入式都有应用 | 有方法区,存在永久代 |
| JRockit | BEA公司 2008年,BEA被Oracle甲骨文公司收购 | 世界上运行最快的jvm 适合财务、军事指挥、电信网络 | 有方法区,不存在永久代 |
| J9 | IBM | 广泛用于IBM的各种Java产品 | 有方法区,不存在永久代 |
我们下面所讲述的都是基于Hotspot的jvm
Hotspot的JVM结构图
JDK7和JDK8内存模型对比:
绿色部分为线程隔离区域
橙色部分为线程共享区域


JDK7和JDK8最大的区别在于,jdk7中有方法区(永久代实现),使用的是jvm的内存,而jdk8的方法区,使用元空间实现,元空间使用的本地内存
下面先分析JDK7的内存模型:


1.类加载器
1.1 类加载器作用
类加载器负责加载Java类的字节代码到Java虚拟机中
1.2 类加载器分类

| 加载器 | 加载内容 | 实现语言 |
|---|---|---|
| 启动类(根类)加载器 Bootstrap ClassLoader | 加载 java_home\jre\lib 下面的 rt.jar,resources.jar、charsets.jar | C++ |
| 扩展类加载器 Extension ClassLoader | 加载 java_home\jre\lib\ext 下面的jar | Java |
| 应用程序加载器 Application ClassLoader | 加载用户类路径(ClassPath)上所有的类库,也就是我们平时开发所编写的类 | Java |
1.3 代码获取类加载器
package com.yuening;
/**
* @author yuening
* @version 1.0
* @description TODO
* @date 2022/6/12 0012 上午 8:35
*/
public class Student {
public static void main(String[] args) {
//创建对象
Student stu1 = new Student();
Student stu2 = new Student();
Student stu3 = new Student();
System.out.println(stu1);//com.yuening.Student@47089e5f
System.out.println(stu2);//com.yuening.Student@4141d797
System.out.println(stu3);//com.yuening.Student@68f7aae2
//获取stu的类模板,发现是一个类(PS:说明类是模板是抽象的,对象才是具体的)
System.out.println(stu1.getClass());//class com.yuening.Student
System.out.println(stu2.getClass());//class com.yuening.Student
System.out.println(stu3.getClass());//class com.yuening.Student
//获取加载Student的类加载器(PS:AppClassLoader应用程序加载器)
System.out.println(stu1.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(stu2.getClass().getClassLoader());
System.out.println(stu3.getClass().getClassLoader());
//获取父类加载器(扩展类加载器 ExtensionClassLoader)
System.out.println(stu3.getClass().getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@4f47d241
//获取父类的父类加载器(根类加载器BootstrapClassLoader)
//因为根加载器是由C++语言实现,这里java获取不到,并不是不存在
System.out.println(stu3.getClass().getClassLoader().getParent().getParent());//null
}
}

1.4 双亲委派机制
1. 类加载器在收到加载的请求时
2. 先判断该类是否已被加载,未被加载时,将加载的请求委托给自己的父类去加载,一直向上委托,直到启动类加载器(根类加载器BootStrapClassLoader)
3. 启动类加载器检查是否能够加载这个类,能加载就加载,不能加载通知子类进行加载
4. 重复步骤3,如果都找不到就抛出Class NotFound异常,程序结束
1.4.1 类加载流程 :

1.4.2 代码演示 :
package java.lang;
/**
* @author yuening
* @version 1.0
* @description 双亲委派机制
* @date 2022/6/12 0012 上午 8:56
*/
public class String {
/**
* 我们定义了一个和jre/lib/rt.jar中的相同的String类
* 并提供了toString方法,我们去测试并调用toString方法,
* 看会出现什么错误?
*
* 结果:
*
* 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
* public static void main(String[] args)
* 否则 JavaFX 应用程序类必须扩展javafx.application.Application
*
* PS:
* 说明根本没有加载到我们自己定义的String这个类,因为rt中提供的String类根本不存在main方法,
* 这是因为双亲委派机制
* 很显然BootStrapClassLoader在rt包内加载到了java.lang.String类,但这个String类不存在main方法,所以就出现上述错误
* @return
*/
public String toString() {
return "String{}";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.toString());
}
}

1.4.3 源码解析:
public abstract class ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查这个类是否已经被加载过
Class<?> c = findLoadedClass(name);
//为null说明未被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//存在父类加载器,递归交由父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//直到启动类(根类)加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private Class<?> findBootstrapClassOrNull(String name) {
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// return null if not found
// 此处发现是个native方法,是因为这里已经超出java作用范围,需要jvm调用本地方法接口
private native Class<?> findBootstrapClass(String name);
}
1.4.4 好处:
如果没有双亲委派,开发者可自定义一个Object同包同名类或者String类,并把它们放到classPath中,那么类之间的比较结果以及类的唯一性将无法保证。因此为了防止内存中出现多分同样的字节码,所以需要双亲委派机制。另外还有两点原因:
-
避免重复加载,性能考虑
-
安全性,防止核心类被修改
2. 本地方法栈
NativeMethodStack : 本地方法栈,主要是用来登记、管理NativeMethod(本地方法)的调用,在执行引擎(Execution Engine)执行的时候加载本地库(Native Method Libraies),NativeMethodStack只负责登记native方法,需要通过本地方法接口NativeInterface调用本地方法库的
注意Native方法库主要是用C/C++实现的
3.本地方法接口
NativeInterface:本地接口,作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,java在诞生的时候是C/++盛行的时候,想要立足,必须调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是在NativeMethodStack中登记native方法,在ExecutionEngine执行引擎执行的时候加载NativeLibraies。目前该方法使用的越来越少,除非是与硬件相关的应用。比如通过java程序驱动打印机或者java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通讯技术很发达,比如可以使用Socket通信,也可以使用WebService等等。
在java代码底层其实随处可见native方法,比如:
public static void main(String[] args){
new Thread(()->{
},"myThreadName").start();
}
//查看底层源码可知start方法中的start0()
public class Thread implements Runnable {
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
/**
* 此方法带有native关键字,说明java的作用范围已经达不到了,需要去调用底层的C语言库,
* 进入本地方法栈(NativeMethodStack),去通过本地方法接口(NativeMethodInterface)调用本地方法库(Native Method Libraies),
* 就是常说的JNI调用,JNI作用,其实就是为了扩展java的使用,融合不同的编程语言为java所用
*/
private native void start0();
}
4.程序计数器(PC寄存器)
程序计数器(Program Counter Register)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即是将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
5.方法区
方法区: Method Area,方法区是被所有线程所共享的,运行时常量池、Class类信息、所有字段以及一些特殊方法,如构造函数,接口代码在此定义,简单说,所有定义的方法的信息都保存在此区域,方法区属于共享区间,方法区最先有数据,因为类先被加载(类加载器将字节码文件加载到方法区)
JDK8之前的方法区和堆其实只是逻辑上的分区,而在物理方面它们又是连续的一块内存(JDK8方法区使用的是元空间实现,使用的本地内存)
JDK6: 方法区中存储字符串常量池、静态变量(static)、常量(final)、Class类信息(构造方法、接口)、运行时的常量池(每一个类和接口的常量池) 都存在方法区中
| JDK版本 | 方法区的落地实现 | 变化 |
|---|---|---|
| JDK6 | PermGen space(永久代) | 字符串常量池、运行时常量池、静态变量(static)都是在永久代中(方法区) |
| JDK7 | PermGen space(永久代) | 字符串常量池、静态变量被移动到了堆(Heap)中 运行时常量池还是在永久代中(方法区) |
| JDK8 | Metaspace(元空间) | 字符串常量池、静态变量仍然在堆(Heap)中; 运行时常量池被移动都了元空间中(方法区的永久代被元空间代替) |
JDK8 Metaspace(元空间)和 JDK8之前 PermGen(永久代)类似,都是对JVM规范中方法区的一种落地实现,最大的区别在于元空间并不在虚拟机中,而是使用本地内存,默认情况下,元空间的大小仅受 本地内存 限制,但可以通过jvm参数来指定元空间的大小.
为什么在jdk使用元空间代替永久代?
1. 类及方法的信息等比较难确定其大小,随着项目的发展类也越来越多,方法也越来越多,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
2. 永久代使用的jvm的内存,会为GC带来不必要的复杂度(GC需要不断扫描垃圾),并且永久代回收效率偏低,而元空间垃圾回收频率很低。
3. Oracle 为了将HotSpot 与 JRockit 合二为一。
字符串常量池为什么要在JDK7移动到堆空间中?
对于字符串常量这种创建完成用几次就不被使用的对象,是很容易被回收的。而要进行频繁GC垃圾回收的地方是堆空间, 这样在JDK7就把字符串常量池移动到堆空间中就是很明智和有必要的选择了。这样就避免了放到不频繁进行垃圾回收的元空间中应该被垃圾回收的对象而不能及时进行垃圾回收的浪费空间的现象出现。
静态变量为什么要在JDK7也一起挪到了堆空间中?
静态变量实际上是类静态成员变量,这个变量是和对象一起的,存在于对象中,其生命周期和对象一样,所以在堆空间中
6.栈
栈:是一种数据结构,类似于桶(顶部开口,底部封口),遵循先进后出,后进先出,先进入的被压到了底部,只有上面的函数执行完出栈后,底部的才出去
队列:是一种数据结构,队列(两端开口),严格遵循FIFO(First Input Frist Output)规则,先进先出,后进后出,比如:MQ消息队列
PS: 栈类似于喝多了吐,而队列类似于吃多了拉,哈哈!!!(出自狂神说博主)
栈作用:主管程序运行、生命周期、线程同步
栈内存:线程结束(main方法也是线程,主线程),栈内存就释放了,栈不存在垃圾回收
栈内存中存储 8大基本数据类型、局部变量、对象的引用、实例的方法(方法索引,方法的输入输出参数、父帧、子帧等)

问:栈的运行原理?
答:1.jvm对java栈的操作只有两个,一个是压栈和出栈,而所有的操作都是基于栈帧。
2.在一条活动线程中,一个时间点上,只有一个活动的栈帧。只有当前正在执行方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与此当前栈帧对应的方法是当前方法,定义这个方法的类就是当前类。
3.执行引擎运行的字节码只对当前栈帧进行操作。
4.如果该方法调用了其他的方法,对应新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧
PS: 1. 每个线程都有自己的栈,栈中存储的是栈帧,栈帧是线程私有的,其他线程不能引用另外一个线程的栈帧
2. 在这个线程上正在执行的每个方法都各自对应一个栈帧。方法与栈帧是一对一关系
3. 当前方法返回之际,当前栈帧会将当前方法执行的结果给前一个栈帧,然后虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
4. java函数返回方式有两种,return或者抛出异常,不管哪种方式,都会导致栈帧被弹出(出栈)
问:为什么main方法先执行,最后结束?
答:java中所有的方法都在栈中执行,程序一启动,首先执行的就是main方法,将main方法压入栈中,然后main中开始调用别的方法,将其他方法压入栈中,而只有当别的方法执行完毕,出栈后,main方法才出栈,程序才结束
问:为什么会出现【栈溢出】错误StackOverflowError
答:无限压栈,导致栈内存空间不足超过上限,比如下面的递归调用
package com.yuening;
/**
* @author yuening
* @version 1.0
* @description TODO
* @date 2022/6/12 0012 上午 2:35
*/
public class Student {
public static void main(String[] args) {
Student stu = new Student();
stu.a();
}
public void a(){
b();
}
public void b(){
a();
}
}
Exception in thread "main" java.lang.StackOverflowError
at com.yuening.Student.b(Student.java:21)
at com.yuening.Student.a(Student.java:17)
at com.yuening.Student.b(Student.java:21)
at com.yuening.Student.a(Student.java:17)
at com.yuening.Student.b(Student.java:21)
at com.yuening.Student.a(Student.java:17)
7.堆
一个JVM只有一个堆内存(Heap),堆内存的大小是可以通过jvm参数调节的。
堆中主要保存的是 类的实例(对象)、成员变量、类的属性、字符串常量池(JDK7挪过来)、静态变量(JDK7挪过来)
堆内存又分为:
| JDK版本 | 堆内存 | 差异 | 备注 |
|---|---|---|---|
| JDK7 | 1. 新生代(伊甸区+幸存区From+幸存区to) 2. 老年代(老年区Old) 3. 永久代(永久区PermGen) | 存在永久代 | 伊甸区(Eden) 幸存区0(ServivorFrom) 幸存区1(ServivorTo) |
| JDK8 | 1. 新生代 2. 老年代 | 不存在永久代,使用元空间代替(元空间不在堆中,在本地内存中) | 元空间(Metaspace) |
GC回收主要存在于伊甸区和老年区,幸存区不需要回收,被用来作为Eden及老年代的中间交换区域,永久区/元空间是常驻内存,不存在垃圾回收,关闭JVM虚拟机就会释放这部分内存
永久区/元空间什么时候会OOM?
一个启动类加载了大量的第三方jar包.tomcat下部署了大量的应用,大量动态生成反射类,不断的被加载直到内存满就会出现oom
jvm的堆内存溢出:java.lang.OutOfMemoryError.java.heap.space
| oom区域 | 错误 | |
|---|---|---|
| 堆 | java.lang.OutOfMemoryError: Java heap space | |
| 栈 | java.lang.StackOverflowError | |
| JDK7 永久代(方法区) | java.lang.OutOfMemoryError: PermGen space | |
| JDK8 元空间 | java.lang.OutOfMemoryError: Metaspace |
| GC类型 | 回收区域 | 触发机制 |
|---|---|---|
| Minor GC | 新生代(伊甸区+幸存区) | 当新生代满的时候触发,这里的新生区指的是伊甸区,而幸存区满了,是不会触发GC的 清理年轻代空间(包括 Eden 和 Survivor 区域),释放在Eden中所有不活跃的对象,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。Survivor区被用来作为Eden及老年代的中间交换区域,当老年代空间足够时,Survivor区的对象会被移到老年代,否则会被保留在Survivor区。 |
| Major GC | 老年代 | 清理老年代空间,当老年代空间不够时,JVM会在老年代进行major gc 出现了 Major GC,通常会伴随至少一次的 Minor GC ,MajorGC 的速度一般会比 Minor GC 慢 10倍以上 |
| Full GC | 新生代+老年代 | 清理整个堆空间,包括年轻代和老年代空间 |
-Xms1024M -Xmx1024M -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=2 -XX:G1HeapRegionSize=4M -XX:ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError
| JVM调优参数 | 备注 | JDK版本 |
|---|---|---|
| -Xms | 设置堆的最小空间大小;通常为操作系统可用内存的1/64大小即可 生产环境一般直接与-Xmx保持一致,可用内存的1/4,可以避免JVM内存自动扩展,一步初始化到位 | |
| -Xmx | 设置堆的最大空间大小;通常为操作系统可用内存的1/4大小 | |
| -Xmn | 设置新生代大小,是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置; 通常为Xmx的1/3或1/4 | JDK1.4后 |
| -XX:NewSize | 设置新生代最小空间大小 | |
| -XX:MaxNewSize | 设置新生代最大空间大小 | |
| -XX:NewRatio | 老年代与新生代的比例,默认为2,则老年代占整个堆空间的2/3,新生代占1/3, | |
| -XX:SurvivorRatio | 新生代中 Survivor与Eden的比值。默认值为 8 。注意Survivor区有两个,即Eden占新生代空间的8/10,另外两个Survivor各占1/10 | |
| -XX:PermSize | 设置永久代(方法区)最小空间大小 | JDK1.7 |
| -XX:MaxPermSize | 设置永久代(方法区)最大空间大小 | JDK1.7 |
| -XX:MetaspaceSize | 设置元空间的初始空间大小 | JDK1.8 |
| -XX:MaxMetaspaceSize | 设置元空间的最大空间大小,,默认是不限制 | JDK1.8 |
| -Xss | 设置每个线程的栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。此值设置越小,相同物理内存下就可以创建更多线程 | |
| -XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照 | |
| -XX:HeapDumpPath=dump存放路径 | 生成堆的dump文件的路径(需要保证目录是存在的,不会自动创建不存在的目录) | |
| -XX:+PrintGCDetails | 在控制台上打印出GC具体内存回收细节 |
| JVM垃圾回收器设置 | 备注 |
|---|---|
| -XX:+UseSerialGC | 设置串行收集器 |
| -XX:+UseParallelGC | 设置并行收集器 |
| -XX:+UseParalledlOldGC | 设置并行老年代收集器 |
| -XX:+UseConcMarkSweepGC | 设置并发收集器 |

8.内存分析工具
Dump 文件是 Java 进程的内存镜像,其中主要包括 系统信息、虚拟机属性、完整的线程 Dump、所有类和对象的状态 等信息。
一般线上都是分析dump文件,当程序发生内存溢出或 GC 异常情况时,怀疑 JVM 发生了 内存泄漏,这时我们就可以导出 Dump 文件分析,文件名称默认格式:java_pid{pid}.hprof。
常用的Dump分析工具?
Dump 分析工具有很多,相对而言 ,以下三种使用的更多
1. JvisualVM:JDK自带的Java性能分析工具,在JDK的bin目录下,文件名就叫jvisualvm.exe。
2. JProfiler:JProfiler是一个商业授权的Java剖析工具,由EJ技术有限公司,针对的Java EE和Java SE应用程序开发的。
3. Eclipse Mat(eclipse插件):Eclipse Memory Analyzer属于eclipse的插件
我们这里使用JProfiler工具,,我这里下载的是windows64位的12版本(这个是个客户端,可直接打开dump文件),不用下最新的,因为需要秘钥,你懂得,最新的找不到秘钥!!!
直接安装就行了,安装目录最好是全英文并且重点不存在空格、
编写测试程序:
jvm参数设置: -Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError
package com.yuening;
import java.util.ArrayList;
import java.util.List;
// -Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError
public class Test {
ArrayList[] byteList = new ArrayList[1*1024*1024];
public static void main(String[] args) {
List<Test> testList = new ArrayList<>();
int count = 0;
try{
while(true){
testList.add(new Test());
count+=1;
}
}catch (OutOfMemoryError o){
System.out.println("OutOfMemoryError count:"+count);
}
}
}
******************************************************************************************
java.lang.OutOfMemoryError: Java heap space(堆内存溢出)
Dumping heap to java_pid15808.hprof ...
Heap dump file created [186151935 bytes in 0.340 secs]
OutOfMemoryError count:22
找到dump文件(默认就在当前项目根目录下),直接打开:



9.GC
Jvm在GC时,并不是对三个区域(新生代、幸存区、老年代)统一进行GC,大部分时候其实是在回收新生代
GC分为轻GC和重GC(Full GC也叫全局GC),java中不能主动垃圾回收,只能是提醒,所有的垃圾回收操作都是jvm控制和执行
常见的GC算法:
| GC回收算法 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| 引用计数法(已经不常用了) | 对象创建时给每个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。当没有引用指向该对象时,该对象死亡,计数器为0,GC这个对象进行垃圾回收操作 | 1. 简单 2.计算代价分散 3.“幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态) | 1.不全面(容易漏掉循环引用的对象) 2.并发支持较弱 3.计数器需要占用额外内存空间 |
| 复制算法 | 该算法将内存平均分成两部分,每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。适用于对象存活度较低的场景,而JVM的 新生代使用的就是复制算法 | 1. 实现简单 2. 不产生内存碎片 | 内存使用率低,每次运行,总有一半内存是空的,导致可使用的内存空间只有一半。 |
| 标记–清除算法 | 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。使用于内存碎片不是很多的时候 | 相比于引用计数法,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。这个算法并不移动对象的位置。 | 1. "幽灵时间长",需要遍历对象进行判断对象是否死亡,耗时,从死亡再到被回收也消耗时间。 2.每个活着的对象都要在标记阶段扫描一次;所有对象都要在清除阶段再扫描一遍,因此算法复杂度较高。3.没有移动对象,会出现内存碎片空间无法利用 |
| 标记–整理算法 | 此算法结合了“标记,清除”和“复制”两个算法 的优点。也是分两阶段,第一阶段标记所有被引用的对象,第二阶段遍历所有对象,清除未标记对象并且把存活对象“压缩”到堆的其中一块连续的区域,按顺序排放。此算法 避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。标记-整理 算法。 | 不会产生大量的内存碎片 | 存活的对象过多,整理阶段将会执行较慢,导致算法效率降低 |
| 分代收集算法 | ①新生代-->复制算法 ②老年代--->标记-清除+标记-整理混合使用 |
9.1 引用计数法(了解)

9.2复制算法

9.3 标记-清除算法

9.4 标记-整理算法

9.5总结
内存效率(时间复杂度):复制算法 > 标记清除 > 标记整理
内存整齐度:复制算法 = 标记整理 > 标记清除
内存利用率:标记整理 = 标记清除 > 复制算法
10.JMM
-
什么是JMM?
JMM:Java Memory Model缩写,java内存模型(注意不是jvm内存模型),围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型
-
JMM用来做什么?
缓存一致性协议,用于定义数据读写的规范。
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
-
JMM的三个特征?
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。
可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。
有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现
-

JVM内存结构详解
浙公网安备 33010602011771号