15.GC :垃圾回收机制
垃圾回收的区域只有在堆里面(方法区在堆里面)
15.1 垃圾回收 = GC
JVM 在进行垃圾回收(GC)时,并不是堆这三个区域统一回收。大部分时候,回收都是新生代~
1.新生代
2.幸存区(form,to)
3.老年区
GC 两种类:轻GC(普通GC),重GC (全局GC)
轻GC 指针对 新生代 和 偶尔走一下 幸存区。
重GC 全部清完。
题目(重点):
1.JVM的内存模型和分区~详细到每一个区放什么?
2.堆里面的分区有哪些?
3.GC的算法有哪些?
标记清除法
标记整理
复制算法
引用计数法
怎么用的?
4.轻GC 和 重GC 分别再什么时候发生?
15.2 引用计数法
假设我对象A 用了 一次就给它加上1
假设我对象B 用了 两次就给它加上2
假设我对象C 没有使用 就是 0
引用计数法就是给每个对象分配一个计数器
假设C 对象为 0,它就要被清除出去了
JVM 现在一般不采用这种方式,不高效。
15.3 复制算法
- -XX:MaxTenuringThreshold=15 设置进入老年代的存活次数条件(动态调整)
一个对象怎么从新生代变成老年代(重点)
JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间,由于JVM频繁的创建对象,所以新生代会频繁触发Minor GC进行垃圾回收。新生代又分为Eden区,survivorForm区和survivorTo区。
- 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区
- 然后将伊甸园区中上一次垃圾回收的幸存者移动到幸存者 0 区(From)。
- 如果再次触发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区(To)。
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区,对象在幸存者区中每熬过一次MinorGC,年龄就加一。
- 什么时候去养老区呢?对象的年龄达到老年代标准,默认是 15 次,也可以设置参数(XX:MaxTenuringThreshold),最大值为 15
- 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理.
- 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常:Java.lang.OutOfMemoryError:Java heap space
复制算法的优缺点:
- 好处:没有内存的碎片,内存效率高
- 坏处:浪费了内存空间(一个幸存区永远是空的);假设对象100%存活,复制成本很高。
复制算法最佳使用场景:对象存活度较低 的时候,新生区~。
15.4 标记清除算法
扫描这些对象,对活着的对象进行标记
清除:对没有标记的对象,进行清除
优点:不需要额外的空间
缺点:两次扫描严重浪费时间,会产生内存碎片。
15.5 标记压缩
本质是再次扫描,将标记了的内存碎片排序到一起
15.6 总结:
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率: 标记压缩算法 = 标记清除算法 > 复制算法
没有最优算法吗?
没有;
没有最好的算法,只有最合适的算法~ ---->GC: 分代收集算法
每一代用合适的算法就好了。
年轻代: (大部分的对象都在这里都死了)
存活率低
复制算法!
老年代:
存活率高,区域大
标记清除 + 标记压缩 混合实现
16. Java垃圾回收的原理与触发时机
16.1 Young GC(Minor GC)
什么时候会触发Minor GC?
- 大多数情况下,对象在年轻代中的Eden区进行分配,若Eden区没有足够空间,就会触发YGC(Minor GC)。
16.2 Full GC(Major GC)
FGC处理的区域包括新生代和老年代。
什么时候会触发Full GC?
- 老年代的内存使用率达到了一定阈值(默认是92% 通过-XX:MaxTenuringThreshoId设置)
- 情景1:Survivor区进行了15次清理后还没清理的对象,则放到老年代。
- 次数这个阈值是可以设置的:-XX:MaxTenuringThreshold(默认是15)
- 情景2:Minor GC时的检查
- 执行Minor GC时,JVM会先检查Survivor空间是否够用
- 如果够用则直接进行Minor GC
- 如果不够用,则检查老年代最大连续可用空间是否大于新生代的总和或者历次晋升到老年代的对象的平均大小
- 如果大于,则直接执行Minor GC(这个时候执行是没有风险的)。
- 如果小于,则直接执行Full GC
- JDK7及之后,空间分配担保参数:-XX:HandlePromotionFailure就失效了,无需考虑这个。
- 执行Minor GC时,JVM会先检查Survivor空间是否够用
- 情景3:使用了大对象
- 大对象会直接进入老年代。比如:一次加载过多数据到内存(比如SQL查询未分页),导致大对象进入老年代。
- 相关参数:-XX:PretenureSizeThreshold。若对象大小大于此值,就会绕过新生代, 直接在老年代中分配
- 情景1:Survivor区进行了15次清理后还没清理的对象,则放到老年代。
- Metaspace(元空间)扩容到了-XX:MetaspaceSize 参数的指定值。(元空间在空间不足时会进行扩容)
- 程序执行了System.gc()
- 只是建议JVM执行FGC,并不一定会执行
- jmap 加了:live参数
- 例如:
jmap -histo:live <pid>
;jmap -dump:live,format=b,file=heap.bin <pid>
- 其他
- 上一次GC之后Heap的各域分配策略动态变化
- RMI等的定时触发
- YGC时的悲观策略
16.3 System.gc()
不要频繁使用gc函数。建议是:保持代码健壮(记得将不用的变量置为null),让虚拟机去管理内存。
System.gc():提醒虚拟机进行垃圾回收,回不回收由虚拟机决定。若虚拟机决定回收,也不是立刻进行回收,它是异步的。当然,System.gc()也不是一点用也没有,当执行System.gc()之后,还是能一定程度影响垃圾回收的。如下所示:
17. JMM
-
什么是JMM? :百度
java内存模型 Java Memory Model,它干嘛的? 学习途径:官方,其他人的博客,教学视频
作用:缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存中, 每个线程都有一个**私有的本地变量内存。
解决共享对象可见性这个问题:volilate
17.1 volilate
Volatile关键字的作用主要有如下两个:
- 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
- 顺序一致性:禁止指令重排序。
- 保证简单操作的原子性,如赋值操作等,像i++这种复杂的复合操作无法保证原子性。
底层原理:
被volatile修饰的变量被修改时,会将修改后的变量直接写入主存中,并且将其他线程中该变量的缓存置为无效,从而让其它线程对该变量的引用直接从主存中获取数据,这样就保证了变量的可见性。
volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
- 写volatile时处理器会将缓存写回到主内存。
- 一个处理器的缓存写回到内存,会导致其他处理器的缓存失效。
17.2 JMM个人理解
线程之间的共享变量存储在主内存中(Main Memory)中,每个线程都一个都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
17.3 Volatile与Synchronized比较
- Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
- Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
- Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
- volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。