Теория и практика Java: Хорошие практические приемы по ведению домашнего хозяйства

Ваши ресурсы злоупотребляют вашим гостеприимством?

Сборка мусора - любимая функция платформы Java™ почти у каждого; она упрощает разработку и устраняет все категории возможных ошибок в коде. Но в то время как сборка мусора, как правило, позволяет вам пренебрегать управлением ресурсами, иногда приходится вести хозяйство самостоятельно. В выпуске Теории и практики Java за этот месяц Брайан Гетц обсуждает ограничения сборки мусора и определяет ситуации, когда вам придется выполнять уборку самостоятельно.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



08.02.2007

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

При наличии достаточного пространства мотивация устранять беспорядок уменьшается. Чем больше у вас пространства, тем меньше у вас мотивация постоянно содержать его в порядке. Знаменитая баллада Арло Гутри (Arlo Guthrie) Ресторан Алисы иллюстрирует эту идею:

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

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

Явно высвобождающиеся ресурсы

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

С другой стороны, ресурсы, не требующие обращения к памяти, как дескрипторы файла (file handles) и дескрипторы сокета (socket handles) должны быть явно высвобождены программой, использующей методы с такими именами, как close(), destroy(), shutdown() или release(). Некоторые классы, такие как реализации потоков дескрипторов файлов в библиотеке классов платформы предусматривают финализаторы в качестве "страховки" так что если программа забудет высвободить ресурс, финализатор все-таки сможет выполнить свою работу, когда сборщик мусора определит, что программа закончила работу с ним. Но даже если дескрипторы файла предусматривают финализаторы, чтобы убирать за вами в случае, если вы сами забудете сделать это, все-таки лучше закрывать их явно, когда вы закончили работу с ними. Это закроет их гораздо быстрее, чем если бы они закрывались иным способом, уменьшая вероятность полной выработки ресурсов.

Для некоторых ресурсов ожидание, пока финализация высвободит их, невозможно. Для виртуальных ресурсов, таких как захват блокировки и семафорные допуски, Lock или Semaphore вряд ли будут собирать мусор до тех пор, пока не станет слишком поздно; для таких ресурсов, как подключения к базам данных, вы бы точно израсходовали ресурсы, если бы ожидали финализации. Многие серверы баз данных принимают только определенное количество подключений, в зависимости от лицензии. Если бы серверное приложение должно было открывать новое подключение к баз данных для каждого запроса, а потом оно бы просто отбрасывалось по завершению, нагрузка на базу данных, вероятно, достигло бы максимума задолго до того, как ненужные соединения были бы закрыты финализатором.


Ресурсы, ограниченные методом

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

В простейшем случае ресурс получается, используется, и, следует надеяться, высвобождается одним и тем же вызовом метода, как например метод loadPropertiesBadly() в Листинге 1:

Листинг 1. Неправильное получение, использование и высвобождение ресурса одним методом - не делайте так
    public static Properties loadPropertiesBadly(String fileName)
            throws IOException {
        FileInputStream stream = new FileInputStream(fileName);
        Properties props = new Properties();
        props.load(stream);
        stream.close();
        return props;
    }

К несчастью, этот пример имеет потенциальную утечку ресурса. Если все идет хорошо, поток будет закрыт до возврата метода. Но если props.load() метод выдаст IOException, тогда поток не будет закрыт (до тех пор, пока сборщик мусора не запустит свой финализатор). Решение состоит в том, чтобы использовать механизм try...finally, чтобы обеспечить закрытие потока, вне зависимости от того, что может разладиться, как показано в Листинге 2:

Листинг 2. Правильное получение, использование и высвобождение ресурса одним методом
    public static Properties loadProperties(String fileName) 
            throws IOException {
        FileInputStream stream = new FileInputStream(fileName);
        try {
            Properties props = new Properties();
            props.load(stream);
            return props;
        }
        finally {
            stream.close();
        }
    }

Обратите внимание, что получение ресурса (открытие файла) находится вне блока повторных попыток; если бы оно происходило внутри блока повторных попыток, тогда бы блок finally выполнялся, даже если получение ресурса выдало исключение. Этот подход не только будет неприемлемым (вы не можете высвободить ресурс, который вы не получили), но и код в блоке finally, скорее всего, выдаст свое собственное исключение, например NullPointerException. Исключение, созданное блоком finally, заменит исключение, которое привело к завершению работы блока, что означает, что первоначальное исключение потеряно и не может быть использовано для помощи при отладке.

Не всегда так легко, как кажется

Использование finally с целью высвобождения ресурсов, получаемых в методе, надежно, но может легко стать громоздким, когда затрагиваются множественные ресурсы. Рассмотрим метод, который использует JDBC Connection, чтобы сделать запрос и выполнить интеграцию ResultSet. Оно получает Connection, использует его, чтобы создать Statement, и выполняет Statement, чтобы выдать ResultSet. Но промежуточные JDBC-объекты Statement и ResultSet имеют собственные методы close(), и они должны быть высвобождены, когда вы закончили работу с ними. Тем не менее, "очевидный" способ очистить их, показанный в Листинге 3, не работает:

Листинг 3. Неудачная попытка высвободить множественные ресурсы - не делайте так
    public void enumerateFoo() throws SQLException {
        Statement statement = null;
        ResultSet resultSet = null;
        Connection connection = getConnection();
        try {
            statement = connection.createStatement();
            resultSet = statement.executeQuery("SELECT * FROM Foo");
            // Use resultSet
        }
        finally {
            if (resultSet != null)
                resultSet.close();
            if (statement != null)
                statement.close();
            connection.close();
        }

    }

Причина, по которой это "решение" не работает, состоит в том, что методы close()ResultSet и Statement могут сами выдавать SQLException, что может заставить следующие операторы close() в блоке finally не выполняться. Это даёт вам несколько вариантов, и все они неприятные: упаковать каждый close() в блок try..catch, вложить блоки try...finally, как показано в Листинге 4, или написать некое подобие мини-среды для управления получением и высвобождением ресурса.

Листинг 4. Надежное (хотя и громоздкое) средство высвобождения множественных ресурсов
    public void enumerateBar() throws SQLException {
        Statement statement = null;
        ResultSet resultSet = null;
        Connection connection = getConnection();
        try {
            statement = connection.createStatement();
            resultSet = statement.executeQuery("SELECT * FROM Bar");
            // Use resultSet
        }
        finally {
            try {
                if (resultSet != null)
                    resultSet.close();
            }
            finally {
                try {
                    if (statement != null)
                        statement.close();
                }
                finally {
                    connection.close();
                }
            }
        }
    }

    private Connection getConnection() {
        return null;
    }

Почти всё может выдать исключение

Мы все знаем, что мы должны использовать finally, чтобы высвободить тяжеловесные объекты, например подключения к базам данных, но мы не всегда используем его для закрытия потоков (в конце концов, финализатор сделает это за нас, не так ли?). Легко забыть использовать finally, когда код, который использует ресурс, не выдает отмеченного исключения. Листинг 5 показывает реализацию метода add() для связанной совокупности, которая использует Semaphore, чтобы обеспечить компоновку и позволить клиентам подождать, пока место не станет доступным:

Листинг 5. Уязвимая реализация связанной совокупности - не делайте так
public class LeakyBoundedSet<T> {
    private final Set<T> set = ...
    private final Semaphore sem;

    public LeakyBoundedSet(int bound) {
        sem = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException {
        sem.acquire();
        boolean wasAdded = set.add(o);
        if (!wasAdded)
            sem.release();
        return wasAdded;
    }
}

LeakyBoundedSet сперва ожидает допуска, чтобы стать доступным (указывая, что в совокупности имеется место), а затем пытается добавить элемент к соединению. Если операция добавления не исполняется, потому что элемент уже был в совокупности, допуск высвобождается (так как зарезервированное место фактически им не использовалось).

Проблема с LeakyBoundedSet не обязательно появится немедленно: что если Set.add() выдаст исключение? Этот сценарий может произойти из-за ошибки в реализации Set, или ошибки в реализации equals() или hashCode() (или реализации compareTo(), в случае SortedSet) для добавляемого элемента, или элемента, который уже находится в Set. Решение, конечно, состоит в том, чтобы использовать finally , чтобы высвободить семафорный допуск; это достаточно легкий подход, хотя о нём слишком часто забывают Эти виды ошибок редко выявляются во время тестирования, что делает их бомбой замедленного действия, которая вот-вот взорвётся. Листинг 6 показывает более надёжную реализацию BoundedSet:

Листинг 6. Использование Semaphore для надёжной компоновки Set
public class BoundedSet<T> {
    private final Set<T> set = ...
    private final Semaphore sem;

    public BoundedHashSet(int bound) {
        sem = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException {
        sem.acquire();
        boolean wasAdded = false;
        try {
            wasAdded = set.add(o);
            return wasAdded;
        }
        finally {
            if (!wasAdded)
                sem.release();
        }
    }
}

Инструментальные средства анализа зависимости кода, например FindBugs (см. Ресурсы) могут определить некоторые случаи ненадлежащего высвобождения ресурсов типа открытия потока в методе без его закрытия.


Ресурсы с произвольным временем существования

В случае ресурсов с произвольным временем существования мы снова оказываемся в таком же положении, как в C, управляя временем существования ресурса вручную. В серверном приложении, где клиенты создают устойчивое сетевое соединение с сервером на всё время сессии (например, серверы многопользовательских игр), любые ресурсы, получаемые по принципу на каждого пользователя (включая сокет-соединение) должны быть высвобождены, когда пользователь выходит из системы. Здесь может помочь хорошая организация; если отдельная ссылка на пользовательские ресурсы содержится в объекте ActiveUser, они могут быть высвобождены, когда высвобождается активный пользователь (либо явно, либо посредством сборки мусора).

Ресурсы с произвольным временем существования почти наверняка будут храниться где-нибудь в глобальной совокупности (или будут доступны из неё). Чтобы избежать утечки ресурса, крайне необходимо определить, когда ресурс больше не нужен и удалить его из этой глобальной совокупности. (Предыдущая статья "Устранение утечек памяти при помощи слабых ссылок," предлагает несколько полезных приемов.) На этом этапе, так как вы знаете, что ресурс будет высвобожден, любые не требующие обращения к памяти ресурсы, ассоциируемые с этим ресурсом, могут также быть высвобожденными в это время.

Принадлежность ресурса

Ключевой прием обеспечения своевременного высвобождения ресурса состоит в том, чтобы сохранить строгую иерархию принадлежности; вместе с принадлежностью приходит ответственность высвобождения ресурса. Если приложение создает пул потока и пул потока создает потоки, эти потоки становятся ресурсами, которые должны быть высвобождены (то есть их можно прервать) до того, как программа сможет завершить работу. Но потоки принадлежат не приложению, а пулу потока, и, следовательно, пул потока должен нести ответственность за их высвобождение. Конечно, он не может высвободить их до того, как пул потока сам не будет высвобожден приложением.

Сохранение иерархии принадлежности, где каждый ресурс владеет ресурсами, которые он получает и за высвобождение которых он отвечает, не дает ситуации выйти из-под контроля. Следствием этого правила является то, что каждый ресурс, который не может быть высвобожден исключительно сборкой мусора (то есть любой ресурс, который явно или неявно владеет ресурсом, который не может быть высвобожден исключительно сборкой мусора) должен обеспечить какую-либо поддержку времени существования, например метод close().

Финализаторы

Если в библиотеках платформы предусмотрены финализаторы для удаления открытых обработчиков файлов, что значительно уменьшает риск забыть закрыть их явно, почему же тогда финализаторы не используются чаще? Существуют определенные причины, самая главная из которых - это то, что финализаторы очень сложно написать правильно (и очень легко написать неправильно). Трудно не только правильно их запрограммировать, время финализации также не детерминировано, и нет гарантии, что финализаторы вообще будут когда-либо функционировать. Кроме того, финализация добавляет непроизводительные издержки при создании экземпляров и сборке мусора финализируемых объектов. Не полагайтесь на финализаторы как на главное средство высвобождения ресурсов.


Резюме

Сборка мусора выполняет за нас очень большой объём чистки, но некоторые ресурсы все-таки нуждаются в явном высвобождении, например дескрипторы файла, дескрипторы сокета, потоки, подключения к базам данных и семафорные допуски. Мы часто можем обойтись использованием блоков finally, чтобы высвободить ресурс, если время его существования привязано к заданным рамкам вызова, но долгоживущие ресурсы требуют стратегии обеспечения их окончательного высвобождения. Для любого объекта, который может явно или неявно владеть объектом, требующим явного высвобождения, вы должны обеспечить методы времени существования - close(), release(), destroy() и т. п., чтобы обеспечить надежную чистку.

Ресурсы

Научиться

Получить продукты и технологии

  • Ресторан Алисы (Warner Brothers, 1969 г.): Узнайте все слова классического народного гимна Арло Гутри (Arlo Guthrie) "Ресторан Алисы" из саундтрека к фильму.
  • FindBugs: Это инструментальное средство анализа зависимости свободного кода может найти невысвобожденные ресурсы и другие ошибки в ваших программах.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=194626
ArticleTitle=Теория и практика Java: Хорошие практические приемы по ведению домашнего хозяйства
publish-date=02082007