文章目录
版权声明
- 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明,所有版权属于黑马程序员或相关权利人所有。本博客的目的仅为个人学习和交流之用,并非商业用途。
- 我在整理学习笔记的过程中尽力确保准确性,但无法保证内容的完整性和时效性。本博客的内容可能会随着时间的推移而过时或需要更新。
- 若您是黑马程序员或相关权利人,如有任何侵犯版权的地方,请您及时联系我,我将立即予以删除或进行必要的修改。
- 对于其他读者,请在阅读本博客内容时保持遵守相关法律法规和道德准则,谨慎参考,并自行承担因此产生的风险和责任。
- 本博客中的部分观点和意见仅代表我个人,不代表黑马程序员的立场。
垃圾回收算法核心思想
- 垃圾回收要做的有两件事:第一,找到内存中存活的对象;第二,释放不再存活对象的内存,使得程序能再次利用该空间
垃圾回收算法的历史
- 1960年John McCarthy发布了第一个GC算法:标记-清除算法
- 1963年Marvin L. Minsky 发布了复制算法。
- 本质上后续所有的垃圾回收算法,都是在上述两种算法的基础上优化而来
垃圾回收算法的评价标准
- Java垃圾回收过程会通过单独的GC线程来完成。所有的GC算法,都会有部分阶段需要停止所有的用户线程,进行垃圾回收。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
垃圾回收算法的评价标准
- 吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即 吞吐量 = 执行用户代码时间 / (执行用户代码时间 + G C 时间) 吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间) 吞吐量=执行用户代码时间/(执行用户代码时间+GC时间)。
- 吞吐量数值越高,垃圾回收的效率就越高.比如:虚拟机总共运行了 100 分钟,其中GC花掉 1 分钟,那么吞吐量就是 99%
- 最大暂停时间:最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。最大暂停时间越短,用户使用系统时受到的影响就越短。
- 堆使用效率:不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算
法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法
- ==上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。==一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。
- 不同的垃圾回收算法,适用于不同的场景。没有最好的垃圾回收算法之说,只有不同场景下更合适的回收算法。
垃圾分类算法分类
标记清除算法
核心思想
- 标记清除算法的核心思想分为两个阶段:第一标记阶段和第二清除阶段。
- 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出有存活对象。
- 清除阶段,从内存中删除没有被标记也就是非存活对象
标记清除算法优缺点
- 优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可
- 缺点:碎片化问题;分配速度慢
- 碎片化问题:由于内存是连续的,在对象被删除后,内存中会出现很多细小的可用内存单元。对象需要一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
- 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,可能每次需要遍历到链表的最后才能获得合适的内存空间
- 碎片化问题:由于内存是连续的,在对象被删除后,内存中会出现很多细小的可用内存单元。对象需要一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
复制算法
核心思想
- 复制算法的核心思想是:准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。在垃圾回收GC阶段,将From中存活对象复制到To空间。将两块空间的From和To名字互换。
完整案例
完整的复制算法的例子:
- 将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象
- GC阶段开始,将GC Root搬运到To空间
- 将GC Root关联的对象,搬运到To空间
- 清理From空间,并把名称互换
复制算法的优缺点
- 优点:吞吐量高、不会发生碎片化
- 吞吐量高:复制算法只需遍历一次存活对象复制到To空间即可,比标记-整理算法 少一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动
- 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间
- 缺点:内存使用效率低
- 每次只能让一半的内存空间来为创建对象使用
标记整理算法
- 标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
核心思想
- 核心思想分为两个阶段:标记阶段和整理阶段
- 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象
- 整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。
标记整理算法优缺点
- 优点:内存使用效率高、不会发生碎片化
- 内存使用效率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存
- 不会发生碎片化:在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间
- 缺点:整理阶段效率不高
- 整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过TwoFinger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能
分代垃圾回收算法
- 现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。
- 分代垃圾回收将整个内存区域划分为年轻代和老年代
arthas查看分代内存情况
- 在JDK8中,添加-XX:+UseSerialGC参数使用分代回收的垃圾回收器,运行程序。
- 在arthas中使用memory命令查看内存,显示出三个区域的内存情况
核心思想
- 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。
- 注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
- 如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代
- 当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
虚拟机内存参数
- 根据以下虚拟机参数,调整堆的大小并观察结果。注意加上-XX:+UseSerialGC
参数名 | 参数含义 | 示例 |
---|---|---|
-Xms | 设置堆的最小和初始大小,必须是1024倍数且大于1MB | 比如初始大小6MB的写法: -Xms6291456、-Xms6144k、-Xms6m |
-Xmx | 设置最大堆的大小,必须是1024倍数且大于2MB | 比如最大堆80 MB的写法:-Xmx83886080、-Xmx81920k、-Xmx80m |
-Xmn | 新生代的大小 | 新生代256 MB的写法:-Xmn256m、-Xmn262144k、-Xmn268435456 |
-XX:SurvivorRatio | 伊甸园区和幸存区的比例,默认为8新生代1g内存,伊甸园区800MB,S0和S1各100MB | 比例调整为4的写法:-XX:SurvivorRatio=4 |
-XX:+PrintGCDetails/verbose:gc | 打印GC日志 | 无 |
StopWorldTest案例代码
java
import lombok.SneakyThrows;
import java.util.LinkedList;
import java.util.List;
/**
* STW测试
*/
public class StopWorldTest {
public static void main(String[] args) {
new PrintThread().start();
new ObjectThread().start();
}
}
class PrintThread extends Thread{
@SneakyThrows
@Override
public void run() {
//记录开始时间
long last = System.currentTimeMillis();
while(true){
long now = System.currentTimeMillis();
System.out.println(now - last);
last = now;
Thread.sleep(100);
}
}
}
class ObjectThread extends Thread{
@SneakyThrows
@Override
public void run() {
List<byte[]> bytes = new LinkedList<>();
while(true){
//最多存放8g,然后删除强引用,垃圾回收时释放8g
if(bytes.size() >= 80){
bytes.clear();
}
bytes.add(new byte[1024 * 1024 * 100]);
Thread.sleep(10);
}
}
}
GC案例1
java
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 垃圾回收器案例1
*/
//-XX:+UseSerialGC -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio=3 -XX:+PrintGCDetails
//-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:ConcGCThreads
//-XX:+UseParallelGC -XX:+UseParallelOldGC
public class GcDemo1 {
public static void main(String[] args) throws IOException {
List<Object> list = new ArrayList<>();
int count = 0;
while (true){
System.in.read();
System.out.println(++count);
//每次添加1m的数据
list.add(new byte[1024 * 1024 * 1]);
}
}
}
GC案例2
java
/**
* 垃圾回收器案例2
*/
//-XX:+UseSerialGC -Xmn10m -Xmx30m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc
public class GcDemo2 {
public static void main(String[] args) throws IOException {
List<Object> list = new ArrayList<>();
int count = 0;
while (true){
System.in.read();
System.out.println(++count);
//每次添加1m的数据
list.add(new byte[1024 * 1024 * 1]);
}
}
}