1. 引用计数算法
-
在JVM中几乎不用,每个对象在创建的时候,就给这个对象绑定一个计数器(有消耗)。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。
-
优点:
- 简单
- 计算代价分散
- “幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态)
-
缺点
- 不全面(容易漏掉循环引用的对象)
- 并发支持较弱
- 占用额外内存空间(计数器消耗)
2. 复制算法
- 年轻代中使用的Minor GC,采用的就是复制算法(Copying),每次轻 GC 之后,Edan区是空, To 区是空。
-
将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
-
优点:空间连续,没有内存碎片,运行效率高。
-
缺点: 每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。复制收集算法在对象存活率高的时候,效率有所下降, 所以复制算法主要用在新生代幸存者区中的from区和to区,因为新生代对象存活率低。
3. 标记-清除算法
- 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
-
优点:
- 实现简单,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。
- 此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置。
-
缺点:
- 需要进行两次动作,标记获得的对象和清除死亡的对象,所以效率低。
- 死亡的对象被GC后,内存不连续,会有内存碎片,GC的次数越多碎片越严重。
4. 标记-压缩/整理算法
- 在整理压缩阶段,不再对标记的对象作回收,而是通过所有存活对象都向一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
5. 标记清除压缩
-
标记整理说明:老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
-
标记清除几次,再压缩
6. 总结
-
内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记清除压缩算法
-
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
-
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
1. 什么是JMM?
- Java内存模型(Java Memory Model)
2. 它干嘛用的?
-
JMM是用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。(找到这个规则! )
-
参考博客:https://www.jianshu.com/p/8a58d8335270
3. JMM的指令规则
4. 内存可见性
- 在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存的变量副本中。那怎样保持这变量之间的一致性呢?
- 当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。
- 白话:当我在修改 A 时,其他的线程都读不了 A,得等我改好并重新放回去。
解决共享对象可见性这个问题:volatile关键字
上图右侧每个线程都有自己的工作区域,可以改变变量的值,所以就存在共享对象可见性不一致的问题,这时就可以使用关键字volatile,保证共享对象可见性的问题,只要右边的线程变量的值改变就会立即被刷新到主内存中。
那 volatile 是怎么解决的呢?得先知道:指令重排序
5. 指令重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,指令重排序使得代码在多线程执行时会出现一些问题。
其中最著名的案例便是在 初始化单例时 由于 可见性 和 重排序 导致的错误。
单例模式案例一
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
//有可能多个线程同时进入该判断
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
-
以上代码是经典的 懒汉式 单例实现,
-
但在多线程的情况下,多个线程有可能会同时进入if (singleton == null) ,
-
从而执行了多次singleton = new Singleton(),单例被破坏。
单例模式案例二
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上代码在检测到singleton为 null 后,【在同步块中再次判断】,
可以保证同一时间只有一个线程可以初始化单例。
但仍然存在问题,原因就是Java中singleton = new Singleton()语句并不是一个原子指令,而是由三步组成:
1.为对象分配内存
2.初始化对象
3.将对象的内存地址赋给引用
但是当经过 指令重排序 后,会变成:
1.为对象分配内存
2.将对象的内存地址赋给引用(会使得singleton != null)
3.初始化对象
所以就存在一种情况,当线程A已经将内存地址赋给引用时,但 实例对象并没有完全初始化,
此时线程B判断singleton已经不为null,就会导致B线程 访问到未初始化的变量 从而产生错误。
单例模式案例三:使用 volatile
public class Singleton {
//此处添加了 volatile 关键字 ↓
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上代码对singleton
变量添加了volatile
修饰,可以【阻止局部指令重排序】。
那么为什么 volatile 可以保证变量的可见性和阻止指令重排序?
6. volatile
-
原理
-
规定线程每次修改变量副本后 立刻同步到主内存 中,用于保证其它线程可以看到自己对变量的修改
-
规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
-
为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入 内存屏障 来 防止指令重排序 。
- 内存屏障:https://www.jianshu.com/p/2ab5e3d7e510
-
-
注意
-
volatile只能保证基本类型变量的内存可见性
-
对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。
-
关于引用变量类型:Java的数据类型。
-
-
volilate只能保证共享对象的可见性,不能保证原子性:
-
假设两个线程同时在做 i++(该操作不具有原子性),在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量
-
所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2
-
更多:https://blog.csdn.net/qq_32534441/article/details/86416015
-
-