首页 文章 精选 留言 我的

精选列表

搜索[c 17],共10000篇文章
优秀的个人博客,低调大师

《SpringMVC7实战》第17招 View Technologies仍要知道请求怎么出去

# REST 与服务端渲染同源:Spring MVC View 层可插拔实战 ```text 旧方案:纯 REST 后端只输出 JSON,管理报表需求来了 冲突:运营要浏览器直接看 HTML 报表,前端团队没排期 后果:临时拼字符串返回 HTML,维护失控,View 层能力被浪费 ``` 我是老李,在深圳南山科技园一家宠物医疗 SaaS 公司做架构师。 我们的 spring-petclinic-rest 一直只输出 JSON,前端用 React 单独部署。 上周运营总监在周会上拍桌子,说要看一张管理报表,浏览器打开就能看,不要等前端排期。 为什么 REST 后端遇到 HTML 报表会这么别扭? 是继续用 @ResponseBody 拼 HTML 字符串,还是启用 Spring MVC 的 ViewResolver 走服务端渲染? 如果选错,会不会把 Controller 写成大泥球,或者引入一套和现有 REST 完全割裂的页面框架? # 01、故事 ```text 周一 09:40→深圳南山科技园→给 Petclinic 加管理报表页→运营催得急,前端没排期 ``` 周一早上九点四十,我坐在工位上,任务是给 spring-petclinic-rest 增加一个 /admin/report 管理报表页。 运营总监在群里连发三条消息,说下午就要看兽医接诊量汇总。 前端组长说本周迭代已满,没法接。 我打开项目,发现只有 REST Controller,所有方法都标着 @ResponseBody。 搜了一圈,没有 ViewResolver,没有模板引擎,也没有静态资源映射。 脑子里闪过一个念头:难道要像十年前那样在 Controller 里用 StringBuilder 拼 HTML? > 晨会催声急,报表无排期 > 后端本无页,何来 HTML > 欲问 Spring 深处,可有视图梯 > 且把源码翻起,寻那渲染机 # 02、问题 ```text 需求变化:纯 JSON API → 增加浏览器直接访问的 HTML 报表 冲突:REST Controller 只懂 @ResponseBody,没有 View 层配置 后果:临时拼 HTML 字符串,转义、布局、维护全部失控 ``` 纯 REST 项目里,所有 Controller 方法的返回值都被 @ResponseBody 交给 HttpMessageConverter,序列化成 JSON 写回响应体。 现在要返回 HTML 页面,如果继续用 @ResponseBody 返回 String,Spring 会把它当普通文本处理,不会走视图渲染。 有人会手动拼接 HTML 字符串,但报表里有表格、日期格式化、金额千分位,还有用户输入需要转义。 拼字符串不仅容易 XSS,改一个样式要动 Java 代码,重新编译部署。 可验证的验收标准是:浏览器访问 /admin/report 返回完整 HTML 文档,Content-Type 为 text/html,且表格数据来自后端 Model。 > 纯 JSON 路,行至半途遇岔口 > 拼串如饮鸩,转义布局皆成仇 > 视图本有径,藏在 Dispatcher 后 > 不识庐山面,只缘身在此山游 # 03、原理 ```text DispatcherServlet → HandlerMapping → Controller → ModelAndView → ViewResolver → View.render → HTML DispatcherServlet → HandlerMapping → Controller → @ResponseBody → HttpMessageConverter → JSON ``` Spring MVC 的核心是 DispatcherServlet,它不关心最终输出是 JSON 还是 HTML。 Controller 返回 ModelAndView 时,DispatcherServlet 拿着逻辑视图名去找 ViewResolver,解析出具体 View 对象,再调用 View.render 把 Model 数据渲染成响应体。 @ResponseBody 是另一条捷径:Controller 直接返回对象,DispatcherServlet 跳过 ViewResolver,用 HttpMessageConverter 把对象序 列化。反直觉的是,REST 和服务端渲染不是两套框架,而是同一个 DispatcherServlet 的两条分支。 你完全可以在同一个项目里让 /api/vets 返回 JSON,让 /admin/report 返回 HTML。 > 一条主路分两径,JSON 与 HTML 同根生 > 视图解析非魔法,接口插拔自有凭 > 莫道 REST 无页面,ModelAndView 藏真经 > 反直觉处须记取,同源异流是本性 # 04、架构 ```text HTTP 请求 → DispatcherServlet → Controller → ModelAndView → ViewResolver → View → HTML 响应 边界:ViewResolver 只负责找 View,View 只负责渲染,Controller 只负责准备 Model ``` View 层在 Spring MVC 里是可插拔设计。 ViewResolver 接口只有一个 resolveViewName 方法,View 接口只有 render 和 getContentType。 Thymeleaf、FreeMarker、JSP、PDF、Excel 都是这套接口的实现。 收益是 Controller 不用关心输出格式,换模板引擎只改配置不改业务代码。 代价是引入模板引擎依赖,增加学习成本。 适用条件是服务端渲染页面、邮件模板、报表导出等场景。 纯 API 服务不需要 ViewResolver,保持轻量。 > 接口两方法,插拔自从容 > 模板换 FreeMarker,业务不动松 > 边界划清楚,Model 归 Controller 中 > 渲染交给 View,各司其职通 # 05、第一版实战 ```text 环境:Spring Boot 3.x + spring-petclinic-rest → 加 Thymeleaf 依赖 → 配置 ViewResolver → 写 Controller → 写模板 → 启动验证 → V1 ``` 在 pom.xml 加入 Thymeleaf 依赖。 Spring Boot 自动配置 ThymeleafViewResolver,默认前缀 classpath:/templates/,后缀 .html。 ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` 新建 AdminReportController,返回 ModelAndView,逻辑视图名为 admin/report。 ```java @Controller @RequestMapping("/admin") public class AdminReportController { private final VetRepository vetRepository; public AdminReportController(VetRepository vetRepository) { this.vetRepository = vetRepository; } @GetMapping("/report") public ModelAndView report() { List vets = vetRepository.findAll(); Map model = new HashMap<>(); model.put("vets", vets); model.put("reportDate", LocalDate.now()); return new ModelAndView("admin/report", model); } } ``` 在 src/main/resources/templates/admin/ 下创建 report.html。 ```html 管理报表 兽医接诊量汇总 报表日期:2025-01-01 ID姓名专科 1 张三 内科 ``` 启动应用,访问验证。 ```bash curl -i http://localhost:9966/petclinic/admin/report ``` 预期返回 Content-Type: text/html,包含兽医表格。未在当前环境实测,以下为预期结果。 ```text 示例输出: HTTP/1.1 200 Content-Type: text/html;charset=UTF-8 管理报表 兽医接诊量汇总 ... ``` > 依赖引入如添翼,模板目录安了家 > Controller 返回视图名,Model 数据手中拿 > 启动一声春雷响,HTML 终见天光洒 > V1 初成心稍定,且看排查细如发 # 06、排查 ```text 诊断树:页面 404 → 模板路径错?→ 视图名拼写?→ 静态资源被拦截? 诊断树:页面 500 → 模板语法错?→ Model 属性缺失?→ 循环对象为 null? ``` 问题一:访问 /admin/report 返回 404。怀疑是模板文件路径不对。检查 target/classes/templates/admin/ 下是否有 report.html。 ```bash ls target/classes/templates/admin/ ``` 证据显示文件存在,但文件名是 report.html,视图名写的是 admin/report,应该能匹配。 继续检查 Controller 的 @RequestMapping,发现类上写的是 @RequestMapping("/admin"),方法上是 @GetMapping("/report"),路径没问 题。最后发现是启动类扫描包范围没包含新 Controller 所在包。 修复:把 Controller 移到主扫描包下。 问题二:页面 500,日志显示 SpelEvaluationException。 怀疑模板里 th:text 表达式写错。 检查 report.html,发现 ${vet.specialties} 是集合,直接输出会调用 toString,但 Thymeleaf 对空集合处理正常。 真正原因是 Vet 实体里 specialties 是懒加载集合,模板渲染时 Session 已关闭,抛出 LazyInitializationException。 修复:在 Controller 里用 JOIN FETCH 查询或把 specialties 转成字符串列表放入 Model。 **错误尝试:** 第一次遇到 404 时,我以为是 Thymeleaf 没生效,手动在 application.properties 里配 spring.thymeleaf.prefix 和 suf fix,结果还是 404。 浪费了半小时,根因是包扫描,不是配置。 > 排查如破案,证据链上寻 > 404 非配置,包扫描漏了人 > 500 藏懒加载,Session 关后魂 > 错误尝试记一笔,莫再绕远门 # 07、优化 ```text V1:Controller 直接返回 ModelAndView,懒加载集合在模板渲染时报错 证据:日志显示 LazyInitializationException,发生在 View.render 阶段 V2:Controller 里把实体转成 DTO,View 层只接触扁平数据 ``` V1 的问题是 Controller 把 JPA 实体直接放进 Model,模板渲染时懒加载集合可能触发数据库访问,但事务已结束。优化方案是引入 DTO,在 Controller 里完成数据组装。 ```java @GetMapping("/report") public ModelAndView report() { List vets = vetRepository.findAllWithSpecialties(); List rows = vets.stream() .map(v -> new VetReportDto( v.getId(), v.getFirstName() + " " + v.getLastName(), v.getSpecialties().stream() .map(Specialty::getName) .collect(Collectors.joining(", ")) )) .collect(Collectors.toList()); Map model = new HashMap<>(); model.put("rows", rows); model.put("reportDate", LocalDate.now()); return new ModelAndView("admin/report", model); } ``` 模板同步改为遍历 rows。 ```html 1 张三 内科 ``` > V1 埋雷懒加载,渲染时刻爆了膛 > 证据日志指方向,事务已关莫逞强 > DTO 扁平化,View 层不再碰 JPA 肠 > V2 落地稳当当,前后对比见真章 # 08、演进 ```text V2:DTO 隔离 + Thymeleaf 渲染 → 能力:HTML 报表、Redirect、Forward、静态资源、PDF/Excel 扩展点 遗留:模板里还有少量逻辑,未做分页和权限控制 ``` 用 V1 同一组请求验证:curl -i http://localhost:9966/petclinic/admin/report 返回 200,Content-Type 为 text/html,表格 数据完整,无懒加载异常。 演进后新增能力:Redirect 用 return "redirect:/admin/report" 实现重定向; Forward 用 return "forward:/admin/report" 实现服务器内转发; 静态资源放在 src/main/resources/static/ 下,Spring Boot 自动映射; PDF 用 AbstractPdfView 或 iText 实现 View 接口,Excel 用 AbstractXlsxView。 代价是引入 Thymeleaf 依赖,模板语法需要学习。适用场景是内部管理页面、报表导出、邮件模板。遗留问题是模板里还有 th:if 条件判断,后续可考虑把复杂逻辑下沉到 DTO。 > V2 验证过,同请求前后同 > 能力渐丰盈,重定向与转发通 > 静态资源自动映,PDF Excel 接口中 > 遗留分页与权限,留待后篇再进攻 # 09、洞见 ```text 从现象到判断:REST 与服务端渲染不是对立,而是同一 DispatcherServlet 的两条分支 ``` ## 反直觉判断:@ResponseBody 不是 View 层的替代品,而是绕过 View 层的捷径 ```text Controller → @ResponseBody → HttpMessageConverter → JSON Controller → ModelAndView → ViewResolver → View → HTML ``` 很多人以为 REST 项目里没有 View 层,其实 @ResponseBody 只是跳过了 ViewResolver,HttpMessageConverter 承担了类似 View 的渲染职责。 理解这一点,就不会在需要 HTML 时硬拼字符串。 ## 反直觉判断:ViewResolver 的解析结果可以缓存,但 View 实例必须无状态 ```text ViewResolver.resolveViewName → View 实例 → 并发 render ``` ViewResolver 可以缓存 View 实例,但 View.render 会被多线程并发调用。 如果 View 里存了请求相关的状态,就会串数据。 所以自定义 View 时,请求数据只能通过 render 方法的参数传入。 ## 判断:模板引擎的选择取决于团队技能和部署环境 ```text Thymeleaf:自然模板,浏览器可直接打开 JSP:依赖 Servlet 容器,打包成 jar 时受限 FreeMarker:性能好,语法简洁 ``` Thymeleaf 适合前后端分离过渡期,JSP 在 Spring Boot 可执行 jar 里支持不完整,FreeMarker 适合高并发渲染。选型要看团队现有技能,不要为了技术而技术。 ## 判断:Redirect 和 Forward 的差异决定了浏览器地址栏和请求次数 ```text Redirect:302 + Location → 浏览器发起第二次请求 Forward:服务器内部转发 → 浏览器无感知 ``` Redirect 会改变浏览器地址栏,适合防止表单重复提交。Forward 不改变地址栏,适合同一请求内跳转。选错会导致刷新时重复提交或地址栏与页面内容不一致。 > 洞见如灯照暗路,反直觉处最醒神 > 捷径非替代,绕过视图另有因 > 无状态 View 铁律,并发渲染不串门 > 重定向与转发,地址栏里见乾坤 # 10、系统落地 ```text 入口:浏览器 /admin/report → Controller → DTO 组装 → ViewResolver → Thymeleaf View → HTML 存储:VetRepository 查询数据库 未实现:分页、权限控制、PDF/Excel 导出 ``` 原来 spring-petclinic-rest 只有 REST API,所有响应都是 JSON。 本篇增加了 Thymeleaf 依赖、AdminReportController、report.html 模板,现在能通过浏览器直接访问 HTML 报表。 系统现在能同时服务 REST 客户端和浏览器用户,View 层可插拔设计得到验证。 下一篇缺的是 PDF/Excel 导出能力,需要实现自定义 View 或使用专门的视图类。 官方文档见 Spring Framework 文档首页 https://docs.spring.io/spring-framework/reference/ 和 spring-petclinic-re st 仓库 https://github.com/spring-petclinic/spring-petclinic-rest 。 > 系统落地图,入口到存储清 > 本篇加模板,HTML 报表生 > 未实现分页权限,导出待后行 > 官方文档可核验,主页仓库指路径明 # 11、小结 ```text 三问三答:为什么别扭?→ 因为纯 REST 项目没有 ViewResolver 选 @ResponseBody 还是 ViewResolver?→ 看输出类型,HTML 走 ViewResolver 选错后果?→ Controller 拼字符串,维护失控 ``` 为什么 REST 后端遇到 HTML 报表会别扭? 因为项目里只有 @ResponseBody 和 HttpMessageConverter,没有 ViewResolver 和模板引擎,返回 HTML 没有现成通道。 是继续用 @ResponseBody 拼 HTML 字符串,还是启用 ViewResolver? 答案是启用 ViewResolver,让 Controller 返回 ModelAndView,模板引擎负责渲染,Controller 只准备数据。 选错会怎样?Controller 里拼 HTML 字符串,转义、布局、维护全部失控,XSS 风险高,改样式要重新编译部署。 当前状态:/admin/report 已能返回 HTML,DTO 隔离了 JPA 实体,View 层可插拔设计验证完毕。 > 三问三答毕,迷雾散如烟 > 别扭因无视图,通道未建全 > 选型看输出,HTML 走 ViewResolver 边 > 选错拼串苦,维护如登山 # 12、作业 ```text 理解→实战→排障→架构判断 ``` ## 理解题:@ResponseBody 和 ViewResolver 在 DispatcherServlet 处理流程中的分叉点在哪里? 分叉点在 Controller 方法返回后。 DispatcherServlet 检查 HandlerMethod 是否标注 @ResponseBody 或返回值类型是 ResponseEntity。 如果是,走 HttpMessageConverter 序列化; 否则把返回值当作逻辑视图名,交给 ViewResolver 解析。 ## 实战题:给 /admin/report 增加一个 CSV 导出功能,点击按钮下载 CSV 文件 在 AdminReportController 增加 @GetMapping("/report.csv"),用 produces = "text/csv" 返回 ResponseEntity,或实现 AbstractCsvView 注册为 View。 Controller 里复用 DTO 组装逻辑,把 rows 转成 CSV 字节流,设置 Content-Disposition: attachment; filename=report.csv。 ## 排障题:访问 /admin/report 返回 500,日志显示 TemplateInputException,如何定位? 先看异常堆栈里的模板文件名和行号,定位到具体 th: 表达式。 检查 Model 里对应属性是否存在、是否为 null。 用调试模式启动,在 Controller 返回前打印 model.keySet(),确认属性名与模板一致。 如果是 th:each 报错,检查集合是否为 null,可在模板里用 th:if="${rows != null}" 防御。 ## 架构判断题:项目同时有 REST API 和服务端渲染页面,是否应该拆成两个独立应用? 不一定。如果页面数量少、与 API 共享同一领域模型,放在同一应用里成本最低,View 层可插拔设计天然支持。 如果页面复杂度高、需要独立前端团队和独立部署节奏,可以拆成两个应用,但要注意 API 版本管理和跨域配置。 判断标准是团队边界和部署频率,不是技术纯洁性。 > 作业四题如四关,理解实战排障判 > 分叉点处明流程,CSV 导出动手干 > 模板异常看行号,属性缺失莫慌乱 > 拆与不拆看团队,莫为纯洁拆两半 # 13、升华 ```text 工程结论:REST 与服务端渲染同源,View 层可插拔是 Spring MVC 的架构红利 ``` Spring MVC 的 View 层不是页面技术,而是一套可插拔的渲染接口。 REST 的 JSON 输出和服务端渲染的 HTML 输出,本质都是 DispatcherServlet 的两条分支。 理解这一点,就不会在需要页面时硬拼字符串,也不会为了页面引入一套与 Spring MVC 割裂的框架。 下一篇将深入 PDF/Excel 视图扩展,实现自定义 View 接口,把报表导出能力补齐。 > 同源异流本一脉,可插拔处见真功 > 莫道 REST 无页面,视图接口自从容 > 工程判断有依据,不拼字符串不盲从 > 下篇导出 PDF Excel,自定义 View 再相逢 **系列项目:** 宠物医疗 SaaS 后端服务,服务对象为宠物医院运营与兽医 **已完成:** 环境工具、Petclinic 启动、DispatcherServlet 流程、Controller 绑定、数据转换、Filter、错误处理、MVC 配置、函数式端点、URI 链接、CORS、安全、Range、缓存、异步 **本篇:** View 层可插拔实战,Thymeleaf 服务端渲染,REST 与 HTML 同源对比,Redirect、Forward、静态资源 **下一篇:** PDF/Excel 自定义 View 扩展,报表导出能力落地

优秀的个人博客,低调大师

《WorkBuddy Skills 实战》第17招 15个别一次全装

## 01、痛点 你是否也有过这种经历:在某个技术群里看到别人推荐了一个新工具,觉得“哇,这个肯定有用”,于是马上装进开发环境。装完头两天确实新鲜,但很快,启动卡顿、快捷键冲突、配置报错接踵而至。我是老李,最近团队里一个小伙子就因为这个吃了苦头。他给自己的开发环境一口气塞了二十多个扩展包,结果每次打开项目要先处理三四个弹窗冲突,真正写代码的时间反而少了。更糟的是,他后来花了一个周末卸载了十几个,留下 5 个,工作效率才恢复。这个场景不是孤例。现在各种 AI 工具和自动化扩展爆发式增长,选择困难成了新问题。很多人不是在用工具,而是在被工具用。今天我想认真聊聊,一个普通开发者到底该怎么选配自己环境里的能力包,而不是盲目追求数量。因为再不刹车,你的工作流可能会变成一场维护灾难。 ## 02、是什么 这里说的“围绕每日循环的能力包组合”,核心是把工具选配的起点从“有什么”切换到“我每天重复做什么”。每日循环,就是你一周里反复出现的具体工作流:写后端接口、写单元测试、提交合并请求、查容器日志、修线上缺陷、写工作周报。能力包,则是能嵌入这些环节里替你省时间的小程序、脚本或配置。它们的正确身份不是“功能展示架”,而是“流水线上的自动化夹具”。比如你每天都要在浏览器里验证界面交互,那浏览器自动化工具就非常值得配;如果你半年不碰前端,那再热门的样式检查工具也和你无关。这个选配逻辑并不新鲜,它就是工程里“按需求选型”的延伸,只是面对 AI 工具时,很多人会把它抛在脑后。 ## 03、为什么 为什么数量不能解决问题?因为每增加一个能力包,你就可能增加四类成本。第一,触发冲突。两个扩展都想绑定同一个快捷键或者同一个命令前缀,结果一个覆盖另一个,或者干脆同时失效,而你以为是环境坏了。第二,依赖成本。某个扩展会拉进一堆底层依赖包,版本不一致时能把你的运行环境直接搞崩,排查起来非常痛苦。第三,上下文成本。每个扩展在加载时都会占用内存和启动时间,数量多了以后,打开项目从 10 秒变成 40 秒,心流被打断。第四,维护复杂度。你不仅要学它怎么用,还要持续跟踪升级、处理兼容性问题、排查误触。我见过一个开发者维护 18 个扩展,每周要专门留半天处理它们的更新和排错,但真正高频调用的只有 5 个,剩下的全是“以后可能用得上”。这个“以后”往往永远不会来,可维护成本却实实在在每天在付。 ## 04、架构 我更推荐分层选配,而不是平铺堆砌。核心层:先装 4 个基础能力,能力包制作器、能力包安装器、计划与调试扩展、浏览器自动化工具。制作器负责把一段你觉得重复的操作封装成可复用能力;安装器负责安全地加载、卸载和管理其他能力包;计划与调试扩展帮你把模糊任务拆成可执行步骤并逐步验证;浏览器自动化工具则覆盖所有网页操作和前端验证。这四样构成你的工作台底座。工作流层:根据你的实际情况补一个,不要多。如果你提交合并请求频率很高,就补一个持续集成能力;如果你负责线上服务,就补错误监控平台;如果你做内容工作,就补防废话过滤器和近 30 天资料整理器。注意是“或”不是“和”。业务层:等你稳定使用一周后,根据记录决定要不要再添加。这个架构的精髓是:底层打稳,中间按需,顶部克制。 ## 05、实战 具体操作分四步。第一步,拿出一张纸或打开一个空白文档,回顾最近两周,写下所有重复做过 3 次以上的具体工作。一定要具体到动作,比如“给接口补充字段说明”,而不是宽泛的“开发”。第二步,按出现频率从高到低排序,只保留前 5 到 8 个。如果超过 8 个,说明你的工作内容比较杂,可以考虑合并同类项。第三步,对每个高频动作问一句:有没有一个能力包能替我完成或明显加速?如果没有,就跳过,别硬凑。第四步,从这些候选里选出覆盖范围最广的 5~8 个,安装并试用。给你看我的例子:我过去两周高频动作是写接口、写单元测试、提交合并请求、看容器日志、修缺陷、写周报。我选了 6 个能力包,分别对应接口文档生成、测试断言辅助、合并请求描述生成、日志解析、错误定位、周报汇总。我没有安装任何前端样式或数据可视化工具,因为那两周我一次都没碰。结果环境启动从 40 秒降到 12 秒,误触冲突基本消失,写代码也更容易进入状态。 ## 06、洞见 这里有个反直觉的结论:最优组合不是数量最多,而是覆盖最多重复工作。覆盖率不等于数量。一个能同时处理“提交合并请求、测试、日志”的大而全扩展,可能比三个专精扩展更适合你;反过来,三个小工具如果各管一摊,只要你每天确实用三次,也比一个花哨但不落地的工具强。还有一点常被忽视:推荐星级只是使用建议,不是官方评级,更不是你的实际需求。别人给五星,可能是因为他天天写前端,而你天天写后端;别人说“一般”,可能是因为他的场景太窄。星级是参考,不是勋章。真正有效的验证方式,是看你自己的工作记录里,哪个工具被调用的次数最多、帮你省了最多时间。调用频率不会说谎。 ## 07、行动 如果你只做一件事,那就是:先少装,用一周,再决定保留。千万别试图第一天就把所有推荐拉满。具体清单如下:第一,今天只装 4 个核心能力:制作器、安装器、计划与调试、浏览器自动化。第二,正常工作一周,每天下班前花两分钟记录哪个扩展帮你省了时间,哪个一次没用。第三,周末打开记录,保留用过 5 次以上的,没用的直接卸载,别心疼。第四,如果某类重复工作确实没有工具覆盖,再针对它去找对应的能力包。第五,每两周复一次盘,防止工具慢慢又堆起来。这个方法很朴素,但它能帮你把工具组合从“收藏夹”变回“工作台”。工具不是用来收藏的,是用来干活的。 ## 08、小结 工具不是资产,重复劳动才是成本。普通开发者不需要一个几十项的能力清单,只需要一个能覆盖你每天真实工作在做的 5~8 件事的精简组合。围绕每日循环去选,而不是围绕热门榜单去装。先少装,用起来,再根据使用记录决定保留。这样你的开发环境会轻很多,冲突会少很多,省下的时间才是真正属于你的生产力。记住,少即是多,尤其在工具这件事上。 ## 09、链接与概念说明 为统一表述,这里解释几个文中用到的概念。每日循环:指一个开发者每天、每周反复执行的具体工作任务,比如写接口、提交合并请求、查日志。能力包:可安装到开发环境中的自动化扩展、插件或脚本,用来完成某类高频工作。能力包制作器:帮助把一段重复操作封装成可复用能力包的工具。能力包安装器:负责加载、卸载和管理其他能力包的基础工具。计划与调试扩展:用于拆解任务、生成执行计划并逐步验证结果的辅助工具。浏览器自动化工具:可模拟人在浏览器中的点击、输入、导航等操作,用于前端验证和数据采集。持续集成:当代码提交后自动运行构建、测试和部署流水线的机制。错误监控平台:收集线上应用报错并归因、告警的服务。防废话过滤器:用于过滤生成文本中的空话、套话和冗余表达。近 30 天资料整理器:从指定时间范围内的文档或记录中提取要点的工具。需要说明,文中提到的具体名称仅为举例,实际选型请结合自身工作场景。建议优先选择活跃维护、社区反馈稳定的版本,并定期检查更新。避免一次性安装过多,以免引入不必要的冲突。

优秀的个人博客,低调大师

OpenJDK17-JVM源码阅读-ZGC-并发标记 | 京东物流技术团队

1、ZGC简介 1.1 介绍 ZGC 是一款低延迟的垃圾回收器,是 Java 垃圾收集技术的最前沿,理解了 ZGC,那么便可以说理解了 java 最前沿的垃圾收集技术。 从 JDK11 中作为试验特性推出以来,ZGC 一直在不停地发展中。 从 JDK14 开始,ZGC 开始支持 Windows。 在 JDK15 中,ZGC 不再是实验功能,可以正式投入生产使用了。 在最新的 JDK 开源库中,已经出现了分代收集的 ZGC 代码,预计不久的将来会正式发布,到时相信 ZGC 各项表现将会更加优秀。   图1 分代收集的ZGC 如上图,JDK21 中已经有了分代 ZGC 的 Feature。 1.2 ZGC 特征 1. 低延迟 2. 大容量堆 3. 染色指针 4. 读屏障 1.3 垃圾收集阶段   图2 ZGC 运作过程 如上图,主要有以下几个阶段,初始标记、并发标记/并发重映射、并发预备重分配、初始重分配、并发重分配,本次主要分析的就是”并发标记/并发重映射“部分源代码。 1.4 带着问题去读源码 1. ZGC 是如何在指针上标记的 2. ZGC 三色标记的过程 3. ZGC 只用了读屏障是如何防止漏标的 4. ZGC 标记过程中的指针自愈过程 5. ZGC 并发标记中的并发重映射过程 2、源码 2.1 入口 整个 ZGC 的源码入口是 ZDriver::gc 函数 void ZDriver::gc(const ZDriverRequest& request) { ZDriverGCScope scope(request); // Phase 1: Pause Mark Start pause_mark_start(); // Phase 2: Concurrent Mark concurrent(mark); // Phase 3: Pause Mark End while (!pause_mark_end()) { // Phase 3.5: Concurrent Mark Continue concurrent(mark_continue); } // Phase 4: Concurrent Mark Free concurrent(mark_free); // Phase 5: Concurrent Process Non-Strong References concurrent(process_non_strong_references); // Phase 6: Concurrent Reset Relocation Set concurrent(reset_relocation_set); // Phase 7: Pause Verify pause_verify(); // Phase 8: Concurrent Select Relocation Set concurrent(select_relocation_set); // Phase 9: Pause Relocate Start pause_relocate_start(); // Phase 10: Concurrent Relocate concurrent(relocate); } 其中,concurrent() 是一个宏定义 // Macro to execute a termination check after a concurrent phase. Note // that it's important that the termination check comes after the call // to the function f, since we can't abort between pause_relocate_start() // and concurrent_relocate(). We need to let concurrent_relocate() call // abort_page() on the remaining entries in the relocation set. #define concurrent(f) \ do { \ concurrent_##f(); \ if (should_terminate()) { \ return; \ } \ } while (false) 于是我们可以知道并发标记的函数是 concurrent_mark void ZDriver::concurrent_mark() { ZStatTimer timer(ZPhaseConcurrentMark); ZBreakpoint::at_after_marking_started(); //开始对整个堆进行标记 ZHeap::heap()->mark(true /* initial */); ZBreakpoint::at_before_marking_completed(); } 2.2 并发标记过程 2.2.1 并发标记 接下来看 ZHeap::heap()->mark void ZHeap::mark(bool initial) { _mark.mark(initial); } 再往下 void ZMark::mark(bool initial) { if (initial) { ZMarkRootsTask task(this); _workers->run(&task); } ZMarkTask task(this); _workers->run(&task); } 这里用了一个任务框架,执行任务逻辑在 ZMarkTask 里 class ZMarkTask : public ZTask { private: ZMark* const _mark; const uint64_t _timeout_in_micros; public: ZMarkTask(ZMark* mark, uint64_t timeout_in_micros = 0) : ZTask("ZMarkTask"), _mark(mark), _timeout_in_micros(timeout_in_micros) { _mark->prepare_work(); } ~ZMarkTask() { _mark->finish_work(); } virtual void work() { _mark->work(_timeout_in_micros); } }; 这里具体执行的函数是 work,往下看 _mark->work void ZMark::work(uint64_t timeout_in_micros) { ZMarkCache cache(_stripes.nstripes()); ZMarkStripe* const stripe = _stripes.stripe_for_worker(_nworkers, ZThread::worker_id()); ZMarkThreadLocalStacks* const stacks = ZThreadLocalData::stacks(Thread::current()); if (timeout_in_micros == 0) { work_without_timeout(&cache, stripe, stacks); } else { work_with_timeout(&cache, stripe, stacks, timeout_in_micros); } // Flush and publish stacks stacks->flush(&_allocator, &_stripes); // Free remaining stacks stacks->free(&_allocator); } 这里的 stripe 就是标记条带,stacks 是一个线程本地栈。 往下看 work_with_timeout 方法 void ZMark::work_with_timeout(ZMarkCache* cache, ZMarkStripe* stripe, ZMarkThreadLocalStacks* stacks, uint64_t timeout_in_micros) { ZStatTimer timer(ZSubPhaseMarkTryComplete); ZMarkTimeout timeout(timeout_in_micros); for (;;) { if (!drain(stripe, stacks, cache, &timeout)) { // Timed out break; } if (try_steal(stripe, stacks)) { // Stole work continue; } // Terminate break; } } 可以看到这里是一个循环,会一直从标记条带里取数据,直到取完了或时间到了。 这里就是 ZGC 三色标记的主循环了。 接下来看 drain 函数,drain 就是使排空的意思,这里就是从栈中取出指针进行标记,直到栈为空。 template <typename T> bool ZMark::drain(ZMarkStripe* stripe, ZMarkThreadLocalStacks* stacks, ZMarkCache* cache, T* timeout) { ZMarkStackEntry entry; // Drain stripe stacks while (stacks->pop(&_allocator, &_stripes, stripe, entry)) { //标记和跟随 mark_and_follow(cache, entry); // Check timeout if (timeout->has_expired()) { // Timeout return false; } } // Success return !timeout->has_expired(); } 可以看到这里一直在从栈里取数据,然后标记和递归标记,直到栈排空。 既然有从栈里取数据的地方,那肯定有往栈里放数据的地方,我们先把这个点记下来,叫“记录点1”。 接着往下看 mark_and_follow 函数 void ZMark::mark_and_follow(ZMarkCache* cache, ZMarkStackEntry entry) { // Decode flags const bool finalizable = entry.finalizable(); const bool partial_array = entry.partial_array(); if (partial_array) { follow_partial_array(entry, finalizable); return; } // Decode object address and additional flags const uintptr_t addr = entry.object_address(); const bool mark = entry.mark(); bool inc_live = entry.inc_live(); const bool follow = entry.follow(); ZPage* const page = _page_table->get(addr); assert(page->is_relocatable(), "Invalid page state"); // Mark //标记对象 if (mark && !page->mark_object(addr, finalizable, inc_live)) { // Already marked return; } // Increment live if (inc_live) { // Update live objects/bytes for page. We use the aligned object // size since that is the actual number of bytes used on the page // and alignment paddings can never be reclaimed. const size_t size = ZUtils::object_size(addr); const size_t aligned_size = align_up(size, page->object_alignment()); cache->inc_live(page, aligned_size); } // Follow if (follow) { if (is_array(addr)) { //递归处理数组 follow_array_object(objArrayOop(ZOop::from_address(addr)), finalizable); } else { //递归处理对象 follow_object(ZOop::from_address(addr), finalizable); } } } 这里有两个函数要看,我们先看 page->mark_object,再看 follow_object,至于follow_array_object,和 follow_object 类似,就不在详细介绍了。 page->mark_object 源码如下 inline bool ZPage::mark_object(uintptr_t addr, bool finalizable, bool& inc_live) { assert(ZAddress::is_marked(addr), "Invalid address"); assert(is_relocatable(), "Invalid page state"); assert(is_in(addr), "Invalid address"); // Set mark bit const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2; return _livemap.set(index, finalizable, inc_live); } 可以看到这里用了一个 map,来存储了所有对象地址的 finalizable 和 inc_live(新增存活)信息。 const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2; 从这个代码看,livemap 的大小大概是整个堆的对象对齐大小分之一,然后再乘以2。 这个 map 存的信息非常重要,ZGC中判断一个地址是否被标记过,一个对象是否存活都是通过这个 map 里的信息来判断的。 再回到上一步,继续看 follow_object 函数 void ZMark::follow_object(oop obj, bool finalizable) { if (finalizable) { ZMarkBarrierOopClosure<true /* finalizable */> cl; obj->oop_iterate(&cl); } else { ZMarkBarrierOopClosure<false /* finalizable */> cl; obj->oop_iterate(&cl); } } 这里根据 finalizable 分了两个分支,oop_iterate 看名字我们也能猜到这是做什么的,就是对对象里的指针进行迭代,但毕竟是猜测,具体还要看代码验证。 ZMarkBarrierOopClosure 这个看名字就是读屏障了,我们已经来到开头问题 3 的大门前了,只差临门一脚。 踹进读屏障的大门前,我们还得搬开挡在门口的一个大石头 oop_iterate 方法是怎么和 ZMarkBarrierOopClosure 联系上的。 2.2.2 对象迭代遍历 这一节将看 oop_iterate 内部逻辑,这个属于分支流程,暂不想了解的可以先跳过,直接看下一节 2.2.3 读屏障。 警告:抓紧扶手,下面车速开始加快了! 先把 cl 类型 ZMarkBarrierOopClosure 记录下来,我们记为 “记录点2”。 进入 oop_iterate 函数 template <typename OopClosureType> void oopDesc::oop_iterate(OopClosureType* cl) { OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass()); } 再往下 template <typename OopClosureType> void OopIteratorClosureDispatch::oop_oop_iterate(OopClosureType* cl, oop obj, Klass* klass, MemRegion mr) { OopOopIterateBoundedDispatch<OopClosureType>::function(klass)(cl, obj, klass, mr); } 再往下 static FunctionType function(Klass* klass) { return _table._function[klass->id()]; } function 方法在一个类中,_table 就是这个类的对象实例,_function 是个数组,我们看_function 怎么初始化值的。 public: FunctionType _function[KLASS_ID_COUNT]; Table(){ set_init_function<InstanceKlass>(); set_init_function<InstanceRefKlass>(); set_init_function<InstanceMirrorKlass>(); set_init_function<InstanceClassLoaderKlass>(); set_init_function<ObjArrayKlass>(); set_init_function<TypeArrayKlass>(); } table 构造方法与 _function 定义 这里有多个 Klass 对象,我们只看第一个。我们把 InstanceKlass 暂记为“记录点3”,接着往下看 set_init_function。 template <typename KlassType> void set_init_function() { _function[KlassType::ID] = &init<KlassType>; } 往下看 init 函数 template <typename KlassType> static void init(OopClosureType* cl, oop obj, Klass* k, MemRegion mr) { OopOopIterateBoundedDispatch<OopClosureType>::_table.set_resolve_function_and_execute<KlassType>(cl, obj, k, mr); } 继续 template <typename KlassType> void set_resolve_function_and_execute(OopClosureType* cl, oop obj, Klass* k, MemRegion mr) { set_resolve_function<KlassType>(); _function[KlassType::ID](cl, obj, k, mr); } 继续跟 set_resolve_function template <typename KlassType> void set_resolve_function() { if (UseCompressedOops) { _function[KlassType::ID] = &oop_oop_iterate_bounded<KlassType, narrowOop>; } else { _function[KlassType::ID] = &oop_oop_iterate_bounded<KlassType, oop>; } } 可以看到这里有个 UseCompressedOops,难道 ZGC 支持压缩指针了?感兴趣的同学可以深入研究下。 往下看 oop_oop_iterate_bounded template <typename KlassType, typename T> static void oop_oop_iterate_bounded(OopClosureType* cl, oop obj, Klass* k, MemRegion mr) { ((KlassType*)k)->KlassType::template oop_oop_iterate_bounded<T>(obj, cl, mr); } 这里可以看到调用了 KlassType 的 oop_oop_iterate_bounded 函数,KlassType 又是啥,这是一个泛型,其真实类型在这里其中之一就是记录点3的 InstanceKlass。 在 JVM 中一个普通的 Java 类是以一个 InstanceKlass 类型的 Klass 模型存在的。 以下就不属于 ZGC 的源码了,当然,仍然是 jvm 源码。 进入 InstanceKlass 的 oop_oop_iterate_bounded 函数 template <typename T, class OopClosureType> ALWAYSINLINE void InstanceKlass::oop_oop_iterate_bounded(oop obj, OopClosureType* closure, MemRegion mr) { if (Devirtualizer::do_metadata(closure)) { if (mr.contains(obj)) { Devirtualizer::do_klass(closure, this); } } oop_oop_iterate_oop_maps_bounded<T>(obj, closure, mr); } 往下 template <typename T, class OopClosureType> ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_maps_bounded(oop obj, OopClosureType* closure, MemRegion mr) { //非静态成员变量块起始位置 OopMapBlock* map = start_of_nonstatic_oop_maps(); //非静态成员变量块结束位置 OopMapBlock* const end_map = map + nonstatic_oop_map_count(); //对非静态成员变量块进行遍历 for (;map < end_map; ++map) { oop_oop_iterate_oop_map_bounded<T>(map, obj, closure, mr); } } 可以看到这里对一个对象的非静态成员变量块进行了迭代遍历。 继续往下 template <typename T, class OopClosureType> ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_map_bounded(OopMapBlock* map, oop obj, OopClosureType* closure, MemRegion mr) { T* p = (T*)obj->obj_field_addr<T>(map->offset()); T* end = p + map->count(); T* const l = (T*)mr.start(); T* const h = (T*)mr.end(); assert(mask_bits((intptr_t)l, sizeof(T)-1) == 0 && mask_bits((intptr_t)h, sizeof(T)-1) == 0, "bounded region must be properly aligned"); if (p < l) { p = l; } if (end > h) { end = h; } for (;p < end; ++p) { Devirtualizer::do_oop(closure, p); } } 这里继续对一个对象的非静态成员变量进行遍历。 继续看 Devirtualizer::do_oop template <typename OopClosureType, typename T> inline void Devirtualizer::do_oop(OopClosureType* closure, T* p) { call_do_oop<T>(&OopClosureType::do_oop, &OopClosure::do_oop, closure, p); } 继续 template <typename T, typename Receiver, typename Base, typename OopClosureType> static typename EnableIf<IsSame<Receiver, Base>::value, void>::type call_do_oop(void (Receiver::*)(T*), void (Base::*)(T*), OopClosureType* closure, T* p) { closure->do_oop(p); } 继续,但继续不动了,OopClosureType 是个模板类型,其真实类型是什么呢?一层层往上回溯,发现它就是记录点2的 ZMarkBarrierOopClosure 读屏障,这就又回到了 ZGC 主流程里了。 2.2.3 读屏障 进入 ZMarkBarrierOopClosure,由上一节可知,这个函数的入参 p 就是要标记对象的非静态成员变量的指针。 template <bool finalizable> class ZMarkBarrierOopClosure : public ClaimMetadataVisitingOopIterateClosure { public: ZMarkBarrierOopClosure() : ClaimMetadataVisitingOopIterateClosure(finalizable ? ClassLoaderData::_claim_finalizable : ClassLoaderData::_claim_strong, finalizable ? NULL : ZHeap::heap()->reference_discoverer()) {} virtual void do_oop(oop* p) { ZBarrier::mark_barrier_on_oop_field(p, finalizable); } virtual void do_oop(narrowOop* p) { ShouldNotReachHere(); } }; 继续看 mark_barrier_on_oop_field // // Mark barrier // inline void ZBarrier::mark_barrier_on_oop_field(volatile oop* p, bool finalizable) { const oop o = Atomic::load(p); if (finalizable) { barrier<is_marked_or_null_fast_path, mark_barrier_on_finalizable_oop_slow_path>(p, o); } else { const uintptr_t addr = ZOop::to_address(o); if (ZAddress::is_good(addr)) { // Mark through good oop mark_barrier_on_oop_slow_path(addr); } else { // Mark through bad oop barrier<is_good_or_null_fast_path, mark_barrier_on_oop_slow_path>(p, o); } } } 这里分了3个分支,finalizable先不看,根据指针是good还是bad有分了两个分支,这两个分支最终有都调用了 mark_barrier_on_oop_slow_path 慢路径处理方法。 这里直接看下面的第 2 个分支 template <ZBarrierFastPath fast_path, ZBarrierSlowPath slow_path> inline oop ZBarrier::barrier(volatile oop* p, oop o) { const uintptr_t addr = ZOop::to_address(o); // Fast path if (fast_path(addr)) { return ZOop::from_address(addr); } // Slow path const uintptr_t good_addr = slow_path(addr); if (p != NULL) { //指针自愈 self_heal<fast_path>(p, addr, good_addr); } return ZOop::from_address(good_addr); } 这是读屏障的核心逻辑代码,这里我们也看到了指针自愈。 从这段代码可以看到,首先执行快速路径检查,如果 fast_path(addr) 返回 true,则直接返回 addr 所对应的 oop 对象。 如果快速路径检查失败,则执行慢速路径检查,将 addr 传递给 slow_path 函数,并将返回值存储在 good_addr 变量中。 如果传入的指针 p 不为空,则调用 self_heal 这里的 slow_path 其实就是 mark_barrier_on_oop_slow_path 函数。 这里主要往下看两个函数,慢路径处理和指针自愈,慢路径处理流程比较长,我们先看指针自愈。 template <ZBarrierFastPath fast_path> inline void ZBarrier::self_heal(volatile oop* p, uintptr_t addr, uintptr_t heal_addr) { if (heal_addr == 0) { // Never heal with null since it interacts badly with reference processing. // A mutator clearing an oop would be similar to calling Reference.clear(), // which would make the reference non-discoverable or silently dropped // by the reference processor. return; } assert(!fast_path(addr), "Invalid self heal"); assert(fast_path(heal_addr), "Invalid self heal"); for (;;) { // Heal const uintptr_t prev_addr = Atomic::cmpxchg((volatile uintptr_t*)p, addr, heal_addr); if (prev_addr == addr) { // Success return; } if (fast_path(prev_addr)) { // Must not self heal return; } // The oop location was healed by another barrier, but still needs upgrading. // Re-apply healing to make sure the oop is not left with weaker (remapped or // finalizable) metadata bits than what this barrier tried to apply. assert(ZAddress::offset(prev_addr) == ZAddress::offset(heal_addr), "Invalid offset"); addr = prev_addr; } } 这个就比较简单了,类似 Java 的 cas 操作,把坏指针修改为好指针,然后执行快速路径。 好指针是上面执行慢路径返回的。 接下来看慢路径 // // Mark barrier // uintptr_t ZBarrier::mark_barrier_on_oop_slow_path(uintptr_t addr) { assert(during_mark(), "Invalid phase"); assert(ZThread::is_worker(), "Invalid thread"); // Mark return mark<GCThread, Follow, Strong, Overflow>(addr); } 这里对进入读屏障的指针又进行了标记。 到这里,其实读屏障主要逻辑已经结束了,下面是读屏障触发的标记逻辑。 2.2.4 读屏障触发的标记逻辑 继续 template <bool gc_thread, bool follow, bool finalizable, bool publish> uintptr_t ZBarrier::mark(uintptr_t addr) { uintptr_t good_addr; if (ZAddress::is_marked(addr)) { // Already marked, but try to mark though anyway good_addr = ZAddress::good(addr); } else if (ZAddress::is_remapped(addr)) { // Already remapped, but also needs to be marked good_addr = ZAddress::good(addr); } else { // Needs to be both remapped and marked good_addr = remap(addr); } // Mark if (should_mark_through<finalizable>(addr)) { ZHeap::heap()->mark_object<gc_thread, follow, finalizable, publish>(good_addr); } if (finalizable) { // Make the oop finalizable marked/good, instead of normal marked/good. // This is needed because an object might first becomes finalizable // marked by the GC, and then loaded by a mutator thread. In this case, // the mutator thread must be able to tell that the object needs to be // strongly marked. The finalizable bit in the oop exists to make sure // that a load of a finalizable marked oop will fall into the barrier // slow path so that we can mark the object as strongly reachable. return ZAddress::finalizable_good(good_addr); } return good_addr; } 这里要继续跟的函数有三个 ZAddress::good 、remap、ZHeap::heap()->mark_object。 ZHeap::heap()->mark_object 就是在堆中对对象进行标记。 先看下 ZAddress::good 的逻辑 inline uintptr_t ZAddress::offset(uintptr_t value) { return value & ZAddressOffsetMask; } inline uintptr_t ZAddress::good(uintptr_t value) { return offset(value) | ZAddressGoodMask; } 可以看到只是改变了下指针标记位,比较简单,也符合我们没读代码前对 ZGC 流程的认识。 2.2.5 重映射 继续看 remap 重映射方法,这里我们就找到了开头问题5【ZGC并发标记中的并发重映射过程】 uintptr_t ZBarrier::remap(uintptr_t addr) { assert(!ZAddress::is_good(addr), "Should not be good"); assert(!ZAddress::is_weak_good(addr), "Should not be weak good"); return ZHeap::heap()->remap_object(addr); } 继续 inline uintptr_t ZHeap::relocate_object(uintptr_t addr) { assert(ZGlobalPhase == ZPhaseRelocate, "Relocate not allowed"); ZForwarding* const forwarding = _forwarding_table.get(addr); if (forwarding == NULL) { // Not forwarding return ZAddress::good(addr); } // Relocate object return _relocate.relocate_object(forwarding, ZAddress::good(addr)); } 可以看到这里先查转发表,转发表中不存在直接返回好指针,存在则执行重分配操作,再往下就是重分配的逻辑了,本文中就不详解了,在以后重分配文章中的详细介绍。 2.2.6 回到读屏障标记流程 然后,返回 2.2.4 开头,继续看 ZHeap::heap()->mark_object template <bool gc_thread, bool follow, bool finalizable, bool publish> inline void ZHeap::mark_object(uintptr_t addr) { assert(ZGlobalPhase == ZPhaseMark, "Mark not allowed"); _mark.mark_object<gc_thread, follow, finalizable, publish>(addr); } 继续 template <bool gc_thread, bool follow, bool finalizable, bool publish> inline void ZMark::mark_object(uintptr_t addr) { assert(ZAddress::is_marked(addr), "Should be marked"); ZPage* const page = _page_table->get(addr); if (page->is_allocating()) { // Already implicitly marked return; } const bool mark_before_push = gc_thread; bool inc_live = false; if (mark_before_push) { // Try mark object if (!page->mark_object(addr, finalizable, inc_live)) { // Already marked return; } } else { // Don't push if already marked if (page->is_object_marked<finalizable>(addr)) { // Already marked return; } } // Push ZMarkThreadLocalStacks* const stacks = ZThreadLocalData::stacks(Thread::current()); ZMarkStripe* const stripe = _stripes.stripe_for_addr(addr); ZMarkStackEntry entry(addr, !mark_before_push, inc_live, follow, finalizable); stacks->push(&_allocator, &_stripes, stripe, entry, publish); } 这里有两个重要逻辑,一是已经标记的不再进行 push,二是未标记过的 push 到栈里。 还记得前面的“记录点1”吗,push 指针 到栈里的地方就是这儿了,被 push 的指针将会在下次标记循环中被取出来。 到此,ZGC 三色标记的主流程就头尾相接了,完结撒花。 3、问题回顾 3.1 开头的问题 1. ZGC是如何在指针上标记的 2. ZGC三色标记的过程 3. ZGC只用了读屏障是如何防止漏标的 4. ZGC标记过程中的指针自愈过程 5. ZGC并发标记中的并发重映射过程 上面5个问题,1、2、4、5上面源码分析过程中都已经有了,问题3是要从整个流程上看。 3.2 ZGC只用了读屏障是如何防止漏标的 3.2.1 什么情况下会漏标 当以下两个情况同时发生时: 1. 新增了一个从黑色对象到白色对象的新引用 2. 删除了全部从灰色对象到该白色对象的引用 3.2.2 如何避免? 破坏条件1,这就是增量更新(Incremental Update)解决方案。 破坏条件2,这就是原始快照(Snapshot At The Beginning, SAT B )解决方案。 ZGC 采用的是增量更新解决方案。但是,增量更新不是新增吗,应该是写屏障啊,读屏障怎么做到呢? 其实新增一个引用时,之前肯定要从某处读到这个引用,那么这个读的操作便触发了读屏障。前面源码中,我们知道 ZGC 读屏障里有一个标记的过程,而且还会自动返回好指针,也就是说经过读屏障返回的指针中不存在白色对象,这就从根本上避免了条件1。 4、扩展思考 ZGC 标记是在指针上,而不是在对象上,当回收某个 region 时,对于此 region 里的某个对象,无法获得其他对象指向此对象的指针,那如何得知这对象是否存活呢? 这个问题需要我们将标记阶段和重分配阶段的代码连起来看才能得到答案了。 5、结束 对于ZGC并发标记的过程的源码分析就到这里了,如果觉得对您有启发或有帮助就多多点赞支持呀~ 作者:京东物流刘家存 来源:京东云开发者社区 自猿其说Tech 转载请注明来源

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

用户登录
用户注册