Мягкие ссылки на страже доступной памяти или как экономить память правильно

    image
    Все java-разработчики, рано или поздно, встречаются с пресловутой ошибкой OutOfMemoryError. 
    После этой встречи мы начинаем более бережно относится к используемой памяти, экономить ее. Начиная с версии 1.2 в Java появился пакет java.lang.ref.* с классами SoftReference, WeakReference, PhantomReference. Далее я расскажу вам о том, как помогут эти классы в борьбе с OutOfMemoryError. И что более интересно, приведу реальные примеры их использования. Начнем.

    Общее Описание


    Для начала немного общей теории. Вспомним, в общих чертах, как работает Garbage Collector (далее GC). Если не вдаваться в детали, то алгоритм прост: при запуске сборщика виртуальная машина рекурсивно находит, для всех потоков, все доступные объекты в памяти и помечает их неким образом. А на следующем шаге GC удаляет из памяти все непомеченные объекты. Таким образом, после чистки, в памяти будут находиться только те объекты, которые могут быть полезны программе. Идем дальше.
    В Java есть несколько видов ссылок. Есть StrongReference — это самые обычные ссылки которые мы создаем каждый день.
    	StringBuilder builder = new StringBuilder();
    
    builder это и есть strong-ссылка на объект StringBuilder.
    И есть 3 «особых» типа ссылок — SoftReference, WeakReference, PhantomReference. По сути, различие между всеми типами ссылок только одно — поведение GC с объектами, на которые они ссылаются. Мы более детально обсудим особенности каждого типа ссылок позже, а пока достаточно будет следующих знаний:
    1. SoftReference — если GC видит что объект доступен только через цепочку soft-ссылок, то он удалит его из памяти. Потом. Наверно.
    2. WeakReference — если GC видит что объект доступен только через цепочку weak-ссылок, то он удалит его из памяти.
    3. PhantomReference — если GC видит что объект доступен только через цепочку phantom-ссылок, то он его удалит из памяти. После нескольких запусков GC.
    Если пока не понятно в чем же разница, то не переживайте, скоро все станет на свои места. Мелочи в деталях, а детали будут дальше.
    Эти 3 типа ссылок наследуются от одного родителя — Reference, у которого они собственно и берут все свои public методы и конструкторы.

    StringBuilder builder = new StringBuilder();
    SoftReference<StringBuilder> softBuilder = new SoftReference(builder);
    

    После выполнения этих двух строчек у нас будет 2 типа ссылок на 1 объект StringBuilder:
    • builder — strong-ссылка
    • softBuilder — soft-ссылка (формально это strong-ссылка на soft-ссылку, но для простоты я буду писать soft-ссылка)
    И если во время выполнения программы, переменная builder станет недоступной, но при этом ссылка на объект, на который ссылается softBuilder, будет еще доступна И запустится GC -> то объект StringBuilder будет помечен как доступный только через цепочку soft-ссылок.
    Рассмотрим доступные методы:
    softBuilder.get() — вернет strong-ссылку на объект StringBuilder в случае если GC не удалил этот объект из памяти. В другом случае вернется null.
    softBuilder.clear() — удалит ссылку на объект StringBuilder (то есть soft-ссылки на этот объект больше нет)
    Все то же самое работает и для WeakReference и для PhantomReference. Правда, PhantomReference.get() всегда будет возвращать null, но об этом позже.
    Есть еще такой класс – ReferenceQueue. Он позволяет отслеживать момент, когда GC определит что объект более не нужен и его можно удалить. Именно сюда попадает Reference объект после того как объект на который он ссылается удален из памяти. При создании Reference мы можем передать в конструктор ReferenceQueue, в который будут помещаться ссылки после удаления.

    Детали SoftReference


    Особенности GC
    Так всё же, как ведет себя GC когда видит что объект доступен только по цепочке soft-ссылок? Давайте рассмотрим работу GC более детально:
    И так, GC начал свою работу и проходит по всем объектам в куче. В случае, если объект в куче это Reference, то GC помещает этот объект в специальную очередь в которой лежат все Reference объекты. После прохождения по всем объектам GC берет очередь Reference объектов и по каждому из них решает удалять его из памяти или нет. Как именно принимается решение об удалении объекта — зависит от JVM. Но общий контракт звучит следующим образом: GC гарантировано удалит с кучи все объекты, доступные только по soft-ссылке, перед тем как бросит OutOfMemoryError.
    SoftReference это наш механизм кэширования объектов в памяти, но в критической ситуации, когда закончится доступная память, GC удалит не использующиеся объекты из памяти и тем самым попробует спасти JVM от завершения работы. Это ли не чудно?
    Вот как Hotspot принимает решение об удалении SoftReference: если посмотреть на реализацию SoftReference, то видно, что в классе есть 2 переменные — private static long clock и private long timestamp. Каждый раз при запуске GC, он сетит текущее время в переменную clock. Каждый раз при создании SoftReference, в timestamp записывается текущее значение clock. timestamp обновляется каждый раз при вызове метода get() (каждый раз, когда мы создаем strong-ссылку на объект). Это позволяет вычислить, сколько времени существует soft-ссылка после последнего обращения к ней. Обозначим этот интервал буквой I. Буквой F обозначим количество свободного места в куче в MB(мегабайтах). Константой MSPerMB обозначим количество миллисекунд, сколько будет существовать soft-ссылка для каждого свободного мегабайта в куче.
    Дальше все просто, если I <= F * MSPerMB, то не удаляем объект. Если больше то удаляем.
    Для изменения MSPerMB используем ключ -XX:SoftRefLRUPolicyMSPerMB. Дефалтовое значение — 1000 ms, а это означает что soft-ссылка будет существовать (после того как strong-ссылка была удалена) 1 секунду за каждый мегабайт свободной памяти в куче. Главное не забыть что это все примерные расчеты, так как фактически soft-ссылка удалится только после запуска GC.
    Обратите внимание на то, что для удаления объекта, I должно быть строго больше чем F * MSPerMB. Из этого следует что созданная SoftReference проживет минимум 1 запуск GC. (*если не понятно почему, то это останется вам домашним заданием).
    В случае VM от IBM, привязка срока жизни soft-ссылки идет не к времени, а к количеству переживших запусков GC.

    Применение
    Главная плюшка SoftReference в том что JVM сама следит за тем нужно удалять из памяти объект или нет. И если осталось мало памяти, то объект будет удален. Это именно то, что нам нужно при кэшировании. Кэширование с использованием SoftReference может пригодится в системах чувствительных к объему доступной памяти. Например, обработка изображений. Первый пример применения будет немного выдуманным, зато показательным:
    Наша система занимается обработкой изображений. Допустим, у нас есть громадное изображение, которое находиться где-то в файловой системе и это изображение всегда статично. Иногда пользователь хочет соединить это изображение с другим изображением. Вот наша первая реализация такой конкатенации:

    public class ImageProcessor {
    	private static final String IMAGE_NAME = "bigImage.jpg";
    	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {
    		InputStream defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);                
    		// calculate and return concatenated image
    	}            
    }
    

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

    public class CachedImageProcessor {
    	private static final String IMAGE_NAME = "bigImage.jpg";
    	private InputStream defaultImage;           
    	
    	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {
    		if (defaultImage == null) {
    			defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);
    		}                
    		// calculate and return concatenated image
    	}            
    }
    

    Этот вариант уже лучше, но проблема все ровно есть. Изображение большое и забирает много памяти. Наше приложение работает со многими изображениями и при очередной попытке пользователя обработать изображение, легко может свалиться OutOfMemoryError. И что с этим можно сделать? Получается, что нам нужно выбирать, либо быстродействие либо стабильность. Но мы то знаем о существовании SoftReference. Это поможет нам продолжать использовать кеширование, но при этом в критических ситуациях выгружать их из кэша для освобождения памяти. Да еще и при этом нам не нужно беспокоиться о детектировании критической ситуации. Вот так будет выглядеть наша третья реализация:

    public class SoftCachedImageProcessor {
    	private static final String IMAGE_NAME = "bigImage.jpg";
    	private SoftReference<InputStream> defaultImageRef = new SoftReference(loadImage());
    
    	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {                
    		if (defaultImageRef.get() == null) {        //  1
    			defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);
    			defaultImageRef = new SoftReference(defaultImage);
    		}        
    		
    		defaultImage = defaultImageRef.get();        //  2        
    		// calculate and return concatenated image
    	}            
    }
    

    Эта версия не идеальна, но она показывает как просто мы можем контролировать размер занимаемый кэшем, а точнее возложить контроль на виртуальную машину. Опасность данной реализации заключается в следующем. В строчке №1 мы делаем проверку на null, фактически мы хотим проверить, удалил GC данные с памяти или нет. Допустим, что не удалил. Но перед выполнением строки №2 может начать работу GC и удалить данные. В таком случае результатом выполнения строчки №2 будет defaultImage = null. Для безопасной проверки существования объекта в памяти, нам нужно создать strong-ссылку, defaultImage = defaultImageRef.get(); Вот как будет выглядеть финальная реализация:

    public class SoftCachedImageProcessor {
    	private static final String IMAGE_NAME = "bigImage.jpg";
    	private SoftReference<InputStream> defaultImageRef = new SoftReference(loadImage());;
    
    	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {
    		defaultImage = defaultImageRef.get();
    		if (defaultImage == null) {
    			defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);
    			defaultImageRef = new SoftReference(defaultImage);
    		}                
    		// calculate and return concatenated image
    	}            
    }
    

    Пойдем дальше. java.lang.Class тоже использует SoftReference для кэширования. Он кэширует данные о конструкторах, методах и полях класса. Интересно посмотреть, что именно они кешируют. После того как решено использовать SoftReference для кеширования, нужно решить что именно кешировать. Допустим нам нужно кешировать List. Мы можем использовать как List<SoftReference> так и SoftReference<List>. Второй вариант более приемлемый. Нужно помнить, что GC применяет специфическую логику при обработке Reference объектов, да и освобождение памяти будет происходить быстрее если у нас будет 1 SoftReference а не их список. Это мы и видим в реализации Class — разработчики создали soft-ссылку на массив конструкторов, полей и методов. Если говорить про производительность, то стоить отметить что часто, ошибочно, люди используют WeakReference для построения кэша там где стоит использовать SoftReference. Это приводит к низкой производительности кэша. На практике weak-ссылки быстро будут удалены из памяти, как только исчезнут strong-ссылки на объект. И когда нам реально понадобиться вытянуть объект с кэша, мы увидим что его там уже нет.
    Ну и еще один пример использования кэша на основе SoftReference. В Google Guava есть класс MapMaker. Он поможет нам построить ConcurrentMap в которой будут следующая особенность — ключи и значения в Map могут заворачиваться в WeakReference или SoftReference. Допустим в нашем приложении есть данные, которые может запросить пользователь и эти данные достаются с базы данных очень сложным запросом. Например, это будет список покупок пользователя за прошлый год. Мы можем создать кэш в котором значения (список покупок) будут храниться с помощью soft-ссылок. А если в кэше не будет значения то нужно вытянуть его с БД. Ключом будет ID пользователя. Вот как может выглядеть реализация:

    ConcurrentMap<Long, List<Product>> oldProductsCache = new MapMaker().softValues().
               .makeComputingMap(new Function<User, List<Product>>() {
                       @Override
                       public List<Product> apply(User user) {
                         return loadProductsFromDb(user);
                       }
                 });
    

    WeakReference

    Особенности GC
    Теперь рассмотрим более детально, что же собой представляет WeakReference. Когда GC определяет, что объект доступен только через weak-ссылки, то этот объект «сразу» удаляется с памяти. Тут стоить вспомнить про ReferenceQueue и проследить за порядком удаления объекта с памяти. Напомню что для WeakReference и SoftReference алгоритм попадания в ReferenceQueue одинаковый. Итак, запустился GC и определил что объект доступен только через weak-ссылки. Этот объект был создан так:
    StrIngBuilder AAA = new StringBuilder();
    ReferenceQueue queue = new ReferenceQueue();
    WeakReference weakRef = new WeakReference(AAA, queue);
    

    Сначала GC очистит weak-ссылку, то есть weakRef.get() – будет возвращать null. Потом weakRef будет добавлен в queue и соответственно queue.poll() вернет ссылку на weakRef. Вот и все что хотелось написать про особенности работы GC с WeakReference. Теперь посмотрим, как это можно использовать.
    Применение
    Ну конечно WeakHashMap. Это реализация Map<K,V> которая хранит ключ, используя weak-ссылку. И когда GC удаляет ключ с памяти, то удаляется вся запись с Map. Думаю не сложно понять, как это происходит. При добавлении новой пары <ключ, значение>, создается WeakReference для ключа и в конструктор передается ReferenceQueue. Когда GC удаляет ключ с памяти, то ReferenceQueue возвращает соответствующий WeakReference для этого ключа. После этого соответствующий Entry удаляется с Map. Все довольно просто. Но хочется обратить внимание на некоторые детали.
    • WeakHashMap не предназначена для использования в качестве кэша. WeakReference создается для ключа а не для значения. И данные будут удалены только после того как в программе не останется strong-ссылок на ключ а не на значение. В большинстве случаев это не то чего вы хотите достичь кэшированием.
    • Данные с WeakHashMap будут удалены не сразу после того как GC обнаружит что ключ доступен только через weak-ссылки. Фактически очистка произойдет при следующем обращении к WeakHashMap.
    • В первую очередь WeakHashMap предназначен для использования с ключами, у которых метод equals проверяет идентичность объектов (использует оператор ==). Как только доступ к ключу потерян, его уже нельзя создать заново.
    Хорошо, тогда в каких случаях удобно использовать WeakHashMap? Допустим нам нужно создать XML документ для пользователя. Конструированием документа будут заниматься несколько сервисов, которые на вход будут получать org.w3c.Node в который будут добавлять необходимые элементы. Так же для сервисов нужно много информации о пользователе с Базы Данных. Эти данные мы будем складировать в классе UserInfo. Класс UserInfo занимает много места в памяти и актуален только для построения конкретного XML документа. Кешировать UserInfo не имеет смысла. Нам нужно только ассоциировать его с документом и желательно удалить из памяти, когда документ более не используется программой. Все что нам нужно сделать:
    private static final NODE_TO_USER_MAP = new WeakHashMap<Node, UserInfo>();
    

    Создание XML документа будет выглядеть примерно так:
    Node mainDocument = createBaseNode();
    NODE_TO_USER_MAP.put(mainDocument, loadUserInfo());
    

    Ну а вот чтение:
    UserInfo userInfo = NODE_TO_USER_MAP.get(mainDocument);
    If(userInfo != null) {
    	// …
    }
    

    UserInfo будет находиться в WeakHashMap до тех пор пока GC не заметит, что на mainDocument остались только weak-ссылки.
    Другой пример использования WeakHashMap. Многие знают про метод String.intern(). Так вот с помощью WeakReference можно создать нечто подобное. (Давайте не будет обсуждать, в рамках этой статьи, целесообразность этого решения, и примем факт, что у этого решения есть некоторые преимущества по сравнению с intern()). Итак, у нас есть ооочень много строк. Мы знаем что строки повторяются. Для сохранения памяти мы хотим использовать повторно уже существующие объекты, а не создавать новые объекты для одинаковых строк. Вот как в этом нам поможет WeakHashMap:
    private static Map<String, WeakReference<String>> stringPool = new WeakHashMap<String, WeakReference<String>>;
    
    public String getFromPool(String value) {
    	WeakReference<String> stringRef = stringPool.get(value);
    	if (stringRef == null || stringRef.get() == null ) {
    		stringRef = new WeakReference<String>(value);
    		stringPool.put(value, stringRef);
    	}
    
    	return stringRef.get();
    }
    

    И на последок добавлю, что WeakReference используется во многих классах – Thread, ThreadLocal, ObjectOutpuStream, Proxy, LogManager. Вы можете посмотреть на их реализацию для того чтоб понять в каких случаях вам может помочь WeakReference.

    PhantomReference

    Особенности GC
    Особенностей у этого типа ссылок две. Первая это то, что метод get() всегда возвращает null. Именно из-за этого PhantomReference имеет смысл использовать только вместе с ReferenceQueue. Вторая особенность – в отличие от SoftReference и WeakReference, GC добавит phantom-ссылку в ReferenceQueue послетого как выполниться метод finalize(). Тоесть фактически, в отличии от SoftReference и WeakReference, объект еще есть в памяти.

    Практика
    На первый взгляд не ясно как можно использовать такой тип ссылок. Для того чтоб объяснить как их использовать, ознакомимся сначала с проблемами возникающими при использовании метода finalize(): переопределение этого метода позволяет нам очистить ресурсы связанные с объектом. Когда GC определяет что объект более недоступный, то перед тем как удалит его из памяти, он выполняет этот метод. Вот какие проблемы с этим связаны:
    1. GC запускается непредсказуемо, мы не можем знать когда будет выполнен метод finalize()
    2. Методы finalize() запускаются в одном потоке, по очереди. И до тех пор, пока не выполниться этот метод, объект не может быть удален с памяти
    3. Нет гарантии, что этот метод будет вызван. JVM может закончить свою работу и при этом объект так и не станет недоступным.
    4. Во время выполнения метода finalize() может быть создана strong-ссылка на объект и он не будет удален, но в следующий раз, когда GC увидит что объект более недоступен, метод finalize() больше не выполнится.
    Вернемся к PhantomReference. Этот тип ссылок в комбинации с ReferenceQueue позволяет нам узнать, когда объект более недоступен и на него нет других ссылок. Это позволяет нам сделать очистку ресурсов, используемых объектом, на уровне приложения. В отличии от finalize() мы сами контролируем процесс очистки ресурсов. Помимо этого, мы можем контролировать процесс создания новых объектов. Допустим у нас есть фабрика, которая будет возвращать нам объект HdImage. Мы можем контролировать, сколько таких объектов будет загружено в память:
    public HdImageFabric {
    	public static final int IMAGE_LIMIT = 10;
    	public static int count = 0;
    	public static ReferenceQueue<HdImage> queue = new ReferenceQueue<HdImage>();
    
    	public HdImage loadHdImage(String imageName) {
    		while (true) {
    			if (count < IMAGE_LIMIT) {
    				return	wrapImage(loadImage(imageName));	
    			} else {
    				Reference<HdImage> ref = queue.remove(500);
    				if (ref != null) {
    					count--;
    					System.out.println(“remove old image”);
    				}
    			}
    		}
    	}
    
    	private HdImage wrapImage(HdImage image) {
    		PhantomReference<HdImage> refImage = new PhantomReference(image, queue);
    		count++;
    		return refImage ;
    	}
    }
    

    Этот пример не потокобезопасный и имеет друге недостатки, но зато он показывает, как можно использовать на практике PhantomReference.
    Из-за того что метод get() всегда возвращает null, становится непонятным а как все же понять какой именно объект был удален. Для этого нужно создать собственный класс, который будет наследовать PhantomReference, и который содержит некий дескриптор, который в будущем поможет определить какие ресурсы нужно чистить.
    Когда вы используете PhantomReference нужно помнить о следующих вещах:
    1. Контракт гарантирует что ссылка появится в очереди после того как GC заметит что объект доступен только по phantom-ссылкам и перед тем как объект будет удален из памяти. Контракт не гарантирует, что эти события произойдут одно за другим. В реальности между этими событиями может пройти сколько угодно времени. Поэтому не стоит опираться на PhantomReference для очистки критически важных ресурсов.
    2. Выполнение метода finalize() и добавление phantom-ссылки в ReferenceQueue выполняется в разных запусках GC. По этому если у объекта переопределен метод finalize() то для его удаления необходимы 3 запуска GC, а если метод не переопределен, то нужно, минимум, 2 запуска GC
    .

    В качестве вывода хочу сказать что java.lang.ref.* дает нам неплохие возможности для работы с памятью JVM и не стоит игнорировать эти классы, они могут здорово нам помочь. Их использование связанно с большим количеством ошибок, и нужно быть крайне осторожным для достижения желаемого результата. Но разве эти трудности нас когда-то останавливали? На этом все. Спасибо всем кто дочитал до конца. Постараюсь в комментариях ответить на те вопросы, которые не сумел раскрыть в этой статье.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 22
    • +1
      Как же вы вовремя! Как-раз делаю нечто очень похожее на ваш «надуманный» пример. Спасибо за полезную статью.
      • +1
        Рад что эта статья кому-то пригодится. Советую перед использованием Reference, сформулировать себе чего именно вы хотите достичь. Часто бывает, что непродуманное использование приводит только к снижению производительности.
        • 0
          У вас вот очень даже хорошо сформулировано, прямо как под заказ: мне нужен кеш изображений, работающий по принципу — держать в памяти пока её хватает, удалять изображения из памяти, если её становится мало. SoftReference подходит идеально.
          • 0
            Хотя вот ниже пишут, что это не идеальное решение. Ушел разбираться.
      • +1
        Очень интересная тема, надо бы разобраться во всех нюансах. Не подозревал о таких возможностях джавы под боком.
        • +4
          Будучи дотнетчиком, сильно уважаю джаву за её технологичность и открытость. Замечательная статья, спасибо!
          • 0
            В .NET есть слабые ссылки:
            msdn.microsoft.com/ru-ru/library/system.weakreference.aspx
            • 0
              Да, я как-то писал кэш объектов на основе WeakReference дотнета. Если проводить параллели, то они больше соответствуют SoftReference, чем одноимённым ссылкам:
              A weak reference is valid only during the indeterminate amount of time until the object is collected when no strong references exist.
          • +6
            Хочу добавить, что если программируете под андроид, то лучше не использовать SoftReference для кеширование. Поведение Dalvik VM с SoftReference не совсем такое, как на JVM, и они могут быть удалены слишком рано, что сведёт на нет всю пользу от кеша. В документации к SoftReference на андроиде советуют использовать LruCache.
            • 0
              По поводу Dalvik VM не скажу, но вот по поводу JVM часто можно встретить подобные высказывания. Многие, в качестве лучшей альтернативы, рекомендуют использовать кэш, который реализовывает LRU алгоритм. Например в LinkedHashSet можно переопределить метод removeEldestEntry. Но нельзя говорить о том что кэш на основе SoftReference всегда проигрывает. У него есть свои плюсы. Как пример, почитайте обсуждение тут — SoftReference Read-Write Cache для Hibernate
              • 0
                А не подскажете, соответствует ли работа Dalvik-а с PhantomReference официальной документации, или тоже существуют какие-то подводные камни?
              • 0
                Очень познавательно, спб!
                • +1
                  Прочел я эту статью о хитростях и премудростях Java и возрадовался, что программирую на Си.
                  • –8
                    Насоздают софтссылок, в итоге жабоприложение выжирает память откуда только можно и сваливает полсистемы в своп. Привет, тормоза. При этом софтссылки автоматом не очищаются, т. к. память-то как бы есть и приложение больше не просит. А то что там кому-то другому не хватает памяти, жабе пофигу.
                    • +3
                      Привет, java -help
                      • +3
                        Вы кого обвиняете? Автора за то, что он рассказал про один из инструментов джавы? Давайте ещё производителей ножей обвиним, в том, что «понакупают ножей, а потом убивают обычных людей».
                        • +2
                          Ваше «жабоприложение» выжрет памяти не больше чем вы ему разрешите.
                          • –2
                            По дефолту разрешено весьма до фига.
                        • 0
                          Немного сумбурно, но всё-таки полезная статья, знал про них, но всегда забывал разобраться подробнее.
                          PS: Зайдите пожалуйста сюда и если не сложно, то поправьте статью, читать тяжело.
                          • 0
                            эх, зря я пользовался для организации кэшей WeakHashMap. Так что, спасибо за статью, было трудно усвоить различия между разными типами ссылок.
                            • –1
                              Отмечу, что MapMaker.makeComputingMap() в Guava — deprecated с версии 10.0 (сейчас уже 14.0). Вместо него нужно использовать CacheBuilder.build()
                              • 0
                                За что заминусовали-то???

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