Почему растет потребление памяти при массовой записи объектов справочника в 1С и как это оптимизировать

Программист 1С v8.3 (Обычные формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При выполнении групповых обработок, связанных с изменением реквизитов тысяч элементов справочников или документов, программисты часто сталкиваются с тем, что процесс 1cv8.exe (или rphost в серверном варианте) начинает стремительно поглощать оперативную память. В некоторых случаях это приводит к падению клиента с ошибкой "Недостаточно памяти". Разберем по шагам, почему это происходит, как работает платформа 1С на низком уровне и как правильно писать код для обработки больших массивов данных.

Анализ исходной ситуации: почему код вызывает рост памяти

Рассмотрим типовой пример кода, который часто используется для простых задач (например, замена ставки НДС в справочнике Номенклатура):


Процедура КнопкаВыполнитьНажатие(Кнопка)
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ Код, Ссылка КАК Номенклатура, СтавкаНДС
    |ИЗ Справочник.Номенклатура 
    |ГДЕ СтавкаНДС В(ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.НДС18), ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.НДС18_118))";
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        Попытка
            объ = Выборка.Номенклатура.ПолучитьОбъект();
            объ.СтавкаНДС = Перечисления.СтавкиНДС.НДС20;
            объ.Записать();
        Исключение
            // Обработка ошибки
        КонецПопытки;
    КонецЦикла;
КонецПроцедуры

На первый взгляд, код выглядит корректно. Однако при обработке 57 000 элементов проанализируем, что происходит внутри системы. Основная проблема здесь заключается в методе ПолучитьОбъект(). Когда мы вызываем этот метод в цикле, платформа создает в оперативной памяти полноценный объект метаданных. Справочник Номенклатура в типовых решениях (УТ, УПП, ERP) является крайне "тяжелым": он содержит десятки реквизитов и множество табличных частей (единицы измерения, штрихкоды, дополнительные свойства). При вызове ПолучитьОбъект() 1С считывает из базы данных абсолютно все данные этого элемента, включая все его табличные части, даже если нам нужно изменить всего один реквизит.

Механизм объектного кеширования в 1С

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

Важные особенности кеширования:

  1. Кеш не очищается мгновенно: Даже если переменная объ в коде выше переинициализируется на следующей итерации, старый объект может оставаться в "куче" (heap) менеджера памяти 1С.
  2. Фрагментация памяти: 1С написана на языке C++. Менеджер памяти операционной системы не всегда забирает выделенные блоки обратно сразу после их освобождения программой. Процесс может пометить память как "свободную для внутреннего использования", но в диспетчере задач Windows мы будем видеть стабильно высокий или растущий объем Private Working Set.
  3. Размер объекта: Для "тяжелых" справочников один объект в памяти может занимать от нескольких десятков килобайт до мегабайта (с учетом всех связанных структур). Умножив это на тысячи итераций, мы получаем сотни мегабайт занятого пространства.

Влияние подписок на события и модулей объекта

Проанализируем ситуацию глубже. Метод объ.Записать() инициирует целую цепочку событий. В типовых конфигурациях на справочник Номенклатура обычно настроено множество подписок на события: ПередЗаписью, ПриЗаписи. Внутри этих подписок могут выполняться:

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

Рекомендации по оптимизации массовой обработки

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

1. Минимизация данных в запросе

В исходном коде в запросе выбираются поля Код и СтавкаНДС. Если они используются только для анализа внутри цикла — это допустимо. Но если ваша цель — просто изменить значение, старайтесь не тянуть лишние данные. Однако помните, что ПолучитьОбъект() все равно прочитает всё остальное. Посмотрим на пример более эффективного кода:


Запрос.Текст = "ВЫБРАТЬ Ссылка КАК Номенклатура
|ИЗ Справочник.Номенклатура 
|ГДЕ СтавкаНДС <> ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.НДС20)";

2. Проверка необходимости записи

Самая частая ошибка — перезаписывать объект, который уже имеет нужное значение. Операция Записать() — самая дорогая. Всегда добавляйте условие:


объ = Выборка.Номенклатура.ПолучитьОбъект();
Если объ.СтавкаНДС <> Перечисления.СтавкиНДС.НДС20 Тогда
    объ.СтавкаНДС = Перечисления.СтавкиНДС.НДС20;
    объ.Записать();
КонецЕсли;

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

3. Использование объектных блокировок

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


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура");
ЭлементБлокировки.УстановитьЗначение("Ссылка", Выборка.Номенклатура);
Попытка
    Блокировка.Заблокировать();
    объ = Выборка.Номенклатура.ПолучитьОбъект();
    // ... логика изменения ...
    объ.Записать();
Исключение
    // Объект занят, пропускаем или логируем
КонецПопытки;

4. Обработка данных порциями

Если элементов сотни тысяч, лучше всего обрабатывать их порциями (например, по 1000 элементов) с фиксацией прогресса. В управляемых формах для этого можно использовать фоновые задания, которые после завершения определенного объема работы завершаются, полностью освобождая память rphost. В обычном приложении (как в случае с УТ 10.2) можно принудительно вызывать очистку кеша или даже перезапускать обработку с того места, где она остановилась.

Почему 100-300 МБ — это не приговор?

Важно понимать разницу между утечкой памяти (когда память теряется безвозвратно из-за ошибок в коде платформы или конфигурации) и рабочим кешем. Если после обработки 57 000 элементов потребление памяти выросло на 200 МБ и зафиксировалось на этой отметке — это нормальное поведение системы. Платформа 1С удерживает эти данные в куче, чтобы не тратить время на повторное выделение памяти у ОС при следующих операциях. Тревогу стоит бить только в том случае, если рост памяти неограничен и приводит к падению приложения при достижении лимита (около 2-3 ГБ для 32-битных версий клиента).

Подведем итог: для стабильной работы массовых обновлений в 1С всегда анализируйте "тяжесть" объектов, проверяйте необходимость записи перед вызовом Записать() и старайтесь выполнять такие задачи на стороне сервера (в клиент-серверном варианте), чтобы минимизировать передачу объектных данных по сети и нагрузку на клиентский терминал.

← На главную