百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

90%的Java程序员都忽视的内部类使用不当导致内存泄露!

ztj100 2025-08-02 22:49 3 浏览 0 评论

你知道的越多,不知道的就越多,业余的像一棵小草!

你关注,我们一起精进!你星标,我们便有了更多故事!

业余时间 Java 种草!

编辑:业余草

推荐: https://t.zsxq.com/ R0nBd

为什么spring.factories也下岗了?为什么SpringBoot3.x要废弃它?

从 Java 中的 CAS 看 Kimi K2 大模型与 MuonClip 技术。

湖北十堰非常值得一去的旅游景点!

写了三年中台代码,结果公司说不做了...

写个 Maven 插件装装样子:在 build 阶段干点自己的事

大概是 3 年前,我在微信读书上拜读周志明老师的《深入理解 Java 虚拟机》第三版,当时做了不少留言。今天,有微信群友在书里遇到了我,在群里咨询问题,我也是凭着印象与记忆,献丑了一番。这会空闲下来,我整理成文分享给更多的网友。一起了解一些 Java 内部类:作用、陷阱与内存泄露( Java 内部类:爱你一万年,哪怕内存溢出 )等经验。

Java 内部类的作用

我们先从 Java 的内部类的作用说起。

在 Java 中,内部类(Inner Class)是一种定义在另一个类内部的类。内部类提供了许多有用的功能,包括但不限于以下几点:

  1. 访问外部类的私有成员 :内部类可以无条件地访问外部类的所有元素,包括私有成员变量和方法,这使得内部类非常适合用于辅助类的设计。
  2. 实现隐藏 :内部类可以对外部隐藏,这样可以更好地封装代码逻辑,减少对外暴露的类数量,提高代码的安全性和可维护性。
  3. 多重继承的模拟 :虽然 Java 不支持多继承,但内部类可以通过继承不同的外部类来模拟多重继承的效果。
  4. 优化接口实现 :通过匿名内部类,可以简化接口或抽象类的实现,特别是在事件监听等场景中非常常见。

Java 内部类的分类

通过上面内部类的作用中可知,Java 中的内部类通常可以按照下面的种类进行划分。

成员内部类(Member Inner Class)

直接定义在另一个类的内部,并且不加 static 修饰。

其特点是:

静态内部类(Static Nested Class)

与成员内部类相似,定义在另一个类的内部,但用 static 修饰。

其特点是:

局部内部类(Local Inner Class)

定义在方法、构造器或代码块内部的类。这个局部内部类用的场景更少。

其特点是:

匿名内部类 Anonymous Inner Class

定义:没有显式类名,直接通过 new 接口/父类的方式定义。

特点:

回顾或了解了以上内容后,接下来我们进入正题,看看内部类在使用过程中的一些陷阱。

使用内部类的陷阱

尽管内部类有很多优势,但在使用过程中也存在一些潜在的陷阱,尤其是与内存管理和生命周期控制相关的陷阱。

内存泄露(Memory Leak)

非静态内部类(如成员内部类和匿名内部类)会隐式持有外部类的引用 。如果内部类的生命周期长于外部类的生命周期,或者内部类被长时间持有(例如被缓存、线程池引用等),则可能导致外部类无法被垃圾回收器回收,从而引发内存泄露。

难以理解的封装结构

过度使用内部类可能会导致代码结构变得复杂,尤其是嵌套层次过多时,会增加代码的可读性和维护难度。

写到这里,我想起了我刚入职场时,面试官问到我的一个问题。 内部类,可以嵌套多少层?

这个问题真不高明,反过来问面试官,面试官也回答不出来。

class Outer {
class Inner1 {
class Inner2 {
class Inner3 {
// 可以继续嵌套...
}
}
}
}

实际上,也很少有人去考究。我觉得可以从下面 4 个方面来回答这个问题。

  1. Java 语法上允许无限嵌套:Java 的语法并没有限制内部类的嵌套层数。
  2. 编译器限制:某些 Java 编译器(如 javac)可能对嵌套层数有隐式限制(例如 65535 层,受 .class 文件结构的限制),但这种限制在现实中几乎不会遇到。
  3. 可读性与维护性:过度嵌套会导致代码难以理解和维护,通常不建议超过 2~3 层。
  4. JVM 限制:方法调用栈的深度(如递归调用时)可能间接影响嵌套内部类的实例化,但这是运行时问题,而非语法问题。

基本上,说出你的思路即可。下面我们说说内部类对内存回收的影响

内部类对内存回收的影响

由于非静态内部类会持有外部类的引用,如果内部类实例被长期持有,外部类实例将无法被释放,即使它已经不再需要。这种现象在使用匿名内部类时尤为常见,尤其是在 Android 开发中,Handler、Runnable 等对象常常引发内存泄露问题。

下面是一个简单的内存泄露案例,代码展示了匿名内部类如何导致外部类无法被回收。

public class OuterClass {
private Object heavyResource;

public OuterClass() {
this.heavyResource = new Object(); // 假设这是一个占用大量内存的资源
}

public void startBackgroundTask() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000); // 模拟长时间运行的任务
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用外部类的资源
System.out.println("Resource used: " + heavyResource.hashCode());
}
}).start();
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
OuterClass outer = new OuterClass();
outer.startBackgroundTask();
// outer 对象本应在此处被回收,但由于匿名内部类持有其引用,可能导致其无法被回收
}
}
}

上面这个例子中, Runnable OuterClass 的一个匿名内部类,它隐式持有 OuterClass 实例的引用。当我们在 main 方法中循环创建 OuterClass 实例并启动后台任务时,这些实例可能不会被及时回收,从而导致内存占用不断上升。

为了更快速的出现内存泄露,我建议大家可以 new 一个大的 byte 数组,加快异常现象的显现。

如何避免内部类引发的内存泄露

这里,我总结了 3 条经验,大家可以在评论区补充留言评论。

  1. 使用静态内部类 :静态内部类不会持有外部类的引用,因此可以避免因内部类生命周期过长而导致的内存泄露问题。
  2. 手动解除引用 :在不再需要内部类实例时,显式地将其置为 null ,以帮助垃圾回收器回收资源。
  3. 使用弱引用(WeakReference) :在某些场景下,可以使用 WeakReference 来持有外部类的引用,这样即使内部类仍然存在,外部类也可以被回收。

总结

内部类是 Java 中一个强大但很容易被滥用的功能。合理使用内部类可以提高代码的封装性和可读性,但如果忽视其潜在的陷阱,尤其是内存泄露问题,可能会导致严重的性能问题甚至程序崩溃。作为 Java 开发者,我们应当充分理解内部类的工作机制,并在实际开发中谨慎使用,确保程序的健壮性和高效性。

以上,我们明天见!

相关推荐

Java的SPI机制详解

作者:京东物流杨苇苇1.SPI简介SPI(ServiceProvicerInterface)是Java语言提供的一种接口发现机制,用来实现接口和接口实现的解耦。简单来说,就是系统只需要定义接口规...

90%的Java程序员都忽视的内部类使用不当导致内存泄露!

...

一文读懂 Spring Boot 启动原理,开发效率飙升!

在当今的Java开发领域,SpringBoot无疑是最热门的框架之一。它以其“约定大于配置”的理念,让开发者能够快速搭建和启动应用,极大地提高了开发效率。但是,你是否真正了解Spring...

ServiceLoader

ServiceLoader是Java提供的一种服务发现机制(ServiceProviderInterface,SPI)...

深入探索 Spring Boot3 中的自定义扩展操作

在当今互联网软件开发领域,SpringBoot无疑是最受欢迎的框架之一。随着其版本迭代至SpringBoot3,它为开发者们带来了更多强大的功能和特性,其中自定义扩展操作更是为我们在项目开发中...

Spring Boot启动过程全面解析:从入门到精通

一、SpringBoot概述SpringBoot是一个基于Spring框架的快速开发脚手架,它通过"约定优于配置"的原则简化了Spring应用的初始搭建和开发过程。...

Spring Boot 3.x 自定义 Starter 详解

今天星期六,继续卷springboot3.x。在SpringBoot3.x中,自定义Starter是封装和共享通用功能、实现“约定优于配置”理念的强大机制。通过创建自己的Starte...

Spring Boot 的 3 种动态 Bean 注入技巧

在SpringBoot开发中,动态注入Bean是一种强大的技术,它允许我们根据特定条件或运行时环境灵活地创建和管理Bean。相比于传统的静态Bean定义,动态注入提供了更高的灵活性和可...

大佬用4000字带你彻底理解SpringBoot的运行原理!

SpringBoot的运行原理从前面创建的SpringBoot应用示例中可以看到,启动一个SpringBoot工程都是从SpringApplication.run()方法开始的。这个方法具体完成...

Springboot是如何实现自动配置的

SpringBoot的自动配置功能极大地简化了基于Spring的应用程序的配置过程。它能够根据类路径中的依赖和配置文件中的属性,自动配置应用程序。下面是SpringBoot实现自动配置的...

Spring Boot3.x 应用的生命周期深度解析

SpringBoot应用的生命周期可以清晰地划分为三个主要阶段:启动阶段(Startup)...

Springboot 启动流程及各类事件生命周期那点事

前言本文通过Springboot启动方法分析SpringApplication逻辑。从静态run方法执行到各个阶段发布不同事件完成整个应用启动。...

Spring框架基础知识-常用的接口1

BeanDefinition基本概念BeanDefinition是Spring框架中描述bean配置信息的核心接口,它包含了创建bean实例所需的所有元数据。...

一家拥有 158 年历史的公司遭遇索赔,被迫关闭!

...

Java 技术岗面试全景备战!从基础到架构的系统性通关攻略分享

Java技术岗的面试往往是一项多维度的能力检验。本文将会从核心知识点、项目经验到面试策略,为你梳理一份系统性的备战攻略!...

取消回复欢迎 发表评论: