ZGC关键技术分析

news2024/12/27 15:10:09

一、引言

垃圾回收对于Javaer来说是一个绕不开的话题,工作中涉及到的调优工作也经常围绕垃圾回收器展开。面对不同的业务场景没有一个统一的垃圾回收器能保证可GC性能。因此对程序员来说不仅要会编写业务代码,同时也要卷一下JVM底层原理和调优知识。这种局面可能因为ZGC的出现而发生改变,新一代回收器ZGC几乎不需要调优的情况下GC停顿时间可以降低到亚秒级。

Oracle从JDK11开始正式引入ZGC,ZGC设计三大目标:

  • 支持TB级内存 (8M~4TB) 。
  • 停顿时间控制在10ms之内 (生产环境实际观测在微秒级) ,停顿不会随着堆的大小,或者活跃对象的大小而增加。
  • 对程序吞吐量影响小于15%。

ZGC是如何设计怎么达到这个目标的呢?本文将从ZGC算法的关键特性入手,通过分析ZGC周期处理过程来理解这些特性,探索ZGC设计思想。

二、ZGC术语

非分代:将对内存划分为新生代和老年代 (G1已经逻辑分代) ,ZGC取消分代设计,每个GC周期都将标记整个堆中的所有活动对象。

页面:ZGC将堆空间分解成一块块区域,这些区域叫做页面,ZGC通过页面来回收内存。

并发性:GC和线程和业务线程同时运行ZGC的高度并发设计,几乎所有GC工作、标记和堆碎片整理都是和业务线程 (mutators) 同时运行的,只包含了短暂的STW同步暂停。

并行:多个线程进行GC线程同时工作,加快回收速度。

标记-复制算法:标记-复制算法主要包括以下3个过程。

  • 标记阶段,即从GC Roots集合开始,分析对象可达性,标记出活跃对象。

图1:可达性分析后对象的引用状态

  • 对象转移阶段,即把活跃对象复制到新的内存地址上。
  • 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。

标记-复制算法的最大优势就是防止堆内存碎片化的出现,复制的过程就可以对堆内存进行整理。ZGC、CMS和G1都是采用了标记-复制算法,但是不同的实现导致了很大的性能差异。

三、ZGC性能数据

ZGC设计致力于提供几毫秒的最大暂停时间,同时保证吞吐量不受影响。下面是SPECjbb2015针对OpenJDK中的不同收集器运行的性能测试数据。在128G堆内存下,无论是延迟还是吞吐量上面ZGC的性能表现都高于其他收集器。

图2:SPECjbb2015GC性能评分

图3: SPECjbb2015GC延迟比较

四、ZGC关键特性

ZGC的周期是高度并发的,并发性越高意味着GC工作时对业务线程的影响越小,SPECjbb2015的性能报告可以看出ZGC在延迟上比G1低10倍以上,ZGC的工作周期只有三个阶段是STW的,其他阶段完全并发。这得益于ZGC在堆视图并发一致性设计上的改进。我们都清楚在并发的场景下需要协调各个线程对共享资源达成一致性,常用的手段就是对资源加锁,而在垃圾回收器下的思路也是类似,如果GC线程工作是需要锁定对象资源进行处理,业务线程则需要全部暂停,这就产生了STW (Stop The Word) 。以往的垃圾回收器都是让GC线程和业务线程就堆中对象地址达成一致,对象在发生转移时业务线程是不能访问的 (因为对象的地址发生了变化) ,无论G1还是CMS对象在进行复制时都是需要STW。ZGC使用到的着色指针(Colored Pointer)和读屏障(Load Barrier)技术,可以让所有线程在并发的条件下就指针的颜色 (状态) 达成一致,而不是对象地址。因此,ZGC可以并发的复制对象,这大大的降低了GC的停顿时间。我们先对着色指针和读屏障有个初步的理解,然后在通过ZGC回收周期来看这2项技术的具体运用。

着色指针(Colored Pointer)

在指针中嵌入元数据(使用地址中的高阶位来实现),这种通过在指针存储元数据的技术就叫做着色指针 (Colored Pointer) 。ZGC中指针始终是64位结构,由元位(指针的颜色)和地址位组成。地址位数决定了理论上支持的最大堆大小,ZGC使用42位存储地址也就意味着ZGC最大支持4TB堆内存。如图所示,低42位是地址位,中间4位是元位,高18位未使用。四个元位是Finalized ( F )、Remapped ( R )、Marked1 ( M1 ) 和Marked0 ( M0 )。

图4: 64位地址使用示意图

ZGC中将指定上的标记通过颜色来表示,颜色可以是“good” (地址有效) 或“bad” (地址可能无效) 。指针的颜色由其元位的状态决定:F、R、M1和M0。“good”是R、M1、M0元位中的一个被设置,另外三个未设置,比如0100、0010和 0001属于“good”颜色。通过在指针上的颜色就能区分出对象状态,不用额外做内存访问,这使得ZGC在标记和转移阶段会更快。

通过设置地址元位的状态,可以形成不同地址视图,ZGC同一物理堆内存被映射到虚拟地址空间三次,从而产生同一物理内存的三个“视图”,GC活动的不同时期会只存在一个活跃视图,根据垃圾回收的周期ZGC通过切换不同视图标来记出对象的颜色。

下图是虚拟地址的空间划分:

图5:虚拟地址空间划分和多视图映射

[0~4TB) 对应Java堆;

[4TB ~ 8TB) 称为M0地址空间;

[8TB ~ 12TB) 称为M1地址空间;

[12TB ~ 16TB) 预留未使用;

[16TB ~ 20TB) 称为Remapped空间。

ZGC是不分代的,这意味着垃圾回收是需要扫描整个堆空间,地址视图将整个Java堆分成多个部分,并为每个部分分配一个虚拟内存段。在垃圾回收时,ZGC只需要扫描其中一个虚拟内存段,并将其作为当前视图映射到实际的内存位置。同时,ZGC会将其他虚拟内存段映射到虚拟地址上,这些内存段不会被收集器扫描。

读屏障(Load Barrier)

ZGC 通过利用读屏障而不是写入屏障,与HotSpot JVM中以前的GC (CMS,G1等) 算法显著不同。读屏障解决了并发转移时对象指针更新问题:在转移期间,如果移动对象而不用更新引用对象的传入指针(移动的对象可能被堆中的任何其他对象所引用),就会产生悬空指针 (已经被释放的内存空间或者无效的内存地址,访问悬空指针会出现问题) 。通过读屏障技术能够捕获此类悬空指针对象,并触发代码,更新对象的新位置,从而“修复”悬空指针。为了跟踪对象如何移动,以便在加载时固定悬空指针,ZGC中使用转发表 (forwarding tables ) 来将重定位前(旧)地址映射到重定位后(新)地址。无论是业务线程作为使用者访问对象,还是GC线程遍历堆中的所有活动对象(在标记期间)都有可能会触发读屏障。

ZGC读屏障如何实现呢?举个例子,代码 var x = obj.fieldvar x = obj.fieldvar x = obj.field。x是一个位于堆栈上的局部变量,field是一个位于堆上的指针。业务线程在操作堆对象时触发读屏障。读屏障的执行路径有快 (fast path) 和慢 (slow path) 两种,如果正在加载的指针有效状态 (good color) ,则采用加载屏障的快速路径,否则,采用慢速路径。快速路径实际上是空的,而慢速路径包含计算有效状态指针的逻辑:检查对象是否已经(或即将)重新定位,如果是,则查找或生成新的地址。读屏障除了能让触发读屏障的线程读取到最新地址,同时还具有自我修复指针(self-healed)的功能,这意味着读屏障会修改指针的状态,以便后续其他线程访问时能执行快速路径。无论采用哪条路径,都会返回正确状态的地址。下面用伪代码表示ZGC在执行读屏障时的大体逻辑:

/**
slot 是值线程栈中的局部变量,也就是屏障要操作的目标对象
*/
unintptr_t barrier(unintptr_t *slot,unintptr_t addr){
    //快速路径,fast path
    if(is_good_or_null(addr))return addr;
    //慢速路径,slow path
    good_addr = process(addr);
    //自我修复
    self_heal(slot,addr,good_addr);
    return good_addr;
}
/*
自我修复,将指针恢复到正常状态
*/
void self_heal(unintptr_t *slot,unintptr_t old_addr,unintptr_t new_addr){
    if(new_addr == 0)return;
    while(true){
        if(CAS(slot,&old_addr,new_addr)
        return;
        if(is_good_or_null(old_addr))
        return;
    }
}

ZGC的读屏障可能被GC线程和业务线程触发,并且只会在访问堆内对象时触发,访问的对象位于GC Roots时不会触发,这也是扫描GC Roots时需要STW的原因。

下面是一个简化的示例代码,展示了读屏障的触发时机。

Object o = obj.FieldA   // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o  // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i =  obj.FieldB  //无需加入屏障,因为不是对象引用

五、ZGC执行周期

如下图 所示,ZGC 周期由三个 STW 暂停和四个并发阶段组成:标记/重新映射( M/R )、并发引用处理( RP )、并发转移准备( EC ) 和并发转移( RE )。为了读者能快速理解,下面对ZGC执行过程进行了大量简化。

图6:ZGC周期表示

初始标记(STW1)

ZGC 初始标记执行包含三个主要任务。

  • 地址视图被设置成M0 (或M1) ,M0还是M1根据前一周期交替设置的。
  • 重新分配新的页面给业务线程创建对象,ZGC只会处理当前周期之前分配的页面。
  • 初始标记只会存活的根对象被标记为M0 (M1) ,并被加入标记栈进行并发标记。

GC周期中地址视图窗口

图7:ZGC周期中状态窗口划分

并发标记(M/R)

并发标记的任务有2个:

第一,并发标记线程从待标记的对象列表出发,根据对象引用关系图遍历对象的成员变量,递归进行标记。

第二,计算,并更新关联页面的活跃度信息。活动信息是页面上的活动字节数,用于选择将要回收的页面,这些对象将作为堆碎片整理的一部分进行重新定位。

下面伪代码是并发标记的主要过程:

while(obj in mark_stack){
    //标记存活对象,当且仅当该对象未被标记并且当前线程成功标记该对象时才返回true
    success = mark_obj(obj);
    if(success){
        for(e in obj->ref_fields()){
            MarkBarrier(slot_of_e,e);
        }
    }
}
//GC线程调用
//EC是待回收页面的集合
void MarkBarrier(uintptr_t *slot,unintptr_t addr){
    if(is_null(addr))return;
    //判断是否在待回收集合内
    if(is_pointing_into(addr,EC)){
        //地址重映射到当前GC视图
        good_addr = remap(addr);
    } else {
        good_addr = good_color(addr);
    }
    //访问的对象添加到标记栈
    mark_stack->add(good_addr);
    self_heal(slot,addr,good_addr);
}

//读屏障前面有介绍过,由业务线程调用
void LoadBarrier(uintptr_t *slot,unintptr_t addr){
    if(is_null(addr))return;
    if(is_pointing_into(addr,EC)){
        good_addr = remap(addr);
    } else {
        good_addr = good_color(addr);
    }
    mark_stack->add(good_addr);
    self_heal(slot,addr,good_addr);
    return good_addr;
}

代码片段显示了并发标记阶段的GC线程主循环。mark_obj()当且仅当该对象未被标记并且当前线程成功标记该对象时才返回 true。它在内部使用原子操作(compare and swap,CAS)来设置位图中的位,因此它是线程安全的。MarkBarrier()遍历该对象的成员属性,完成对象引用的标记。并发标记时业务线程也在运行,此前如果业务线程访问对象将执行LoadBarrier()协助GC线程完成对象标记。

再标记阶段(STW2)

再标记阶段的主要任务有3个:

  • 执行修复任务,指线程运行C2编译的代码,在进入再标记阶段时可能发生漏标。
  • 结束标记,并发标记后业务线程本地标记栈可能存在待标记的对象,执行本步骤的目的就是对这些待标记对象进行标记。
  • 执行部分非强根并行标记。

并发转移准备(EC)

并发转移准备任务:

  • 筛选所有可以被回收的页面
  • 选择垃圾比较多的页面作为页面转移集

初始转移(STW3)

初始转移主要以下过程:

  • 调整地址视图:将地址视图从M0或者M1调整为Remapped,说明进入真正的转移,此后所有分配的对象视图都是Remapped。
  • 重定位TLAB:因为地址视图调整,所以要调整TLAB中地址的视图。
  • 开始转移:从根集合出发,遍历根对象的直接引用的对象,对这些对象进行转移。

初始转移是STW的,其处理时间和GC Roots的数量成正比,一般情况耗时非常短。

并发转移(RE)

初始转移完成了GC Roots对象重定位,在并发转移阶段将对前面步骤确定的转移集 (EC) ,对转移集的每一页执行转移。

并发转移的过程可以抽象成如下伪代码过程:

//GC线程主循环遍历EC的页面,将个将EC集页面中对象进行转移
for (page in EC){
    for(obj in page){
        relocate(obj);
    }
}
//该方法GC和业务线程都有可能执行,如果是业务线程访问对象会先进行转移在进行操作
unintptr_t relocate(unintptr_t obj) {
    //获取对象的地址转发表
    ft = forwarding_tables_get(obj);
    if (ft->exist(obj)){
        return ft->get(obj);
    }
    new_obj = copy(obj);
    //CAS写对象转发表数据
    if(ft->insert(obj,new_obj)){
        return new_obj;
    }
    //CAS发生竞争,写转发表失败,释放分配的内存
    dealloc(new_obj)
    return ft->get(obj);
}

转发表的作用是存储对转移后旧地址到新地址的映射,转发表的数据存储在页面中,转移完成的页面即可被回收掉。

并发转移完成之后整个ZGC周期完成。

六、ZGC算法演示

为了说明ZGC算法,下图演示了示例中的所有阶段。

图8:ZGC算法演示

图8(1)显示了堆的初始状态,应用启动后ZGC完成了初始化。

在图8(2)中,选择M0作为全局标记,并且所有根指针都被标记成M0。然后,所有根都被推送到标记堆栈,该标记堆栈在并发标记 (M/R) 期间由GC线程消耗。

如图8(3)所示,图中用合适的颜色绘制对象本身,以表明它们已被标记,即使指针有状态。

在图8(4) 中,选择存活对象最少的页面(中间的页面)作为转移候选集 (EC) 。

随后,在图8(5)中,全局标记被设置为Remmaped,并且所有根指针都已更新Remmaped。如果根指向EC,则相应的对象将被重新定位,并且根指针更新为新地址。

在图8(6)中,EC中的对象被转移,并且地址记录被逐出页面中转发表上,用于新旧地址转换。当并发转移阶段结束时,当前GC周期也会结束。当前周期内整个EC都会被回收。这里可能有个疑问,对象的旧地址还没有更新,页面如果被回收了如何还能访问对象呢?原因是回收的是页面中对象存储空间,转发表不会被回收,如果此时业务线程访问这些对象,会触发读屏障的慢路径位,失效指针会被修复。对于没有访问到的失效指针,直到下一个GC并发标记 (M/R) 阶段才会被修复。

在图8(7)中,下一个GC循环开始,M1被选择为全局状态(M0 和 M1 之间交替使用)。

在图8(8)中,并发标记阶段 (M/R) 通过查询转发表失效的指标被映射到新位置。

最后,在图8(9)中,上一周期EC页面的转发表被回收,为即将到来的并发转移 (RE) 阶段做准备。

七、总结

ZGC是一个十分复杂的JVM子系统,没办法通过一篇文章把所有的细节描述清楚。本文详细探讨了ZGC的着色指针和读屏障关键技术,他们也是ZGC中创新点,最后通过一个示例对ZGC算法过程做了一个简化版的演示。通过对ZGC这种复杂系统的学习,让我也体会到分析复杂系统时没必要一开始就过多的纠结实现细节,可以先从关键流程入手再层层深入。

ZGC的高并发设计造就了它的高性能,背后要归功于着色指针和读屏障运用,当然除了这2项还有其他精妙的设计比如:内存模型,并发模型,预测算法等这里不展开,读者可以参考其他文章。了解ZGC的基本原理可以帮助优化应用程序的性能,为应用调优做些知识储备。最后,ZGC有卓越的性能和稳定性表现,我们在选择GC选型时可以优先考虑使用ZGC。

参考内容:

[1]彭成寒:《新一代垃圾回收器ZGC设计与实现》.机械工业出版社, 2019.

[2]https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html

[3]https://www.baeldung.com/jvm-zgc-garbage-collector

[4]https://openjdk.org/projects/zgc/

[5]https://www.jfokus.se/jfokus18/preso/ZGC--Low-Latency-GC-for-OpenJDK.pdf

*文 / byteyangyang

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1109847.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

tika解压遇到压缩炸弹如何继续解压

1.问题 项目中要对10层压缩的zip、7z等文件用tika解压遇到错误&#xff1a;tika zip bomb detected 也就是说tika认为这是个压缩炸弹。 “压缩炸弹”是一个压缩包文件的木马程序&#xff0c;通常只有几百KB&#xff0c;解压后会变成上百MB或者上GB庞然大物。把你本地磁盘占满…

PTE-精听学习(三)

目录 WFD 答题技巧 多写单词不扣分数 RS 只有三秒钟准备时间 空挡时间1-2秒钟 WFD 犹豫 扣分点 fluency 单词一致顺序也是一致 总会有题目简单到有短又容易理解 预测的命中率 抽屉理论 印度发音其实是算标准的 反正就是归于模仿 影子跟读法 打拍子 看前缀…

UE4 Ultradynamicsky进行地面交互

第一步&#xff1a; 找到地面材质&#xff0c;进入地面材质 找到该节点 找到之前本该连在这里的&#xff0c;现在连到这个节点上&#xff0c;然后把这个节点的输出连到材质输出节点上 新建一个物理材质 然后给过来 找到Ultra_Dynamic_Weather&#xff0c;把Enable Dynamic Lan…

airflow报ModuleNotFoundError: No module named ‘dags‘原因和解决方法

ModuleNotFoundError: No module named ‘dags’ 原因&#xff1a;airflow默认是从dags目录下开始搜所有模块&#xff0c;如果你加上dags目录名&#xff0c;就相当于在dags目录下找dags包。 解决方法&#xff1a;导入的时候&#xff0c;去掉dags&#xff0c;详细可以参考下面案…

Python中的With ...as... 作用

Python中的with … as …作用&#xff1a; 1、通过with语句可以得到一个上下文管理器 2、执行对象 3、加载__enter__方法 4、加载__exit__方法 5、执行__enter__方法 6、as 可以得到enter的返回值 7、拿到对象执行相关操作 8、执行完了之后调用__exit__方法 9、如果遇到异常&a…

Android之使用GirdLayoutManager时候给Item设置边距

效果&#xff1a; 一、自定义设置边距方法 SpaceItemDecoration.java package com.custom.jfrb.ui.jfrb.finishedProduct; //自己包名位置import android.graphics.Rect; import android.view.View;import androidx.annotation.NonNull; import androidx.recyclerview.widg…

threejs(2)-Geometry进阶详解

一、全面讲解UV与应用 在本节中&#xff0c;我们将讨论Three.js中的UV映射&#xff0c;包括UV映射的概念、与顶点位置的关系和区别以及如何在Geometry中设置UV坐标。我们将使用BufferGeometry进行示例说明。 颜色对应 什么是UV映射&#xff1f; UV映射是一种将二维纹理映…

【根据车间号[81321000]未找到ERP逻辑仓】

以条码Z42310062781622举例&#xff0c;WMS集成报错。 先说业务逻辑&#xff1a; 新建包装工单&#xff0c;维护包装批次管制时&#xff0c;会把包装批次管制里的部门信息传给115。 赛龙捷包装后&#xff0c;会根据115里的这个部门对应的车间号&#xff0c;返还给MES。 MES会…

程序连接oracle查询数据的环境配置

连接oracle 数据库真麻烦&#xff0c;还是MySQL方便 Oracle Instant Client 这个东西的版本跟oracle的版本是有讲究的&#xff0c;引用文档的说明 Oracle 标准的客户端-服务器网络互操作性允许不同版本的 Oracle 客户端和 Oracle 数据库之间的连接。有关经过认证的配置&#…

springboot之quartz动态可控定时任务

Quartz Quartz是一个开源的任务调度框架&#xff0c;可以用来实现定时任务的调度&#xff0c;如定时发送邮件、定时备份数据等。Quartz具有很高的可靠性和灵活性&#xff0c;支持集群部署和分布式调度&#xff0c;并且提供了丰富的API和插件&#xff0c;可以轻松实现复杂的调度…

2023年中国GPS导航设备产量、销量及市场规模分析[图]

GPS导航设备行业是指生产和销售用于导航、定位和监控目的的GPS设备的行业&#xff0c;可以用于汽车、船只、飞机、人员和其他物体的定位和导航&#xff0c;以及用于地理信息系统&#xff08;GIS&#xff09;、测绘、海洋抢险、森林监测、地质勘探、气象预报、交通管理、物流跟踪…

中国地级市-环境规制18个相关指标数据(2002-2021年)

本次数据为地级市-环境规制相关指标&#xff0c;主要用于计算城市的环境规制、污染排放水平。指标数目总计18个&#xff0c;数据来源为《中国城市统计年鉴》&#xff0c;存在一定缺失。数据包含原始、线性插值、回归填补3个版本&#xff0c;希望对大家有用 一、数据介绍 数据…

uni-app:js二维数组与对象数组之间的转换

一、二维数组整理成对象数组 效果 [ ["前绿箭","DI10","RO1"], ["前红叉","DI2","RO2"], ["后绿箭","DI12","RO3"], ["后红叉","DI4","RO6"] ] …

2023国考证件照要求什么底色?证件照换背景底色的方法

2023年国家公务员考试报名已经开始了&#xff0c;我们在考试平台提交报名信息的时候&#xff0c;有一项就是需要上传证件照片&#xff0c;对于证件照片也会有具体的要求&#xff0c;比如背景底色、尺寸大小、dpi和kb大小。今天就为大家详细介绍一下关于国考证件照背景色的内容&…

数据结构与算法课后题-第六章(图的存储及基本操作)

文章目录 1、选择题12、选择题23、选择题34、选择题45、选择题56、选择题67、选择题78、选择题89、选择题910、选择题1011、选择题1112、选择题1214、选择题1415、选择题1516、选择题16 1、选择题1 2、选择题2 3、选择题3 4、选择题4 5、选择题5 6、选择题6 7、选择题7 8、选择…

zzy-project-cli,提供多个框架的脚手架

npm地址 install npm install zzy-project-cli -g做什么&#xff1f; 将多个可选的框架提供给使用者选择&#xff0c;选中后自动下载对应模板&#xff0c;快捷使用。 使用 step1 zzy-cli create [项目名称]step2 获取模板之后选取任一进行下载 下载完成之后即可使用 模…

藏在超级应用背后的逻辑和哲学

众所周知&#xff0c;Elon Musk 想将 Twitter 重新设计定位成一款“超级应用 - X”的野心已经不再是秘密。伴随着应用商店中 Twitter 标志性的蓝鸟 Logo 被 X 取代后&#xff0c;赛博世界充满了对这件事情各种角度的探讨与分析。 Musk 曾经无数次通过微信这一样本来推广他的“超…

龙芯3A3000源码编译安装deepin-ide

安装环境 系统为统领专业版1050 CPU为龙芯3A3000 安装步骤 1.安装所有依赖库 sudo apt-get install git debhelper cmake qt5-qmake qtbase5-dev qttools5-dev qttools5-dev-tools lxqt-build-tools libssl-dev llvm llvm-dev libclang-dev libutf8proc-dev libmicrohttpd-d…

利用MixProxy自动录制生成Pytest案例:轻松实现测试脚本编写!

前言 进行接口自动化时&#xff0c;有时候往往没有接口文档&#xff0c;或者文档更新并不及时&#xff0c;此时&#xff0c;想要获取相关接口&#xff0c;通过抓包是一种快速便捷的手段。抓包获取到接口后&#xff0c;开始写接口用例&#xff0c;此时需要复制请求url、请求参数…

人工智能驱动的个性化学习:技术如何彻底改变教育

随着计算机辅助教学的出现&#xff0c;人工智能在教育领域的发展始于20世纪50年代。然而&#xff0c;在20世纪90年代&#xff0c;由于机器学习和数据处理的进步&#xff0c;该领域开始出现大幅增长。人工智能在教育领域的早期应用之一是智能辅导系统&#xff08;ITS&#xff09…