赞
踩
虽然 Java 的优势在于其平台独立性、强大的库和广泛的生态系统,但解决性能问题是充分利用其功能的关键。
1.内存泄漏
有人会问,既然 Java 通过垃圾回收器实现了自动内存管理,这怎么可能呢?的确,Java 的垃圾回收器是一个功能强大的工具,可自动处理内存分配和删除,减轻了我们手动分配和删除内存的负担。 但是,仅仅依靠自动内存管理并不能保证不面临性能挑战。
Java 中的垃圾回收器(GC)可以自动识别和回收不再使用的内存,这是该语言强大内存管理系统的一个重要方面。
然而,即使有了这些先进的机制,即使是最熟练的程序员也仍有可能遇到并无意中引入 Java 内存泄漏。当对象无意中保留在内存中,导致垃圾回收器无法回收相关内存时,就会发生内存泄漏。随着时间的推移,这会导致内存消耗增加并降低应用程序的性能。
内存泄漏的检测和解决非常棘手,这主要是因为它们的症状相互重叠。在我们的案例中,这是最明显的症状:堆出错(OutOfMemoryError heap error),随之而来的是一段时间内的性能下降。
导致 Java 内存泄漏的问题有很多。我们的第一种方法是通过分析内存不足错误消息来确定这是正常的内存耗尽(由于设计不当)还是泄漏。
我们首先检查了最可能的罪魁祸首,静态字段、集合和声明为静态的大型对象,它们可能会在应用程序的整个生命周期中阻塞重要内存。
例如,在下面的代码示例中,在初始化列表时移除静态关键字就能大大减少内存使用量。
public class StaticFieldsMemoryTestExample {
public static List
list =
new ArrayList<>();
public
void addToList() {
for (
int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
Log.info("Debug Point 2");
}
public
static
void main(String[] args) {
new StaticFieldsDemo().addToList();
}
}
我们采取的其他措施包括检查可能会阻塞内存的开放资源或连接,从而将它们阻挡在垃圾回收器的范围之外。通过为 equals() 和 hashCode() 方法编写适当的重载方法,特别是在 HashMaps 和 HashSets 中,不当实现 equals() 和 hashCode() 方法。下面是一个正确实现 equals() 和 hashCode() 方法的示例。
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return person.name.equals(name);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
return result;
}
}
您可能还需要注意内类对象是否隐式地持有对外类对象的引用,从而使内类对象成为垃圾回收的无效候选对象。
防止内存泄漏的技巧
如果您的代码使用外部资源,如文件句柄、数据库连接或网络套接字,请确保在不再需要时明确释放它们。
使用内存剖析工具(如 VisualVM 或 YourKit)来分析和识别应用程序中潜在的内存泄漏。
使用单例时,使用懒加载而不是急迫加载,以避免在实际需要单例之前进行不必要的资源分配。
如果您的代码使用文件句柄、数据库连接或网络套接字等外部资源,请确保在不再需要这些资源时明确释放它们。
2.线程死锁
Java 是一种多线程语言。这是使 Java 成为一种合适的编程语言的特性之一,尤其适用于开发并发处理多个任务的企业应用程序。
顾名思义,多线程涉及多个线程,每个线程都是最小的执行单元。线程是独立的,有单独的执行路径,因此其中一个线程出现异常不会影响其他线程。
但是,如果线程试图同时访问相同的资源(锁),会发生什么情况呢?这时就会出现死锁。我在合作开发实时金融数据处理系统时就有过这样的经历。在这个项目中,我们有多个线程负责从外部 API 获取数据、执行复杂的计算以及更新共享的内存数据库。
随着该工具使用量的增加,我们开始偶尔接到一些关于偶尔冻结的报告。线程转储显示,某些线程陷入了等待状态,形成了对锁的循环依赖。
在这个例子中,我们有两个线程(线程 1 和线程 2)试图以不同的顺序获取两个锁(锁 1 和锁 2)。这就引入了循环等待,增加了死锁的可能性。
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1"); // Introducing a delay to increase the likelihood of deadlock try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2"); // Introducing a delay to increase the likelihood of deadlock try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1");
}
}
});
thread1.start();
thread2.start();
}
}
为了解决这个问题,我们可以重构代码,确保线程始终以一致的顺序获取锁。为此,我们可以引入全局锁顺序,并确保所有线程都遵循相同的顺序。
public class DeadlockSolution {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1"); // Introducing a delay to increase the likelihood of deadlock try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 1"); // Introducing a delay to increase the likelihood of deadlock try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2");
}
}
});
thread1.start();
thread2.start();
}
}
防止线程死锁的技巧
3.过度的垃圾回收
Java 中的垃圾回收就像是为我们管理内存的幕后英雄。它会自动清理不再需要的对象,让我们的开发生活变得轻松许多。 虽然这种自动垃圾回收为开发人员提供了方便,但它可能会以占用垃圾回收的 CPU 周期为代价,从而影响应用程序的性能。
除了典型的内存不足错误外,您还可能偶尔遇到应用程序冻结、延迟或应用程序崩溃。如果您使用的是云计算,优化垃圾回收过程可以为您节省大量计算成本。一个案例是,一家名为 Uber 的公司通过使用高效、低风险、大规模、半自动化的 Go 垃圾回收调整机制,为 30 个关键任务服务节省了 7 万个核。
防止过度垃圾回收的技巧
4.臃肿的库和依赖关系
Maven 和 Gradle 等构建工具彻底改变了我们管理 Java 项目依赖关系的方式。这些工具提供了包含外部库的精简方式,并简化了项目配置过程。然而,在带来便利的同时,臃肿的库和依赖关系也带来了风险。
由于错误修复、新功能和新依赖关系的出现,软件项目有快速增长的趋势。有时,项目的发展会超出我们作为开发人员的能力范围,无法对其进行有效维护。 它们还会带来安全漏洞和额外的性能开销。
如果您遇到这种情况,我建议您研究如何将应用程序中未使用的依赖项和库去掉,以此作为补救措施之一。
在 Java 生态系统中,我发现有几种工具可用于管理依赖关系。其中最常见的包括 Maven 依赖性插件和 Gradle 依赖性分析插件,它们在检测未使用的依赖性、已使用的传递依赖性(您可能希望直接声明)以及在错误配置(API vs 实现 vs 仅编译等)上声明的依赖性方面表现出色。
您还可以利用 Sonarqube 和 JArchitect 等其他工具。一些现代集成开发环境(如 Intellij)也有不错的依赖关系分析功能。
防止 Java 依赖关系臃肿的技巧
5.低效代码
没有哪个开发人员会故意编写低效或次优代码。然而,尽管用心良苦,但由于各种原因,低效代码还是会出现在生产中。这可能是由于项目期限紧迫、对底层技术的理解有限,或者不断变化的需求迫使开发人员优先考虑功能而不是优化。
低效:
String result = "";
for (int i = 0; i < 1000; i++) {
result += "example";
}
提高了的代码:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("example");
}
String result = sb.toString();
低效代码会导致内存消耗增加、响应时间延长以及系统整体效率降低。最终,低效代码会降低用户体验,增加运营成本,并限制应用程序的扩展以应对负载的增加。
摆脱低效代码首先要识别表明低效的模式。我经常注意的一些模式包括:没有适当退出条件的嵌套循环、不必要的对象创建和实例化、过度同步、低效数据库查询等。
6.并发问题
当多个线程同时访问共享资源时,就会出现并发问题,往往会导致意想不到的行为。
如果你已经在编码游戏中摸爬滚打了一段时间,那么你很可能会在整个开发周期中遇到新出现问题的困扰。发现并有效解决这些问题确实是一项挑战。
老实说,如果不能清楚地了解实际性能,这些问题往往会挥之不去,困扰着您的应用程序。在处理复杂的分布式系统时,这种困难甚至更为现实。如果没有适当的洞察力,就很难做出明智的设计决策或评估代码更改的影响。
帮助防止 Java 并发问题的技巧
总结
希望这次讨论能给您带来启发。切记不要成为过早优化的牺牲品。同样重要的是要认识到,并非所有代码部分都对应用程序的性能有同样的影响。关键在于识别并优先处理对性能有重大影响的关键区域。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。