Причина и решение утечки памяти JAVA

Java EE
Причина и решение утечки памяти JAVA

1 Обзор

Важной особенностью языка java является автоматический сбор и переработка сборщика мусора, что не требует от нас ручного управления и освобождения памяти, что также затрудняет обнаружение и устранение утечек памяти java.

Если ваша программа выдает исключение в потоке "main" java.lang.OutOfMemoryError: пространство кучи Java, то это обычно вызвано утечкой памяти.

2. Что такое утечка памяти

В общем, принцип освобождения объекта заключается в том, что он больше никогда не будет использоваться. Присвоение объекту значения null или других объектов сделает пространство, на которое первоначально указывал объект, недоступным и больше не будет использоваться. рециркулировать. Утечка памяти означает, что, хотя память этой части объекта больше не используется, она не будет восстановлена ​​JVM.

  • Как правило, утечка памяти может произойти, если долгоживущие объекты содержат краткосрочные ссылки.

3. Утечка памяти из-за слишком большого объема

3.1 Описание проблемы

public class Simple {
    private Object object;
    public void method() {
        object = new Object();
        // ...
    }
}

В приведенном выше коде мы присваиваем инстанцируемое значение переменной объекта класса в методе метода, но если мы используем только объект в этом методе, это будет означать, что на всем жизненном цикле класса объект занимает хотя пространство объекта больше не будет использоваться, его нельзя переработать, что вызывает утечку памяти.Если объект является контейнером с добавлением множества элементов, проблема будет более очевидной.

3.2. Улучшения

Улучшение приведенного выше кода утечки памяти относительно простое.

public class Simple {
    private Object object;
    public void method() {
        object = new Object();
        // 使用到 object 的业务代码
        object = null;
    }
}

Принцип решения проблемы утечки памяти заключается в освобождении соответствующей ссылки сразу после того, как объект больше не используется, поэтому после выполнения бизнес-кода, когда объектный объект больше не используется, присвойте ему значение null и освободите его ссылку чтобы позволить JVM повторно использовать соответствующий объект памяти.

Ниже приведен исходный код jdk8 LinkedList.

//删除指定节点并返回被删除的元素值
E unlink(Node<E> x) {
    //获取当前值和前后节点
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    if (prev == null) {
        //如果前一个节点为空(如当前节点为首节点),后一个节点成为新的首节点
        first = next;
    } else {
        //如果前一个节点不为空,那么他先后指向当前的下一个节点
        prev.next = next;
        x.prev = null;
    }
    if (next == null) {
        //如果后一个节点为空(如当前节点为尾节点),当前节点前一个成为新的尾节点
        last = prev;
    } else {
        //如果后一个节点不为空,后一个节点向前指向当前的前一个节点
        next.prev = prev;
        x.next = null;
    }
    x.item = null;
    size--;
    modCount++;
    return element;
}

Можно видеть, что после использования членов next, item и prev для x явно присваивается null, так что они не могут быть повторно использованы jvm, что легко игнорируется в реальной разработке.

4. Злой путь памяти, вызванный элементами-контейнерами

4.1 Описание проблемы

Ниже показан метод pop, который мы реализуем с помощью ArrayList.

public E pop(){
    if(size == 0)
        return null;
    else
        return (E) elementData[--size];
}

Это очень просто реализовать, но возникает проблема с утечкой памяти, потому что размер становится меньше и исходные конечные элементы в ArrayList никогда не будут использоваться, но поскольку контейнер содержит их ссылки, они никогда не будут освобождены.

4.2 Улучшения

public E pop(){
    if(size == 0)
        return null;
    else{
        E e = (E) elementData[--size];
        elementData[size] = null;
        return e;
    }
}

Активно назначая null для освобождения ссылки на соответствующий элемент, чтобы можно было освободить соответствующее пространство.

5. Утечка памяти из-за самого контейнера

5.1 Описание проблемы

Vector vec = new Vector();
for (int i = 1; i < 100; i++)
{
    Object obj = new Object();
    vec.add(obj);
    // 使用 obj 的相关业务逻辑
    obj = null;
}
// 使用 vec 的相关业务逻辑

Приведенный выше код является очень классическим примером. На первый взгляд, проблем нет. После использования каждого элемента ссылка на элемент устанавливается в нулевое значение, чтобы гарантировать повторное использование пространства объекта. Однако на самом деле при постоянном расширении самого контейнера он также занимает очень большой объем памяти, что часто игнорируется.Если самому контейнеру не присвоить значение null, то сам контейнер всегда будет выживать в области видимости .

5.2 Улучшения

Vector vec = new Vector();
for (int i = 1; i < 100; i++)
{
    Object obj = new Object();
    vec.add(obj);
    // 使用 obj 的相关业务逻辑
    obj = null;
}
// 使用 vec 的相关业务逻辑
vec = null;

Способ улучшения тоже очень простой, всегда правильнее всего присваивать null сразу, когда контейнер больше не используется.

6. Утечка памяти, вызванная методом equals по умолчанию для контейнеров Set и Map.

6.1 Описание проблемы

public class TestClass implements Cloneable {
    private Long value;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class MainClass {
    public Set<TestClass> method(List<TestClass> testList)
        throws CloneNotSupportedException {
        Set<TestClass> result = new HashSet<>();
        for (int a = 0; a < 100000) {
            for (TestClass test : testList) {
                result.add(test.clone());
            }
        }
    }
}

Кажется, что приведенный выше код реализует логику кода дедупликации входящего testList, хотя он повторяется много раз, наш код дедупликации не приведет к дополнительной трате места. Но на самом деле операции clone и new перераспределяют место в памяти, а это значит, что их адреса разные, а поскольку все классы наследуют Object, их методы equals являются производными от класса Object.Реализация по умолчанию — возвращать объект адрес. Поэтому, несмотря на то, что объекты, полученные клонированием, дедуплицируются в наборе, набор по-прежнему считает их разными объектами, и, таким образом, повторные добавления в конечном итоге вызовут OutOfMemoryError.

6.2 Улучшения

Способ улучшения прост: для пользовательских классов добавьте реализацию соответствующего метода equals, который вам нужен.

public class TestClass implements Cloneable {
    private Long value;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object obj) {
        return Objects.equals(obj.value, value);
    }
}

public class MainClass {
    public Set<TestClass> method(List<TestClass> testList)
        throws CloneNotSupportedException {
        Set<TestClass> result = new HashSet<>();
        for (int a = 0; a < 100000) {
            for (TestClass test : testList) {
                result.add(test.clone());
            }
        }
    }
}

Примечание: сначала профилактика, потом лечение