JVM的自愈能力

在IT行业,碰到问题的第一个反应通常是——“你重启过没”——而这样做可能会适得其反,本文要讲述的就是这样的一个场景。

接下来要介绍的这个应用,它不仅不需要重启,而且毫不夸张地说,它能够自我治愈:刚开始运行的时候它可能会碰到些挫折,但会渐入佳境。为了能实际地展示出它的自愈能力,我们尽可能简单地重现了这一场景,这个灵感还得归功于[url=http://www.javaspecialists.eu/archive/Issue174.html]五年前heinz Kabutz发表的一篇老文章[/url]:




package eu.plumbr.test;

public class HealMe {
private static final int SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.6);

public static void main(String[] args) throws Exception {
for (int i = 0; i < 1000; i++) {
allocateMemory(i);
}
}

private static void allocateMemory(int i) {
try {
{
byte[] bytes = new byte[SIZE];
System.out.println(bytes.length);
}

byte[] moreBytes = new byte[SIZE];
System.out.println(moreBytes.length);

System.out.println("I allocated memory successfully " + i);

} catch (OutOfMemoryError e) {
System.out.println("I failed to allocate memory " + i);
}
}
}



上述代码会循环地分配两块内存。每次分配的内存都是堆中总内存的60%。由于在同一个方法内会不停地进行这个内存分配,因此你可能会认为这段代码会不断地抛出 java.lang.OutOfMemoryError: Java heap space异常,永远无法正常地执行完allocateMemory方法。

我们先来对源代码进行下静态分析,看看这种猜测是否恰当:

1. 乍看一下这段程序的话,这确实是无法成功执行的,因为要分配的内存已经超出了JVM的限制。
2. 但再仔细分析下的话我们会发现第一次分配是在一个块作用域内完成的,也就是说这个块中定义的变量仅对块内可见。这意味着这些内存在这个代码块执行完成后便可以回收掉了。这段代码一开始应该是可以成功执行的,只是当它再去尝试分配moreBytes的时候才会挂掉。
3. 如果再查看下编译后的class文件的话,你会看到如下的字节码:




private static void allocateMemory(int);
Code:
0: getstatic #3 // Field SIZE:I
3: newarray byte
5: astore_1
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: arraylength
11: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
14: getstatic #3 // Field SIZE:I
17: newarray byte
19: astore_1
20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_1
24: arraylength
25: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
[*]--- cut for brevity ----




从中能够看出,第一个数组是在位置3~5处完成分配的,并存储到了序号为1的本地变量中。随后在位置17处,正要分配另一个数组。不过由于第一个数组仍被本地变量所引用着,因此第二次分配总会抛出OOM的异常而失败。字节码解释器不会允许GC去回收第一个数组,因为它仍然存在着一个强引用。

从静态代码分析中可看出,由于底层的两个约束,上述的代码是无法成功执行的,而在第一种情况下则是能够运行的。这三点分析里面哪个才是正确的呢?我们来实际运行下看看结果吧。结果表明,这些结论都是正确的。首先,应用程序的确无法分配内存。但是,经过一段时间之后(在我的Mac OS X上使用Java 8大概是出现在第255次迭代中),内存分配开始能够成功执行了:





java -Xmx2g eu.plumbr.test.HealMe
1145359564
I failed to allocate memory 0
1145359564
I failed to allocate memory 1

… cut for brevity ...

I failed to allocate memory 254
1145359564
I failed to allocate memory 255
1145359564
1145359564
I allocated memory successfully 256
1145359564
1145359564
I allocated memory successfully 257
1145359564
1145359564
Self-healing code is a reality! Skynet is near...




为了搞清楚究竟发生了什么,我们得思考一下,在程序运行期间发生了什么变化?显然,Just-In-Time编译开始介入了。如果你还记得的话,JIT编译是JVM的一个内建机制,它可以优化热点代码。JIT会监控运行的代码,如果发现了一个热点,它会将你的字节码转化成本地代码,同时会执行一些额外的优化,譬如方法内联以及无用代码擦除。

我们打开下面的命令行参数重启下程序,看看是否触发了JIT编译。



[*]XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation


这会生成一个日志文件,在我这里是一个hotspot_pid38139.log文件,38139是Java进程的PID。在该文件中可以找到这么一行:



<task_queued compile_id='94' method='HealMe allocateMemory (I)V' bytes='83' count='256' iicount='256' level='3' stamp='112.305' comment='tiered' hot_count='256'/>



这说明,在运行了256次allocateMemory()方法2之后,C1编译器决定将这个方法进行3级编译。看下[url=http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/104743074675/src/share/vm/runtime/advancedThresholdPolicy.hpp]这里[/url]可以了解下分层编译的各个级别以及不同的阈值。在前面的256次迭代中这段程序都是在解释模式下运行的,这里的字节码解释器就是一个简单堆栈机器,它无法提前预知某个变量后续是否会被用到,在这里对应的是变量bytes。但是JIT会一次性查看整个方法,因此它能推断出后面不会再用到bytes变量,可以对它进行GC。所以才会触发垃圾回收,因此我们的程序才能奇迹般地自愈。我只是希望本文的读者都不要在生产环境碰到调试这类问题的情况。不过如果你想让某人抓狂的话,倒是可以试试在生产环境中加下类似的代码。


原创文章转载请注明出处:[url=http://it.deepinmind.com/jvm/2014/12/15/self-healing-jvm.html]http://it.deepinmind.com[/url]

[url=https://plumbr.eu/blog/self-healing-jvm?utm_source=feedly&utm_reader=feedly&utm_medium=rss&utm_campaign=rss]英文原文链接[/url]
采用PyQt5框架与Python编程语言构建图书信息管理平台 本项目基于Python编程环境,结合PyQt5图形界面开发库,设计实现了一套完整的图书信息管理解决方案。该系统主要面向图书馆、书店等机构的日常运营需求,通过模块化设计实现了图书信息的标准化管理流程。 系统架构采用典型的三层设计模式,包含数据存储层、业务逻辑层和用户界面层。数据持久化方案支持SQLite轻量级数据库与MySQL企业级数据库的双重配置选项,通过统一的数据库操作接口实现数据存取隔离。在数据建模方面,设计了包含图书基本信息、读者档案、借阅记录等核心数据实体,各实体间通过主外键约束建立关联关系。 核心功能模块包含六大子系统: 1. 图书编目管理:支持国际标准书号、中国图书馆分类法等专业元数据的规范化著录,提供批量导入与单条录入两种数据采集方式 2. 库存动态监控:实时追踪在架数量、借出状态、预约队列等流通指标,设置库存预警阈值自动提醒补货 3. 读者服务管理:建立完整的读者信用评价体系,记录借阅历史与违规行为,实施差异化借阅权限管理 4. 流通业务处理:涵盖借书登记、归还处理、续借申请、逾期计算等标准业务流程,支持射频识别技术设备集成 5. 统计报表生成:按日/月/年周期自动生成流通统计、热门图书排行、读者活跃度等多维度分析图表 6. 系统维护配置:提供用户权限分级管理、数据备份恢复、操作日志审计等管理功能 在技术实现层面,界面设计遵循Material Design设计规范,采用QSS样式表实现视觉定制化。通过信号槽机制实现前后端数据双向绑定,运用多线程处理技术保障界面响应流畅度。数据验证机制包含前端格式校验与后端业务规则双重保障,关键操作均设有二次确认流程。 该系统适用于中小型图书管理场景,通过可扩展的插件架构支持功能模块的灵活组合。开发过程中特别注重代码的可维护性,采用面向对象编程范式实现高内聚低耦合的组件设计,为后续功能迭代奠定技术基础。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
在Java系统中实现自愈能力,通常涉及故障检测、自动恢复、容错机制以及资源管理等多个方面。通过合理的设计与框架支持,可以显著提升系统的稳定性和可靠性。 ### 故障检测与恢复机制 1. **心跳机制**:利用心跳检测服务的可用性。例如,使用Netty或Apache MINA等网络通信框架,定期发送心跳包以确认远程服务是否存活。如果未收到响应,则触发相应的恢复逻辑。 2. **健康检查**:通过Spring Boot Actuator等工具,提供 `/health` 端点来监控应用的健康状态。该端点可集成数据库连接池、外部API调用等依赖项的状态检测功能[^3]。 3. **日志分析**:结合ELK(Elasticsearch, Logstash, Kibana)栈进行实时日志分析,识别异常模式并触发告警或自动修复流程。 ### 自动恢复策略 1. **重试机制**:在调用失败时自动重试,通常与指数退避算法结合使用,以避免短时间内频繁请求导致雪崩效应。Hystrix库提供了强大的重试和断路器功能。 2. **断路器模式**:采用Resilience4j或Hystrix实现断路器,当某个服务调用失败率达到阈值时,暂时切断对该服务的调用,防止级联故障,并在一段时间后尝试恢复。 3. **服务降级**:当系统负载过高或某些组件不可用时,返回缓存数据或默认值,保证核心业务流程继续运行。 ### Java生态中的相关框架 - **Spring Retry**:为方法调用添加重试行为,适用于短暂的网络问题或其他瞬态故障。 - **Hystrix**:Netflix开源的延迟和容错库,提供断路器、线程隔离等功能,但已进入维护模式。 - **Resilience4j**:轻量级的容错库,支持Java 8函数式编程风格,包括断路器、重试、限流等模块。 - **Apache ZooKeeper**:用于协调分布式系统中的节点,提供一致性保证和会话管理,有助于构建高可用的服务发现与配置同步机制。 - **Curator Framework**:基于ZooKeeper的高级封装,简化了分布式锁、领导者选举等常见场景的开发工作[^3]。 - **Dropwizard Metrics**:收集运行时指标如吞吐量、响应时间等,帮助及时发现性能瓶颈。 ### 示例代码:使用Resilience4j实现断路器 ```java import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import java.time.Duration; public class CircuitBreakerExample { public static void main(String[] args) { // 配置断路器参数 CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) // 失败率阈值为50% .waitDurationInOpenState(Duration.ofSeconds(10)) // 打开状态下等待时间 .ringBufferSizeInClosedState(10) // 关闭状态下的滑动窗口大小 .build(); // 创建断路器实例 CircuitBreaker circuitBreaker = CircuitBreaker.of("backendService", config); // 包裹需要保护的操作 String result = circuitBreaker.executeSupplier(() -> callBackendService()); System.out.println(result); } private static String callBackendService() { // 模拟一个可能失败的服务调用 if (Math.random() < 0.7) { // 70%的概率抛出异常 throw new RuntimeException("Service is down"); } return "Success"; } } ``` ### 资源管理与弹性伸缩 - **动态调整线程池**:根据当前负载情况动态调整线程池大小,确保资源的有效利用。 - **JVM内存管理**:优化垃圾回收策略,减少Full GC频率;同时设置合理的堆内存上限,防止OOM错误。 - **容器化部署**:借助Docker和Kubernetes实现快速扩缩容,配合健康检查自动替换不健康的Pod。 ### 分布式环境下的自愈实践 - **服务注册与发现**:利用Consul、Eureka或Nacos进行服务注册,确保客户端能够获取最新的服务实例列表。 - **分布式事务处理**:采用Saga模式或两阶段提交协议,确保跨服务操作的一致性。 - **数据一致性保障**:通过Raft或Paxos算法维护多个副本间的数据一致性,提高系统的鲁棒性。 综上所述,在Java系统中实现自愈能力需要综合考虑多种技术和工具,从不同层面增强系统的健壮性和自我修复能力。选择合适的框架和技术栈对于构建具备自愈特性的高质量软件至关重要。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值