Русский
Русский
English
Статистика
Реклама

Jvm

Демократия в Telegram-группах

07.05.2021 14:19:42 | Автор: admin

Многие из нас проводят время в профильных телеграм-группах. Власть над общением здесь принадлежит случайным людям со своими недостатками. Нередко встречаются конфликты и злоупотребления. Это побуждает задуматься, а можно ли поддерживать порядок иначе, так, чтобы не расцветал мошеннический спам и одновременно ни у кого не было абсолютной власти над адекватным собеседником?
И в моем случае эти размышления вылились в разработанную и протестированную систему которую уже сегодня можно подключить в ваш Telegram.

Прежде всего, нужно определить что является ключевым аспектом, валютой системы. Не карма и не баллы, не лайки и не рейтинг. Им стало само время. Что из этого следует?

  • У каждого пользователя копится время с момента написания первого сообщения.
    Это аналог безусловного базового дохода. Минута за минутой, час за часом. Он необходим чтобы эмиссия системной валюты происходила без злоупотреблений, независимо от каких-либо центров управления.

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

  • Каждый также может использовать накопленное время чтобы разбанить другого пользователя.
    Обратимость бана защищает от злоупотреблений свободного оборота банхаммеров. Одновременно эта функция является обратным к эмисии процессом. Время потраченное на выкуп из бана сгорает безвозвратно.

  • Накопленное время можно передать другим людям, полностью или по частям.
    Эта функция в перспективе обеспечивает подключенным группам свободный рынок обмена чем угодно.

Итак, как это работает в теории разобрались, а что необходимо для практического функционирования такой системы?

  • Простой способ подключения чата к системе.
    - Реализована в виде бота

  • Максимально удобный для Telegram команд UX
    - Есть распознавание речи основанное на нормализованных семантических представлениях

Где примеры использования?

Бот понимает в запросах и русский и английский язык в свободной форме. Используются сокращения: d - дни, h - часы, m - минуты, s - секунды. Все уведомления публичны, но исчезают через 15 секунд, чтобы не засорять общий чат.

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

Довольно слов, покажите мне код!

Для бекенда использовался язык Kotlin + JVM, в качестве базы данных используется Redis-кластер. Весь код продокументирован и доступен на GitHub: demidko/timecobot
Чтобы начать использовать бота в вашей телеграм-группе просто добавьте его с правами администратора: @timecbobot

Напишите пожалуйста свое мнение в комментариях, нужное ли это для сообщества дело и стоит ли развивать систему дальше? Что можно улучшить или поменять? Интересна ли подробная статья с кодом на тему как это реализовано внутри?

Всем удачного дня!

Подробнее..

Перевод Графика для JVM

02.05.2021 16:16:26 | Автор: admin


Допустим, я хочу создавать качественные десктопные приложения. Я также хочу сделать это на JVM. Не надейтесь мы еще не достигли цели. Но у меня есть план.

Почему именно JVM?


Это производительность на достаточно высоком уровне, но не заставляет вас слишком много задумываться о каждом выделение памяти. Это кроссплатформенно. В нем есть отличные языки Kotlin, Scala и, конечно же, Clojure. C # тоже подойдет, но в нем нет Clojure.

Разве вы уже не можете создавать десктопные приложения на JVM?


Вы можете. Но традиционно AWT, Swing и JavaFX сопровождались множеством недостатков в качестве и производительности. Они были настолько существенными, что только одной компании удалось создать прилично выглядящее приложение на Swing. Это возможно, но требует огромных усилий.

Разве не все пользовательские интерфейсы Java прокляты?


Нет, не совсем. У AWT, Swing и JavaFX есть свои проблемы, но это исключительно их проблемы. Нет фундаментальной причины, по которой невозможно создать высококачественный пользовательский интерфейс на JVM. Просто это еще не было сделано.

Почему это еще не было сделано?


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

Почему не Electron?


Первая причина производительность. JS отличный язык для создания пользовательского интерфейса, но он намного медленнее, чем JVM. Wasm может быть быстрым, но подразумевает C ++ или Rust.

Второй это модель DOM. Это ужасающая коллекция уловок, которые делают простые вещи сложными, а сложные невозможными. Я много раз думал: Если бы я рисовал этот элемент управления/макет напрямую, я бы закончил несколько часов назад.

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

Однако Electron научил нас двум хорошим вещам:

  • Люди жаждут нативных приложений. Никто не хочет работать из браузера.
  • Людей не волнует, что приложения не выглядят нативными для платформы, если они хорошо выглядят.


Десктоп по-прежнему актуален?


Я верю, что это так!

Недавно я смотрел интервью между разработчиком Android и разработчиком iOS. Один спрашивал:

Кто-нибудь по-прежнему пишет десктопные приложения?

На что другой ответил:

Понятия не имею Может быть?

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

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

Телефоны отлично подходят для небольших быстрых одноцелевых задач. У них есть свое место, но жизнь намного сложнее, чем телефон. Многим из нас все еще нужен этот велосипед для ума.

Хорошо, что же ты предлагаешь?


Путь к качественному пользовательскому интерфейсу на JVM долгий. Нам понадобятся:

  • графическая библиотека,
  • библиотека интеграции окна / ОС,
  • набор инструментов пользовательского интерфейса.


Сегодня я рад анонсировать первую часть этого эпического квеста: графическую библиотеку. Она называется Skija и представляет собой просто набор привязок к очень мощной, хорошо известной библиотеке Skia. Та, которую поддерживает Chrome, Android, Flutter и Xamarin.

image

Как и любая другая библиотека JVM, она кроссплатформенная. Она работает в Windows, Linux и macOS. Это так же просто, как добавить файл JAR. Намного проще, чем массировать флаги компилятора C ++ в течение нескольких дней, прежде чем вы сможете что-либо скомпилировать. Skija позаботится об управлении памятью за вас. И привязки создаются вручную, поэтому они всегда имеют смысл и доставляют удовольствие (по крайней мере, насколько позволяет Skia API).

Что с этим делать? В основном рисовать. Линии. Треугольники. Прямоугольники. Но также: кривые, контуры, формы, буквы, тени, градиенты.

image

Думайте об этом как о Canvas API. Но вроде бы действительно продвинутой версии. Она понимает цветовые пространства, современную типографику, макет текста, ускорение графического процессора и тому подобное.

image

О, и это быстро. Действительно быстро. Если этого достаточно для Chrome, вероятно, этого будет достаточно и для вашего приложения.

Что я могу с этим сделать?


Много вещей! Пользовательские библиотеки виджетов пользовательского интерфейса и целые наборы инструментов, графики, диаграммы, визуализации, игры. Например, мы поигрались с реализацией java.awt.Graphics2D и запуском Swing поверх него похоже, все работает нормально.

Зачем выпускать отдельную графическую библиотеку? Чем это полезно?


Я не большой поклонник объединять все в одном месте. Вы никогда не сможете угадать все детали правильно кто-то всегда будет недоволен конкретным решением.

Независимые взаимозаменяемые библиотеки более гибкие. Филосовия Unix.

Что с остальной кусочками паззла?


Обе вещи находятся в разработке в JetBrains.

  • Для интеграции управления окнами и ОС есть Skiko. Она говорит, что это Skia для Kotlin, но она также реализует создание окон, события, пэкэджинг и все остальное. Она даже интегрируется с AWT и Swing.
  • А для инструментария пользовательского интерфейса есть Compose Desktop. Это форк Android Compose, декларативного UI-фреймворка, работающего в среде рабочего стола.


Но прелесть в том, что это даже не обязательно эти двое!

Не нравится AWT? Принесите свою собственную библиотеку окон.

Kotlin не подходит Вам? Используйте любой другой язык JVM.

Compose плохо справляется с нагрузкой? Молитесь, чтобы кто-нибудь написал альтернативу или написал что-то свое (извините, но хорошего решения пока нет. Это еще только начало).

И, конечно, если вы хотите развернуть свою собственную библиотеку окон или набор инструментов для виджетов пожалуйста, сделайте это! Мы надеемся, что это произойдет.

В заключение


Skija это часть более широкой картины. Прогресс пользовательского интерфейса Java был заблокирован некачественной Graphics2D. Но все меняется. Что из этого выйдет? Время покажет.

Пожалуйста, попробуйте Skija и поделитесь с нами своим мнением. Или, может быть, начните ей пользоваться мы были бы счастливы, если бы вы это сделали! Вот ссылка:

github.com/JetBrains/skija

14 ноября 2020 Обсуждение на HackerNews



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Опыт оптимизации вычислений через динамическую генерацию байт-кода JVM

19.08.2020 12:13:25 | Автор: admin
В своем небольшом проекте по моделированию случайных величин я столкнулся с проблемой низкой производительности вычисления математических выражений, вводимых пользователем, и долго искал разные способы ее решения: попробовал написать интерпретатор на С++ в надежде, что он будет быстрым, сочинил свой байт-код. Наиболее удачной идеей оказалась генерация классов JVM и их загрузка во время выполнения.

Узнав про KMath, я решил обобщить эту оптимизацию для множества математических структур и операторов, определенных на них.

KMath это библиотека для математики и компьютерной алгебры, активно использующая контекстно-ориентированное программирование в Kotlin. В KMath разделены математические сущности (числа, векторы, матрицы) и операции над ними они поставляются отдельным объектом, алгеброй, соответствующей типу операндов, Algebra<T>.

import scientifik.kmath.operations.*ComplexField {   (i pow 2) + Complex(1.0, 3.0)}

Таким образом, после написания генератора байт-кода, с учетом оптимизаций JVM можно получить быстрые расчеты для любого математического объекта достаточно определить операции над ними на Kotlin.

API


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

База всего API выражений интерфейс Expression<T>, а простейший способ его реализовать прямо определить метод invoke от данных параметров или, например, вложенных выражений. Подобная реализация была интегрирована в корневой модуль как справочная, хоть и самая медленная.

interface Expression<T> {   operator fun invoke(arguments: Map<String, T>): T}

Более продвинутые реализации уже основаны именно на MST. К ним относятся:

  • интерпретатор MST,
  • генератор классов по MST.

API для парсинга выражений из строки в MST уже доступно в dev-ветке KMath как и более или менее окончательный генератор кода JVM.

Перейдем к MST. Сейчас в MST представлены четыре вида узлов:

  • терминальные:
    • символы (то есть переменные)
    • числа;
  • унарные операции;
  • бинарные операции.



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



Для работы с MST, помимо языка выражения, еще есть алгебра например MstField:

import scientifik.kmath.ast.*import scientifik.kmath.operations.*import scientifik.kmath.expressions.*RealField.mstInField { number(1.0) + number(1.0) }() // 2.0

Результатом метода выше является реализация Expression<T>, при вызове вызывающая обход дерева, полученного при вычислении функции, переданной в mstInField.

Генерация кода


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

Генерация кода в kmath-ast это параметризованная сборка класса JVM. Входные данные MST и целевая алгебра, на выходе инстанс Expression<T>.

Соответствующий класс, AsmBuilder, и еще несколько extension-функций предоставляют методы для императивной сборки байт-кода поверх ObjectWeb ASM. С их помощью обход MST и сборка класса выглядят чисто и занимают менее 40 строк кода.

Рассмотрим сгенерированный класс для выражения 2*x, приведен декомпилированный из байт-кода исходник на Java:

package scientifik.kmath.asm.generated;import java.util.Map;import scientifik.kmath.asm.internal.MapIntrinsics;import scientifik.kmath.expressions.Expression;import scientifik.kmath.operations.RealField;public final class AsmCompiledExpression_1073786867_0 implements Expression<Double> {   private final RealField algebra;   public final Double invoke(Map<String, ? extends Double> arguments) {       return (Double)this.algebra.add(((Double)MapIntrinsics.getOrFail(arguments, "x")).doubleValue(), 2.0D);   }   public AsmCompiledExpression_1073786867_0(RealField algebra) {       this.algebra = algebra;   }}

Сначала здесь был сгенерирован метод invoke, в котором были последовательно расставлены операнды (т.к. они находятся глубже в дереве), потом вызов add. После invoke был записан соответствующий бридж-метод. Далее было записано поле algebra и конструктор. В некоторых случаях, когда константы нельзя просто положить в пул констант класса, записывается еще поле constants, массив java.lang.Object.

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

Оптимизация вызовов к Algebra


Чтобы вызвать операцию от алгебры, нужно передать ее ID и аргументы:

RealField { binaryOperation("+", 1.0, 1.0) } // 2.0

Однако такой вызов дорог по производительности: для того чтобы выбрать, какой метод вызвать, RealField выполнит сравнительно дорогую инструкцию tableswitch, а еще нужно помнить о боксинге. Поэтому, хотя все операции MST можно представить в такой форме, лучше делать прямой вызов:

RealField { add(1.0, 1.0) } // 2.0


Никакой особенной конвенции о маппинге ID операций к методам в реализациях Algebra<T> нет, поэтому пришлось вставить костыль, в котором вручную прописано, что +, например, соответствует методу add. Также есть поддержка для благоприятных ситуаций, когда для ID операции можно найти метод с таким же именем, нужным количеством аргументов и их типами.

private val methodNameAdapters: Map<Pair<String, Int>, String> by lazy {    hashMapOf(        "+" to 2 to "add",        "*" to 2 to "multiply",        "/" to 2 to "divide",...

private fun <T> AsmBuilder<T>.findSpecific(context: Algebra<T>, name: String, parameterTypes: Array<MstType>): Method? =    context.javaClass.methods.find { method ->...        nameValid && arityValid && notBridgeInPrimitive && paramsValid    }

Другая серьезная проблема это боксинг. Если мы посмотрим на Java-сигнатуры методов, которые получаются после компиляции того же RealField, увидим два метода:

public Double add(double a, double b)// $FF: synthetic method// $FF: bridge methodpublic Object add(Object var1, Object var2)

Конечно, легче не мучаться с боксингом и анбоксингом и вызвать бридж-метод: он тут появился из-за type erasure, чтобы правильно реализовывать метод add(T, T): T, тип T в дескрипторе которого на самом деле стерся до java.lang.Object.

Прямой вызов add от двух double тоже не идеален, потому что боксит возвращаемое значение (по этому поводу есть обсуждение в YouTrack Kotlin (KT-29460), но лучше вызывать именно его, чтобы в лучшем случае сэкономить два приведения типа входных объектов к java.lang.Number и их анбоксинга в double.

На решение этой проблемы ушло больше всего времени. Сложность здесь заключается не в создании вызовов к примитивному методу, а в том, что нужно сочетать на стеке и примитивные типы (как double), и их обертки (java.lang.Double, например), а в нужных местах вставлять боксинг (например, java.lang.Double.valueOf) и анбоксинг (doubleValue) здесь категорически не хватало работы с типами инструкций в байт-коде.

У меня были идеи навесить свою типизированную абстракцию поверх байт-кода. Для этого мне пришлось глубже разобраться в API ObjectWeb ASM. В итоге я обратился к бэкенду Kotlin/JVM, подробно изучил класс StackValue (типизированный фрагмент байт-кода, который в итоге приводит к получению какого-то значения на стеке операндов), разобрался с утилитой Type, которая позволяет удобно оперировать с типами, доступными в байт-коде (примитивы, объекты, массивы), и переписал генератор с ее использованием. Проблема, нужно ли боксить или анбоксить значение на стеке, решилась сама собой путем добавления ArrayDeque, хранящего типы, которые ожидаются следующим вызовом.

  internal fun loadNumeric(value: Number) {        if (expectationStack.peek() == NUMBER_TYPE) {            loadNumberConstant(value, true)            expectationStack.pop()            typeStack.push(NUMBER_TYPE)        } else ...?.number(value)?.let { loadTConstant(it) }            ?: error(...)    }

Выводы


В итоге мне удалось сделать генератор кода с помощью ObjectWeb ASM для вычисления выражений MST в KMath. Прирост производительности по сравнению с простым обходом MST зависит от количества узлов, так как байт-код линеен и в итоге не тратит время на выбор узла и рекурсию. Например, для выражения с 10 узлами разница во времени между вычислением с помощью сгенерированного класса и интерпретатора составляет от 19 до 30%.

Изучив проблемы, с которыми я столкнулся, я сделал следующие выводы:

  • нужно сразу изучить возможности и утилиты ASM они сильно упрощают разработку и делают код читаемым (Type, InstructionAdapter, GeneratorAdapter);
  • нет смысла тратить время на подсчет MaxStack самостоятельно, если это не критично для производительности, есть ClassWriter COMPUTE_MAXS и COMPUTE_FRAMES;
  • очень полезно изучать бэкенды компиляторов реальных языков;
  • следует разбираться в синтаксисе дескрипторов и, в частности, сигнатур, чтобы не ошибаться при использовании дженериков;
  • и наконец, далеко не во всех случаях нужно лезть настолько глубоко есть более удобные способы работать с классами в рантайме, например, ByteBuddy и cglib.

Cпасибо, что прочитали.

Авторы статьи:

Ярослав Сергеевич Постовалов, МБОУ Лицей 130 им. академика Лаврентьева, участник лаборатории математического моделирования под руководством Войтишека Антона Вацлавовича

Татьяна Абрамова, исследователь лаборатории методов ядерно-физических экспериментов в JetBrains Research.
Подробнее..

Recovery mode Лучший язык программирования

24.02.2021 10:16:44 | Автор: admin

Этот вопрос на первый взгляд звучит вызывающе, едва ли не табуирован, на него часто всплывает заезженный ответ, что все субъективно и зависит от задачи. Так ли это на самом деле?
Возвав к логике и здравому смыслу, попытаемся выявить наиболее объективные критерии сравнения языков программирования. Кто не боится разочароваться, увидев свой любимый язык заклейменным и преданным позору, давайте рискнем порассуждать, что лучше подходит для индустриальной разработки?

Принципы отбора

Экосистем много, начинать с выбранных достоинств какой-то одной глупо, поэтому действовать будем методом исключения по ясным пунктам. Все сравнения опираются на объективные бенчмарки.

Поддерживаемость

Прежде всего, язык должен быть достаточно мейнстримным, чтобы проект на нем был поддерживаемым. Сразу выбрасываем за борт всю экзотику и функциональщину вроде Haskell, Elixir, Nim, Erlang умирающий Ruby туда же. По этой же причине отбрасываем и всевозможные языки закрытых (Swift) и тем более окукленных по паспорту (1C например) экосистем.

Типизация

Общая практика в индустрии показывает что слабая типизация однозначно вредит читаемости, поддерживаемости и порождает большое количество ошибок, поэтому выкидываем JavaScript и PHP. Далее, замечаем что динамическая типизация ухудшает скорость работы, а варианты компиляции традиционно динамических языков, отличаются плохой поддерживаемостью, и выглядят скорее как извращение приделанное сбоку костылями, поэтому за бортом остаются и строго-типизированные, но динамические Python и TypeScript.

В сухом остатке

У нас остались мейнстримные, строго-статически-типизированные C#, Java, C++, C и восходящие в последнее время к ним Kotlin, Go и Rust. Не каждый пожелает (а еще меньше смогут) использовать C++ в большом проекте, поэтому, несмотря на лучшие пока показатели по скорости, отложим его вместе с более низкоуровневым Си для узких мест.

Java vs C#

C# и Java близнецы. Экосистемы обоих языков на данный момент открыты и очень развиты. Java здесь выходит вперед за счет большего количества инструментов, библиотек, конкуренции и вариантов выбора. Но C# более стройный синтаксически, в нем исправлены известные проблемы Java (слабые дженерики пропадающие на этапе компиляции, отсутствие пользовательских значимых типов на стеке, сочетаемость значимых и ссылочных типов как List). Кроме того у .NET в целом лучшие показатели по скорости и памяти.

Kotlin

Долгое время C# выглядел практически идеальным по кроссплатформености, высокоуровневости абстракций и скорости приближающейся местами к С++. Однако практика с Kotlin показала, что он превосходит C# и по концепциям, и по синтаксической стройности. И вот почему. Если C# идет по пути добавления в ядро языка все новых и новых абстракций и ключевых слов, то в Kotlin весь синтаксический "сахар" базируется на встраиваемых лямбдах и функциях области видимости и вынесен в стандартную библиотеку. В чем здесь преимущество? Почти любую фичу, казалось бы языка, в Kotlin можно прочитать в стандартной библиотеке и понять как любой другой код. Kotlin, правда, немного уступает C# по скорости, но лишь потому что компилируется в байт-код Java.

Go?

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

Rust?

Начать следует с того что у Rust порог входа выше чем у C++, уже потому что на C++ сразу можно писать как на любом другом высокоуровневом ЯП, предоставив стандартной библиотеке разруливать работу с памятью. Rust же не даст спрятаться от решения вопросов безопасности и размещения в памяти и из-за этой необходимости, он (внезапно!) превращается в достаточно низкоуровневый язык, сравнимый скорее даже с Си, чем с C++.

Вывод

Мы по возможности непредвзято рассмотрели объективные факторы способствующие индустриальной разработке. Вывод из них все же неизбежно будет субъективен. Поэтому привествуются альтернативные выводы!
Итак, по моему мнению, на текущий момент, для написания большинства кроссплатформенных приложений удобнее всего использовать Kotlin, дополняя его C++ в тех случаях когда требуется скорость. Kotlin прекрасно подходит для бекендов, язык экосистемы Android по умолчанию, без проблем компилируется в JS и WebAssembly для браузеров, с небольшими приседаниями может быть использован для iOS, а с помощью jpackage легко подготовить самодостаточный исполняемый файл для Windows, macOS, Linux в "родном" формате.

Подробнее..

Recovery mode Почему Kotlin лучше Java?

24.05.2021 08:13:54 | Автор: admin

Это ответ на переведенную публикацию Почему Kotlin хуже, чем Java?. Поскольку исходная аргументация опирается всего на два примера, то не теряя времени пройдем по этим недостаткам Kotlin.

Проприетарные метаданные?

изрядное количество подробностей внутренней работы kotlinc скрыто внутри сгенерированных файлов классов...без IDEA Kotlin немедленно умрет

Это не проприетарный код, а просто способ для компилятора дописать дополнительные данные в жестко заданном формате .class-файлов, который ранее был заточен только под javac. Метаданные нужны для рефлексии и их можно удалить при компиляции. Исходный код метаданных открыт и общедоступен.

Kotlin будет отставать?

Вкратце, посыл исходной статьи таков что Kotlin был иновационным, но Java добавит все те же языковые возможности, только продуманее и лучше, и уже Kotlin-вариант выпадет из мейнстрима.
В качестве примера автор приводитinstanceof:

В Kotlin сделали примерно так:
if (x instanceof String) {
// теперь x имеет тип String! System.out.println(x.toLowerCase());
}

Но в Java версии 16+ стало так:
if (x instanceof String y) {
System.out.println(y.toLowerCase());
}

Получается, что оба языка имеют способ обработать описанный сценарий, но разными способами. Я уверен, что если бы мог вдавить огромную кнопку сброс, разработать Kotlin с нуля и снова выпустить сегодня бета-версию, то в Kotlin было бы сделано так же, как сейчас в Java. Учитывая, что синтаксис Java более мощный: мы можем сделать с ним намного больше, чем просто проверить тип (например, деконструировать типы-значения).
...
Ему придётся опираться только на себя и перестать рекламировать доступность всех преимуществ Java и её экосистемы. Пример с instanceof уже демонстрирует, почему я думаю, что Kotlin не будет лучше Java: почти каждая новая фича, которая появилась в Java недавно или вот-вот появится (в смысле, имеет активный JEP и обсуждается в рассылках) выглядит более продуманной, чем любая фича Kotlin.

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

when(val v = calcValue()) {  is String -> processString(v)}

Здесь в одной области можно проверить сразу несколько типов вместе со значениями значения, без создания дополнительных переменных. Попробуйте повторить в Java c if/instanceof/switch:

when(val v = calcValue()) {  is String -> processString(v)  42 -> prosess42()  is Int -> processInt(v)  else -> processElse(v)}

Остается разобраться с аргументом, что Kotlin, якобы, придется что-то делать с изменениями вносимыми Java, адаптироваться или расходиться, что это, якобы, создает проблему развития для Kotlin.

Прямо сейчас Kotlin оседлал волну успеха, но со временем жизнь его будет тем тяжелее, чем шире будет зазор между ним и Java, и чем сложнее будет преодолеть этот зазор.

Здесь автор путает местами причину и следствие, пытаясь выдать Kotlin за догоняющий язык. На самом деле, адаптация новых возможностей это в первую очередь проблема именно для Java. Именно новые возможности таких конкурентов как C# и Kotlin подстегивают Java изменяться, и именно для Java сложнее это сделать по причине изначально тяжеловесного и сложившегося за десятилетия синтаксиса, не предусматривавшего появление новых, функциональных возможностей. В Java-парадигме для них попросту осталось не так много места, где их можно прикрутить к синтаксису, и именно поэтому они выглядят чужеродно обременительно для восприятия.

Нативная поддержка null, которая греет душу любого котлиниста, легко заменяется в Java на обёртку из Optional.ofNullable. Data-объекты могут быть заменены более богатым функционалом record.

Все догоняющие возможности Java содержат фатальные изъяны по умолчанию, все больше заставляют прибегать к солгашениям, а не к дизайну языка. Вместо Optional можно передать null, а record ничуть не более богатый чем data class.

Как думаете, сохранит Kotlin свою популярность через пять лет и почему?

Некоторые в комментариях вспомнили историю Scala и Java. Но есть и другая история, история того что сделал С++ со старым Си.
Java несомненно останется там где нужно поддерживать старые решения. Однако новые решения для все больше писаться на Kotlin, пока он не станет языком по умолчанию, как это уже произошло в Android экосистеме, и прямо сейчас происходит для backend разработки в jvm экосистеме. Kotlin не просто лучше, он дает думать в другой парадигме, открыт для новых возможностей, страхует от многих ошибок на этапе компиляции.

Подробнее..

C vs Kotlin

01.06.2021 08:12:59 | Автор: admin

Когда речь заходит о сахаре и модных фичах в языках программирования, среди первых вариантов на ум приходят C# и Kotlin. Поскольку эти два языка занимают схожие ниши, то есть, строго типизированы, обладают сборкой мусора, кроссплатформенны, применяются как на бекенде, так и в мобильной разработке, то сегодня мы попытаемся сравнить их синтаксические возможности и устроить небольшое голосование. Чтобы сравнение прошло честно, будем рассматривать последние версии обоих языков. Оговорюсь о своей непредвзятости: мне одинаково нравятся оба языка, они находятся в непрерывном развитии и не отстают друг от друга. Эта статья является сравнительной, а не обучающей, поэтому некоторые заурядные синтаксические возможности могут быть опущены.

Начнем с точки входа

В C# эту роль играет статический метод Main или top-level entry point, например

using static System.Console;WriteLine("Ok");

В Kotlin нужна функция main

fun main() = println("Ok")

По этим небольшим двум примерам в первую очередь заметно, что в Kotlin можно опускать точку с запятой. При более глубоком анализе видим, что в C#, несмотря на лаконичность показательного entry point, статические методы в остальных файлах по прежнему требуется оборачивать в класс и явно импортировать из него (using static System.Console), а Kotlin идет дальше и разрешает создавать полноценные функции.

Обьявление переменных

В C# тип пишется слева, а для создания экземпляра используется ключевое слово new. В наличии есть специальное слово var, которым можно заменить имя типа слева. При этом переменные внутри методов в C# остаются подвержены повторному присваиванию.

Point y = new Point(0, 0); var x = new Point(1, 2);x = y; // Нормально

В Kotlin типы пишутся справа, однако их можно опускать. Помимо var, доступен и val который не допускает повторного присваивания. При создании экземляров не нужно указывать new.

val y: Point = Point(0, 0)val x = Point(1, 2)x = y // Ошибка компиляции!

Работа с памятью

В C# нам доступны значимые (обычно размещаются на стеке) и ссылочные (обычно размещаются в куче) типы. Такая возможность позволяет применять низкоуровневые оптимизации и сокращать расход оперативной памяти. Для объектов структур и классов оператор '==' будет вести себя по разному, сравнивая значения или ссылки, впрочем это поведение можно изменить благодаря перегрузке. При этом на структуры накладываются некоторые ограничения связанные с наследованием.

struct ValueType {} // структура, экземпляры попадут на стекclass ReferenceType {} // ссылочный тип, экземпляры будут в куче

Что до Kotlin, то у него нет никакого разделения по работе с памятью. Сравнение '==' всегда происходит по значению, для сравнения по ссылке есть отдельный оператор '==='. Объекты практически всегда размещаются в куче, и только для некоторых базовых типов, например Int, Char, Double, компилятор может применить оптизмизации сделав их примитивами jvm и разместив на стеке, что никак не отражается на их семантике в синтаксисе. Складывается впечатление что рантайм и работа с памятью это более сильная сторона .NET в целом.

Null safety

В C# (начиная с 8ой версии) есть защита от null. Однако ее можно явно обойти с помощью оператора !

var legalValue = maybeNull!;// если legalValue теперь null, // то мы получим exception при первой попытке использования

В Kotlin для использования null нужно использовать два восклицания, но есть и другое отличие

val legalValue = maybeNull!! // если maybeNull == null, // то мы получим exception сразу же

Свойства классов

В C# доступна удобная абстракция вместо методов get/set, то есть всем известные свойства. При этом традиционные поля остаются доступны.

class Example{     // Вычислено заранее и сохранено в backing field  public string Name1 { get; set; } = "Pre-calculated expression";    // Вычисляется при каждом обращении  public string Name2 => "Calculated now";    // Традиционное поле  private const string Name3 = "Field"; }

В Kotlin полей нет вообще, по умолчанию доступны только свойства. При этом в отличие от C# public это область видимости по умолчанию, поэтому это ключевое слово рекомендукется опускать. Для разницы между свойствами, допускающими set и без него, используются все те же ключевые var/val

class Example {    // Вычислено заранее и сохранено в backing field  val name1 = "Pre-calculated expression"    // Вычисляется при каждом обращении  val name2 get() = "Calculated now"}

Классы данных

В C# достаточно слова record чтобы создать класс для хранения данных, он будет обладать семантикой значимых типов в сравнении, однако по прежнему остается ссылочным (будет размещаться в куче):

class JustClass{  public string FirstName { init; get; }  public string LastName { init; get; }}record Person(string FirstName, string LastName);...   Person person1 = new("Nancy", "Davolio");Person person2 = person1 with { FirstName = "John" };

В Kotlin нужно дописать ключевое слово data к слову class

class JustClass(val firstName: String, val lastName: String)data class Person(val firstName: String, val lastName: String)...val person1 = Person("Nancy", "Davolio")val person2 = person1.copy(firstName = "John")

Расширения типов

В C# такие типы должны находиться в отдельном статическом классе и принимать вызывающий первым аргументом, помеченным this

static class StringExt{  public static Println(this string s) => System.Console.WriteLine(s)      public static Double(this string s) => s + s}

В Kotlin расширямый тип должен находиться слева от метода, который можно разместить в любом месте. При этом расширить тип можно не только методом, но и свойством

fun String.println() = println(this)fun String.double get() = this * 2

Лямбда выражения

В C# для них есть специальный оператор =>

numbers.Any(e => e % 2 == 0);numbers.Any(e =>   {    // объемная логика ...    return calculatedResult;  })

В Kotlin лямбды органично вписываются в Си-подобный синтаксис, кроме того во многих случаях компилятор заинлайнит их вызовы прямо в используемый метод. Это позволяет создавать эффективные и красивые DSL (Gradle + Kotlin например).

numbers.any { it % 2 == 0 }numbers.any {  // объемная логика ...  return calculatedResult}

Условия и шаблоны

У C# есть очень мощный pattern matching c условиями (пример из документации)

static Point Transform(Point point) => point switch{  { X: 0, Y: 0 }                    => new Point(0, 0),  { X: var x, Y: var y } when x < y => new Point(x + y, y),  { X: var x, Y: var y } when x > y => new Point(x - y, y),  { X: var x, Y: var y }            => new Point(2 * x, 2 * y),};

У Kotlin есть аналогичное switch выражение when, которое, несмотря на наличие возможности сопоставления с образцом, не может одновременно содержать деконструкции и охранных условий, но благодаря лаконичному синтаксису можно выкрутиться:

fun transform(p: Point) = when(p) {  Point(0, 0) -> Point(0, 0)  else -> when {    x > y     -> Point(...)    x < y     -> Point(...)    else      -> Point(...)  }}// или такfun transform(p: Point) = when {  p == Point(0, 0) -> Point(0, 0)  p.x < y          -> Point(p.x + y, p.y)  p.x > y          -> Point(p.x - p.y, p.y)  else             -> Point(2 * p.x, 2 * p.y)}

Подводя итоги

Уложить в одной статье все отличия обоих языков практически нереально. Однако кое какие выводы сделать уже можем. Заметно что Kotlin-way скорее в том чтобы минимизировать количество ключевых слов, реализуя весь сахар поверх базового синтаксиса, а C# стремится стать более удобным увеличивая количество доступных выражений на уровне самого языка. У Kotlin преимущество в том что его создатели могли оглядываться на удачные фичи C# и лаконизировать их, а C# выигрывает за счет мощной поддержки в лице Microsoft и лучшего рантайма.

Подробнее..

Java HotSpot JIT компилятор устройство, мониторинг и настройка (часть 1)

07.01.2021 02:21:44 | Автор: admin
JIT (Just-in-Time) компилятор оказывает огромное влияние на быстродействие приложения. Понимание принципов его работы, способов мониторинга и настройки является важным для каждого Java-программиста. В цикле статей из двух частей мы рассмотрим устройство JIT компилятора в HotSpot JVM, способы мониторинга его работы, а также возможности его настройки. В этой, первой части мы рассмотрим устройство JIT компилятора и способы мониторинга его работы.

AOT и JIT компиляторы


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

Существуют компилируемые языки программирования, такие как C и C++. Программы, написанные на этих языках, распространяются в виде машинного кода. После того, как программа написана, специальный процесс Ahead-of-Time (AOT) компилятор, обычно называемый просто компилятором, транслирует исходный код в машинный. Машинный код предназначен для выполнения на определенной модели процессора. Процессоры с общей архитектурой могут выполнять один и тот же код. Более поздние модели процессора как правило поддерживают инструкции предыдущих моделей, но не наоборот. Например, машинный код, использующий AVX инструкции процессоров Intel Sandy Bridge не может выполняться на более старых процессорах Intel. Существуют различные способы решения этой проблемы, например, вынесение критичных частей программы в библиотеку, имеющую версии под основные модели процессора. Но часто программы просто компилируются для относительно старых моделей процессоров и не используют преимущества новых наборов инструкций.

В противоположность компилируемым языкам программирования существуют интерпретируемые языки, такие как Perl и PHP. Один и тот же исходный код при таком подходе может быть запущен на любой платформе, для которой существует интерпретатор. Минусом этого подхода является то, что интерпретируемый код работает медленнее, чем машинный код, делающий тоже самое.

Язык Java предлагает другой подход, нечто среднее между компилируемыми и интерпретируемыми языками. Приложения на языке Java компилируются в промежуточный низкоуровневый код байт-код (bytecode).
Название байт-код было выбрано потому, что для кодирования каждой операции используется ровно один байт. В Java 10 существует около 200 операций.

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

HotSpot JIT компилятор


В различных реализациях JVM JIT компилятор может быть реализован по-разному. В данной статье мы рассматриваем Oracle HotSpot JVM и ее реализацию JIT компилятора. Название HotSpot происходит от подхода, используемого в JVM для компиляции байт-кода. Обычно в приложении только небольшие части кода выполняются достаточно часто и производительность приложения в основном зависит от скорости выполнения именно этих частей. Эти части кода называются горячими точками (hot spots), их и компилирует JIT компилятор. В основе этого подхода лежит несколько суждений. Если код будет исполнен всего один раз, то компиляция этого кода пустая трата времени. Другая причина это оптимизации. Чем больше раз JVM исполняет какой либо код, тем больше статистики она накапливает, используя которую можно сгенерировать более оптимизированный код. К тому же компилятор разделяет ресурсы виртуальной машины с самим приложением, поэтому ресурсы затраченные на профилирование и оптимизацию могли бы быть использованы для исполнения самого приложения, что заставляет соблюдать определенный баланс. Единицей работы для HotSpot компилятора является метод и цикл.
Единица скомпилированного кода называется nmethod (сокращение от native method).

Многоуровневая компиляция (tiered compilation)


На самом деле в HotSpot JVM существует не один, а два компилятора: C1 и C2. Другие их названия клиентский (client) и серверный (server). Исторически C1 использовался в GUI приложениях, а C2 в серверных. Отличаются компиляторы тем, как быстро они начинают компилировать код. C1 начинает компилировать код быстрее, в то время как C2 может генерировать более оптимизированный код.

В ранних версиях JVM приходилось выбирать компилятор, используя флаги -client для клиентского и -server или -d64 для серверного. В JDK 6 был внедрен режим многоуровневой компиляции. Грубо говоря, его суть заключается в последовательном переходе от интерпретируемого кода к коду, сгенерированному компилятором C1, а затем C2. В JDK 8 флаги -client, -server и -d64 игнорируются, а в JDK 11 флаг -d64 был удален и приводит к ошибке. Выключить режим многоуровневой компиляции можно флагом -XX:-TieredCompilation.

Существует 5 уровней компиляции:
  • 0 интерпретируемый код
  • 1 C1 с полной оптимизацией (без профилирования)
  • 2 C1 с учетом количества вызовов методов и итераций циклов
  • 3 С1 с профилированием
  • 4 С2

Типичные последовательности переходов между уровнями приведены в таблице.
Последовательность
Описание
0-3-4 Интерпретатор, уровень 3, уровень 4. Наиболее частый случай.
0-2-3-4 Случай, когда очередь уровня 4 (C2) переполнена. Код быстро компилируется на уровне 2. Как только профилирование этого кода завершится, он будет скомпилирован на уровне 3 и, наконец, на уровне 4.
0-2-4 Случай, когда очередь уровня 3 переполнена. Код может быть готов к компилированию на уровне 4 все еще ожидая своей очереди на уровне 3. Тогда он быстро компилируется на уровне 2 и затем на уровне 4.
0-3-1 Случай простых методов. Код сначала компилируется на уровне 3, где становится понятно, что метод очень простой и уровень 4 не сможет скомпилировать его оптимальней. Код компилируется на уровне 1.
0-4 Многоуровневая компиляция выключена.

Code cache


Машинный код, скомпилированный JIT компилятором, хранится в области памяти называемой code cache. В ней также хранится машинный код самой виртуальной машины, например, код интерпретатора. Размер этой области памяти ограничен, и когда она заполняется, компиляция прекращается. В этом случае часть горячих методов так и продолжит выполняться интерпретатором. В случае переполнения JVM выводит следующее сообщение:

Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full.
Compiler has been disabled.

Другой способ узнать о переполнении этой области памяти включить логирование работы компилятора (как это сделать обсуждается ниже).
Code cache настраивается также как и другие области памяти в JVM. Первоначальный размер задаётся параметром -XX:InitialCodeCacheSize. Максимальный размер задается параметром -XX:ReservedCodeCacheSize. По умолчанию начальный размер равен 2496 KB. Максимальный размер равен 48 MB при выключенной многоуровневой компиляции и 240 MB при включенной.

Начиная с Java 9 code cache разделен на 3 сегмента (суммарный размер по-прежнему ограничен пределами, описанными выше):
  • JVM internal (non-method code). Содержит машинный код, относящийся к самой JVM, например, код интерпретатора. Размер этого сегмента зависит от количества потоков компиляции. На машине с четырьмя ядрами по умолчанию его размер составляет около 5.5 MB. Задать произвольный размер сегмента можно параметром -XX:NonNMethodCodeHeapSize.
  • Profiled code. Содержит частично оптимизированный машинный код с коротким временем жизни. Размер этого сегмента равен половине пространства оставшегося после выделения non-method code сегмента. По умолчанию это 21.2 MB при выключенной многоуровневой компиляции и 117.2 MB при включенной. Задать произвольный размер можно параметром -XX:ProfiledCodeHeapSize.
  • Non-profiled code. Содержит полностью оптимизированный код с потенциально долгим временем жизни. Размер этого сегмента равен половине пространства оставшегося после выделения non-method code сегмента. По умолчанию это 21.2 MB при выключенной многоуровневой компиляции и 117.2 MB при включенной. Задать произвольный размер можно параметром -XX: NonProfiledCodeHeapSize.

Мониторинг работы компилятора


Включить логирование процесса компиляции можно флагом -XX:+PrintCompilation (по умолчанию он выключен). При установке этого флага JVM будет выводить в стандартный поток вывода (STDOUT) сообщение каждый раз после компиляции метода или цикла. Большинство сообщений имеют следующий формат: timestamp compilation_id attributes tiered_level method_name size deopt.
Поле timestamp это время со старта JVM.
Поле compilation_id это внутренний ID задачи. Обычно он последовательно увеличивается в каждом сообщении, но иногда порядок может нарушаться. Это может произойти в случае, если существует несколько потоков компиляции работающих параллельно.
Поле attributes это набор из пяти символов, несущих дополнительную информацию о скомпилированном коде. Если какой-то из атрибутов не применим, вместо него выводится пробел. Существуют следующие атрибуты:
  • % OSR (on-stack replacement);
  • s метод является синхронизированным (synchronized);
  • ! метод содержит обработчик исключений;
  • b компиляция произошла в блокирующем режиме;
  • n скомпилированный метод является оберткой нативного метода.

Аббревиатура OSR означает on-stack replacement. Компиляция это асинхронный процесс. Когда JVM решает, что метод необходимо скомпилировать, он помещается в очередь. Пока метод компилируется, JVM продолжает исполнять его интерпретатором. В следущий раз, когда метод будет вызван снова, будет выполняться его скомпилированная версия. В случае долгого цикла ждать завершения метода нецелесообразно он может вообще не завершиться. JVM компилирует тело цикла и должна начать исполнять его скомпилированную версию. JVM хранит состояние потоков в стеке. Для каждого вызываемого метода в стеке создается новый объект Stack Frame, который хранит параметры метода, локальные переменные, возвращаемое значение и другие значения. Во время OSR создается новый объект Stack Frame, который заменяет собой предыдущий.

Источник: The Java HotSpotTM Virtual Machine Client Compiler: Technology and Application
Атрибуты s и ! в пояснении я думаю не нуждаются.
Атрибут b означает, что компиляция произошла не в фоне, и не должен встречаться в современных версиях JVM.
Атрибут n означает, что скомпилированный метод является оберткой нативного метода.
Поле tiered_level содержит номер уровня, на котором был скомпилирован код или может быть пустым, если многоуровневая компиляция выключена.
Поле method_name содержит название скомпилированного метода или название метода, содержащего скомпилированный цикл.
Поле size содержит размер скомпилированного байт-кода, не размер полученного машинного кода. Размер указан в байтах.
Поле deopt появляется не в каждом сообщении, оно содержит название проведенной деоптимизации и может содержать такие сообщения как made not entrant и made zombie.
Иногда в логе могут появиться записи вида: timestamp compile_id COMPILE SKIPPED: reason. Они означают, что при компиляции метода что-то пошло не так. Есть случаи, когда это ожидаемо:
  • Code cache filled необходимо увеличть размер области памяти code cache.
  • Concurrent classloading класс был модифицирован во время компиляции.

Во всех случаях, кроме переполнения code cache, JVM попробует повторить компиляцию. Если этого не происходит, можно попробовать упростить код.

В случае, если процесс был запущен без флага -XX:+PrintCompilation, взглянуть на процесс компиляции можно с помощью утилиты jstat. У jstat есть два параметра для вывода информации о компиляции.
Параметр -compiler выводит сводную информацию о работе компилятора (5003 это ID процесса):
% jstat -compiler 5003
Compiled Failed Invalid Time FailedType FailedMethod
206 0 0 1.97 0

Эта команда также выводит количество методов, компиляция которых завершилась ошибкой и название последнего такого метода.
Параметр -printcompilation выводит информацию о последнем скомпилированном методе. В сочетании со вторым параметром периодом повторения операции, можно наблюдать процесс компиляции с течением времени. В следующем примере команда -printcompilation выполняется каждую секунду (1000 мс):
% jstat -printcompilation 5003 1000
Compiled Size Type Method
207 64 1 java/lang/CharacterDataLatin1 toUpperCase
208 5 1 java/math/BigDecimal$StringBuilderHelper getCharArray

Планы на вторую часть


В следующей части мы рассмотрим пороговые значения счетчиков при которых JVM запускает компиляцию и как можно их поменять. Мы также рассмотрим как JVM выбирает количество потоков компилятора, как можно его поменять и в каких случаях стоит это делать. И наконец, кратко рассмотрим некоторые из оптимизаций выполняемых JIT компилятором.

Список литературы и ссылки


Подробнее..

Java HotSpot JIT компилятор устройство, мониторинг и настройка (часть 2)

11.01.2021 02:05:19 | Автор: admin

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

Счетчики вызовов методов и итераций циклов

Главным фактором влияющим на решение JVM о компиляции какого-либо кода является частота его исполнения. Решение принимается на основе двух счетчиков: счетчика количества вызовов метода и счетчика количества итераций циклов в методе.

Когда JVM исполняет какой-либо метод, она проверяет значения этих двух счетчиков, и принимает решение о необходимости его компиляции. У этого типа компиляции нет официального названия, но часто его называют стандартной компиляцией.

Аналогично, после каждой итерации цикла проверяется значение счетчика цикла и принимается решение о необходимости его компиляции.

При выключенной многоуровневой компиляции стандартная компиляция управляется параметром -XX:CompileThreshold. Значением по умолчанию является 10000. Несмотря на то, что параметр всего один, превышение порога определяется суммой значений двух счетчиков. В настоящее время этот флаг ни на что не влияет (многоуровневая компиляция включена по умолчанию), но знать о нем полезно, ведь существует довольно много унаследованных систем.

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

Последнее утверждение довольно интересно. Ведь если программа исполняется бесконечно, не должен ли весь ее код в конце концов скомпилироваться? На самом деле не должен, поскольку значения счетчиков не только увеличиваются при каждом вызове метода или итерации цикла, но и периодически уменьшаются. Таким образом, они являются отражением текущего нагрева метода или цикла.

Получается, что до внедрения многоуровневой компиляции методы, выполнявшиеся довольно часто, но недостаточно часто, чтобы превысить порог, никогда бы не были скомпилированы. В настоящее время такие методы будут скомпилированы компилятором C1, хотя, возможно, их производительность была бы выше, будь они скомпилированы компилятором C2. При желании можно поиграть параметрами -XX:Tier3InvocationThreshold (значение по умолчанию 200) и -XX:Tier4InvocationThreshold (значение по умолчанию 5000), но вряд ли в этом есть большой практический смысл. Такие же параметры (-XX:TierXBackEdgeThreshold) существуют и для задания пороговых значений счетчиков циклов.

Потоки компиляции

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

Компиляторы C1 и C2 имеют собственные очереди, каждая из которых может обрабатывается несколькими потоками. Существует специальная формула для вычисления количества потоков в зависимости от количества ядер. Некоторые значения приведены в таблице:

Количество ядер

Количество потоков C1

Количество потоков C2

1

1

1

2

1

1

4

1

2

8

1

2

16

2

6

32

3

7

64

4

8

128

4

10

Задать произвольное количество потоков можно параметром -XX:CICompilerCount. Это общее количество потоков компиляции, которое будет использовать JVM. При включенной многоуровневой компиляции одна треть из них (но минимум один) будут отданы компилятору C1, остальные достанутся компилятору C2. Значением по умолчанию для этого флага является сумма потоков из таблицы выше. При выключенной многоуровневой компиляции все потоки достанутся компилятору C2.

В каком случае имеет смысл менять настройки по умолчанию? Ранние версии Java 8 (до update 191) при запуске в Docker контейнере не корректно определяли количество ядер. Вместо количества ядер, выделенных контейнеру определялось количество ядер на сервере. В этом случае есть смысл задать количество потоков вручную, исходя из значений, приведенных в таблице выше.

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

Еще один параметр, влияющий на количество потоков компиляции - это -XX:+BackgroundCompilation. Его значение по умолчанию - true. Он означает, что компиляция должна происходить в асинхронном режиме. Если установить его в false, каждый раз при наличии кода, который необходимо скомпилировать, JVM будет ожидать завершения компиляции, прежде чем этот код исполнить.

Оптимизации

Как мы знаем, JVM использует результаты профилирования методов в процессе компиляции. JVM хранит данные профиля в объектах, называемых method data objects (MDO). Объекты MDO используются интерпретатором и компилятором C1 для записи информации, которая затем используется для принятия решения о том, какие оптимизации возможно применить к компилируемому коду. Объекты MDO хранят информацию о вызванных методах, выбранных ветвях в операторах ветвления, наблюдаемых типах в точках вызова. Как только принято решение о компиляции, компилятор строит внутреннее представление компилируемого кода, которое затем оптимизируется. Компиляторы способны проводить широкий набор оптимизаций, включающий:

  • встраивание (inlining);

  • escape-анализ (escape-analysis);

  • размотка (раскрутка) цикла (loop unrolling);

  • мономорфная диспетчеризация (monomorphic dispatch);

  • intrinsic-методы.

Встраивание

Встраивание - это копирование вызываемого метода в место его вызова. Это позволяет устранить накладные расходы, связанные с вызовом метода, такие как:

  • подготовка параметров;

  • поиск по таблице виртуальных методов;

  • создание и инициализация объекта Stack Frame;

  • передача управления;

  • опциональный возврат значения.

Встраивание является одной из оптимизаций, выполняемых JVM в первую очередь, оно включено по умолчанию. Отключить его можно флагом -XX:-Inline, хотя делать этого не рекомендуется. JVM принимает решение о необходимости встраивания метода на основе нескольких факторов, некоторые из которых приведены ниже.

  • Размер метода. Горячие методы являются кандидатами для встраивания если их размер меньше 325 байт (или меньше размера заданного параметром -XX:MaxFreqInlineSize). Если метод вызывается не так часто, он является кандидатом для встраивания только если его размер меньше 35 байт (или меньше размера заданного параметром -XX:MaxInlineSize).

  • Позиция метода в цепочке вызовов. Не подлежат встраиванию методы с позицией больше 9 (или значения заданного параметром -XX:MaxInlineLevel).

  • Размер памяти, занимаемой уже скомпилированными версиями метода в code cache. Встраиванию не подлежат методы, скомпилированные на последнем уровне, версии которых занимают более 1000 байт при выключенной многоуровневой компиляции и 2000 байт при включенной (или значения заданного параметром -XX:InlineSmallCode).

Escape-анализ

Escape-анализ - это техника анализа кода, которая позволяет определить пределы достижимости какого-либо объекта. Например, escape-анализ может использоваться для определения является ли объект созданный внутри метода достижимым за пределами области видимости метода. Сам по себе escape-анализ не является оптимизацией, но оптимизации могут выполняются по его результатам.

Предотвращение выделений памяти в куче

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

Блокировки и escape-анализ

JVM способна использовать результаты escape-анализа для оптимизации производительности блокировок. Это относится только к блокировкам с помощью ключевого слова synchronized, блокировки из пакета java.util.concurrent таким оптимизациям не подвержены. Возможными оптимизации приведены ниже.

  • Удаление блокировок с объектов недостижимых за пределами области видимости (lock elision).

  • Объединение последовательных синхронизированных секций, использующих один и тот же объект синхронизации (lock coarsening). Выключить эту оптимизацию можно флагом -XX:-EliminateLocks.

  • Определение и удаление вложенных блокировок на одном и том же объекте (nested locks). Выключить эту оптимизацию можно флагом -XX:-EliminateNestedLocks.

Ограничения escape-анализа

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

Еще одно ограничение заключается в том, что частичный escape-анализ не поддерживается. Если объект выходит за пределы области видимости метода хотя бы по одной из веток, оптимизация по предотвращению создания объекта в куче не применима. Пример подобного кода приведен ниже.

for (int i = 0; i < 100_000_000; i++) {    Object mightEscape = new Object(i);    if (condition) {        result += inlineableMethod(mightEscape);    } else {        result += tooBigToInline(mightEscape);    }}

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

if (condition) {        Object mightEscape = new Object(i);        result += inlineableMethod(mightEscape);    } else {        Object mightEscape = new Object(i);        result += tooBigToInline(mightEscape);    }}

Размотка (раскрутка) цикла

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

В результате размотки цикла такой код:

int i;for ( i = 1; i < n; i++){    a[i] = (i % b[i]);}

преобразуется в код вида:

int i;for (i = 1; i < n - 3; i += 4){    a[i] = (i % b[i]);    a[i + 1] = ((i + 1) % b[i + 1]);    a[i + 2] = ((i + 2) % b[i + 2]);    a[i + 3] = ((i + 3) % b[i + 3]);}for (; i < n; i++) {    a[i] = (i % b[i]);}

JVM принимает решение о размотке цикла по нескольким критериям:

  • по типу счетчика цикла, он должен быть одним из типов int, short или char;

  • по значению, на которое меняется счетчик цикла каждую итерацию;

  • по количеству точек выхода из цикла.

Мономорфная диспетчеризация

Многие оптимизации, выполняемые компилятором C2 основаны на эмпирических наблюдениях. Одним из примеров является оптимизация под названием мономорфная диспетчеризация. Она основана на факте, что очень часто в точках вызова тип объекта во время выполнения остается неизменным. Это связано с особенностями объектно-ориентированного дизайна. Например, при вызове метода на объекте, наблюдаемый тип объекта при первом и последующих вызовах будет одним и тем же. Если это предположение верно, то вызов метода в этой точке можно оптимизировать. В частности, нет необходимости каждый раз выполнять поиск по таблице виртуальных методов. Достаточно один раз определить целевой тип объекта и заменить вызов виртуального метода быстрой проверкой типа и прямым вызовом метода на объекте целевого типа. Если в какой-то момент тип объекта поменяется, JVM откатит оптимизацию и будет снова выполнять вызов виртуального метода.

Большое число вызовов в типичном приложении являются мономорфными. JVM также поддерживает биморфную диспетчеризацию. Она позволяет делать быстрые вызовы методов в одной точке на объектах двух разных типов.

Вызовы, которые не являются ни мономорфными ни биморфными, называются мегаморфными. Если в точке вызова наблюдается не очень большое число типов, используя один трюк, можно немного выиграть в производительности. Достаточно отделить от точки вызова несколько типов, используя оператор instanceof так, чтобы в ней осталось только 2 конкретных типа. Примеры биморфного, мегаморфного и разделенного мегаморфного вызовов приведены ниже.

interface Shape {int getSides();}class Triangle implements Shape {public int getSides() {return 3;}}class Square implements Shape {public int getSides() {return 4;}}class Octagon implements Shape {public int getSides() {return 8;}}class Example {  private Random random = new Random();private Shape triangle = new Triangle();private Shape square = new Square();private Shape octagon = new Octagon();public int getSidesBimorphic() {Shape currentShape = null;switch (random.nextInt(2)) {case 0:currentShape = triangle;break;case 1:currentShape = square;break;}return currentShape.getSides();}  public int getSidesMegamorphic() {    Shape currentShape = null;    switch (random.nextInt(3))    {    case 0:      currentShape = triangle;      break;    case 1:      currentShape = square;      break;    case 2:      currentShape = octagon;      break;    }    return currentShape.getSides();}  public int getSidesPeeledMegamorphic() {    Shape currentShape = null;    switch (random.nextInt(3))    {    case 0:      currentShape = triangle;      break;    case 1:      currentShape = square;      break;    case 2:      currentShape = octagon;      break;    }    // peel one observed type from the original call site    if (currentShape instanceof Triangle) {      return ((Triangle) currentShape).getSides();    }    else {      return currentShape.getSides(); // now only bimorphic    }}}

Intrinsic-методы

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

Метод

Описание

java.lang.System.arraycopy()

Быстрое копирование, используя векторную поддержку процессора.

java.lang.System.currentTimeMillis()

Быстрая реализация предоставляемая большинством ОС.

java.lang.Math.min()

Может быть выполнено без ветвления на некоторых процессорах.

Другие методы класса java.lang.Math

Прямая поддержка инструкций некоторыми процессорами.

Криптографические функции

Может использоваться аппаратная поддержка на некоторых платформах.

Шаблоны intrinsic-методов содержатся в исходном коде OpenJDK в файлах с расширением .ad (architecture dependent). Для архитектуры x86_64 они находятся в файле hotspot/src/cpu/x86/vm/x86_64.ad.

Деоптимизации

Когда мы рассматривали мониторинг работы компилятора в первой части, мы упомянули, что в логе могут появиться сообщения о деоптимизации кода. Деоптимизация означает откат ранее скомпилированного кода. В результате деоптимизации производительность приложения будет временно снижена. Существует два вида деоптимизации: недействительный код (not entrant code) и зомби код (zombie code).

Недействительный код

Код может стать недействительным в двух случаях:

  • при использовании полиморфизма;

  • в случае многоуровневой компиляции.

Полиморфизм

Рассмотрим пример:

Validator validator;if (document.isOrder()) {  validator = new OrderValidator();} else {  validator = new CommonValidator();}ValidationResult validationResult = validator.validate(document);

Выбор валидатора зависит от типа документа. Пусть для заказов у нас есть собственный валидатор. Предположим, что необходимо провалидировать большое количество заказов. Компилятор зафиксирует, что всегда используется валидатор заказов. Он встроит метод validate (если это возможно) и применит другие оптимизации. Далее, если на валидацию придет документ другого типа, предыдущее предположение окажется неверным, и сгенерированный код будет помечен как недействительный (non entrant). JVM перейдет на интерпретацию этого кода, и в будущем сгенерирует новую его версию.

Многоуровневая компиляция

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

Зомби код

Если в логе компиляции появилось сообщение о зомби коде, это значит, что все объекты, использующие предыдущие оптимизации были удалены из памяти и, как следствие, из code cache был удален код, ранее помеченный недействительным.

Список литературы и ссылки

  • Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond, Scott Oaks. ISBN: 978-1-492-05611-9.

  • Optimizing Java: Practical Techniques for Improving JVM Application Performance, Benjamin J. Evans, James Gough, and Chris Newland. ISBN: 978-1-492-02579-5.

  • Размотка цикла - википедия

Статьи цикла

Подробнее..

Анбоксинг в современной Java

20.01.2021 22:15:38 | Автор: admin

Сейчас новые версии Java выходят раз в полгода. В них время от времени появляются новые возможности: var в Java 10, switch-выражения в Java 14, рекорды и паттерны в Java 16. Про это всё, конечно, написано множество статей, блог-постов, сделано множество докладов на конференциях. Оказалось, однако, что мы все пропустили один очень крутой апгрейд языка, который произошёл в Java 14 - апгрейд обычного цикла for по набору целых чисел. Дело в том, что этот апгрейд случился не в языке, а в виртуальной машине, но заметно поменял на то как мы можем программировать на Java.

Вспомним старый добрый цикл for:

for (int i = 0; i < 10; i++) {  System.out.println(i);}

У такого синтаксиса уйма недостатков. Во-первых, переменная цикла упоминается три раза. Очень легко перепутать и упомянуть не ту переменную в одном или двух местах. Во-вторых, такая переменная не является effectively final. Её не передашь в явном виде в лямбды или анонимные классы. Но ещё важнее: нельзя застраховаться от случайного изменения переменной внутри цикла. Читать код тоже трудно. Если тело цикла большое, не так-то легко сказать, изменяется ли она ещё и внутри цикла, а значит непонятно, просто мы обходим числа по порядку или делаем что-то более сложное. Есть ещё потенциальные ошибки, если необходимо сменить направление цикла или включить границу. Да и выглядит это старомодно.

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

for (x in 0 until 10) {  println(x)}

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

Можно ли приблизиться к этому варианту в Java? Да, с помощью цикла for-each, который появился в Java 5. Достаточно написать такой библиотечный метод в утилитном классе:

/** * Возвращает диапазон целых чисел * @param fromInclusive начальное значение (включительно) * @param toExclusive конечное значение (не включается) * @return Iterable, содержащий числа от fromInclusive до toExclusive. */public static Iterable<Integer> range(int fromInclusive,                                       int toExclusive) {  return () -> new Iterator<Integer>() {    int cursor = fromInclusive;    public boolean hasNext() { return cursor < toExclusive; }    public Integer next() { return cursor++; }  };}

Никакого rocket science, исключительно тривиальный код, даже комментировать нечего. После этого вы легко можете писать красивые циклы и в Java:

for (int i : range(0, 10)) { // импортируем наш метод статически  System.out.println(i);}

Красиво. Можно явно объявить переменную final, чтобы запретить случайные изменения. Несложно сделать такие же методы с шагом или с включённой верхней границей и пользоваться ими. Почему же никто так до сих пор не делает? Потому что в данном случае из-за боксинга фатально страдает производительность. Давайте для примера посчитаем сумму кубов чисел в простеньком JMH-бенчмарке:

@Param({"1000"})private int size;@Benchmarkpublic int plainFor() {  int result = 0;  for (int i = 0; i < size; i++) {    result += i * i * i;  }  return result;}@Benchmarkpublic int rangeFor() {  int result = 0;  for (int i : range(0, size)) {    result += i * i * i;  }  return result;}

Тело цикла весьма быстрое и не выделяет никакой памяти, но при этом делает какую-то полезную работу, что не позволит JIT-компилятору выкинуть цикл совсем. Также я на всякий случай параметризовал верхнюю границу, чтобы JIT не заложился на конкретное значение количества итераций. Запустим на Java 8 и увидим безрадостную картину:

Benchmark            (size)  Mode  Cnt     Score     Error  Units BoxedRange.plainFor    1000  avgt   30   622.679    7.286  ns/op BoxedRange.rangeFor    1000  avgt   30  3591.052  792.159  ns/op

Использование метода range снизило производительность практически в шесть раз: тест выполняется 3,5 мкс вместо 0,6 мкс. Если посчитать аллокации с помощью -prof gc, мы обнаружим, что версия rangeFor выделяет 13952 байта, тогда как версия plainFor ожидаемо не выделяет памяти вообще. Легко понять, откуда взялось это число, если вспомнить, что целые числа до 127 кэшируются. Новые объекты Integer выделяются на итерациях 128-999, то есть создаётся 872 объекта по 16 байт. Заметьте, кстати, что ни объект Iterable, ни объект Iterator не создаются: здесь наш код прекрасно обрабатывается оптимизацией скаляризации (scalar replacement). Однако боксинг всё портит.

Понятно, что такие накладные расходы на обычный цикл for часто неприемлемы, поэтому программировать в таком стиле на Java никто всерьёз рекомендовать не будет. Однако давайте попробуем более новые версии Java:

Вот тут нас ждёт приятный сюрприз: начиная с Java 14 производительность варианта с range сравнялась с простой версией! JIT-компилятор стал достаточно умным, чтобы сгенерировать настолько же хороший ассемблер, как и в простой версии.

На самом деле работа над оптимизацией по уничтожению ненужного боксинга велась много лет. Её плоды можно было пощупать ещё с версии Java 8 с помощью опций JVM -XX:+UnlockExperimentalVMOptions -XX:+AggressiveUnboxing. Мы можем попробовать запустить наш тест с этой опцией, и окажется, что с ней уже в восьмёрке производительность была существенно лучше:

В Java 8-11 мы имели производительность на уровне 0,9 мкс, в 12 стало в районе 0,8, а начиная с 13 сравнялось с обычным циклом. И вот к Java 14 эта оптимизация стала достаточно стабильной, чтобы её включить по умолчанию. Вы можете пытаться сделать это и в более ранних версиях, но я бы не рекомендовал этого на серьёзном продакшне. Смотрите, например, какие страшные баги приходилось исправлять в связи с этой опцией.

В чём была сложность реализации автоматического удаления боксинга? Одна из основных проблем - как раз тот самый кэш объектов Integer до 127. При боксинге целых чисел выполняется нетривиальный метод valueOf (цитата по Java 16):

public static Integer valueOf(int i) {  if (i >= IntegerCache.low && i <= IntegerCache.high)    return IntegerCache.cache[i + (-IntegerCache.low)];  return new Integer(i);}

Как видно, этот метод берёт значения в диапазоне от IntegerCache.low до IntegerCache.high из кэша, который заполняется на ранней стадии инициализации виртуальной машины. Поэтому, если у нас происходит боксинг с последующим анбоксингом, нельзя просто положиться на механизм скаляризации: иногда мы должны возвращать закэшированные объекты. В режиме AggressiveUnboxing JIT-компилятор принудительно начинает игнорировать этот кэш, если может доказать, что ссылка на объект никуда не уплывает. В этом можно убедиться, написав какой-нибудь такой код:

Field field = Class.forName("java.lang.Integer$IntegerCache").getDeclaredField("cache");field.setAccessible(true);Integer[] arr = (Integer[]) field.get(null);arr[130] = new Integer(1_000_000);for (int i = 0; i < 10000; i++) {  int res = rangeFor();  if (res != -1094471800) {    System.out.println("oops! " + res + "; i = " + i);    break;  }}

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

oops! 392146832; i = 333

То есть когда JIT-компилятор C2 добирается до метода rangeFor, он выкидывает обращение к испорченному кэшу, и начинает выдавать правильный ответ, который должен получаться, если бы мы не залезли в кэш грязными руками.

Однако хоть кэш и игнорируется, до Java 12 включительно я вижу в ассемблерных листингах инструкции вида cmp r10d,7fh, то есть счётчик сравнивается с числом 127 (=0x7f). Похоже, условие полностью выкинуть удалось только в Java 13. Я могу спекулировать, что эти лишние проверки не только отъедают такты процессора, но и занимают дополнительные регистры, из-за чего страдает уровень развёртки цикла. Во всяком случае, до Java 12 цикл с rangeFor разворачивается по 8 итераций, а начиная с Java 13 лишние проверки исчезают и развёртка уже охватывает 16 итераций, сравниваясь с plainFor.

Так или иначе, мы видим результат: агрессивное уничтожение боксинга стало поведением по умолчанию с Java 14, что позволяет гораздо чаще наплевать на боксинг и пользоваться удобными конструкциями. В связи с этим цикл вида for (int i : range(0, 10)) должен стать каноническим в новых версиях Java и должен заменить динозавра for (int i = 0; i < 10; i++), в том числе в учебниках по языку.

Окончательное решение проблемы с боксингом должно прийти к нам после специализации дженериков в проекте Valhalla. Тогда можно будет возвращать Iterable<int>, и боксинг в данном случае не потребуется вообще. Вероятно, мы не увидим в стандартной библиотеке методов вроде range пока этого не случится. Однако боксинг уже не так страшен и с Iterable<Integer> можно комфортно жить.

Подробнее..

Обзор программы JPoint 2021 воркшопы, Spring, игра вдолгую

24.03.2021 18:04:11 | Автор: admin


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


  • Пришла весна, то есть самое время поговорить о Spring. О нём будет четыре доклада, в том числе большое двухчастное выступление Евгения Борисова. Для него мы даже продлили JPoint на пятый день получился специальный день Борисова :)
  • Онлайн-формату подходят воркшопы. Поэтому в отдельных случаях можно будет не просто любоваться слайдами: спикер будет выполнять конкретные задачи на практике, объясняя всё происходящее и отвечая на вопросы зрителей.
  • Есть доклады не строго про Java, а про то, как успешно разрабатывать на длинной дистанции (чтобы всё радовало не только на стадии прототипа, а годы спустя): как делать проекты поддерживаемыми, не плодить велосипеды, работать с легаси.
  • Ну и никуда не девается привычное. Знакомые темы: что у Java внутри, тулинг/фреймворки, языковые фичи, JVM-языки. Спикеры, посвятившие теме годы жизни: от технического лида Project Loom Рона Пресслера до главного Spring-адвоката Джоша Лонга. Возможность как следует расспросить спикера после доклада. И уточки для отладки методом утёнка!

Оглавление


Воркшопы
VM/Runtime
Тулинг и фреймворки
Spring
JVM-языки
Люби свою IDE
Жизнь после прототипа




Воркшопы


Воркшоп: Парное программирование, Андрей Солнцев, Антон Кекс


Парное программирование знакомо многим, но вот много ли вы знаете людей, использующих его на постоянной основе? А вот Андрей Солнцев и Антон Кекс действительно практикуют его постоянно и уверены, что подобные практики из мира экстремального программирования помогают работать эффективнее.


Они и раньше об этом говорили, а теперь попробуют показать наглядно. В ходе воркшопа они вдвоём напишут небольшое приложение по TDD (с помощью тестов) и в ходе этого вы лично убедитесь, что с таким подходом разработка идёт быстрее и проще. Или не убедитесь. Интересно будет посмотреть, какую часть аудитории получится убедить.




Воркшоп: Строим Бомбермена с RSocket, Олег Докука, Сергей Целовальников


Олег Докука и Сергей Целовальников на небольшом игровом примере продемонстрируют практический опыт использования RSocket-Java и RSocket-JS.


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




VM/Runtime


CRIU and Java opportunities and challenges, Christine H Flood


Зачем каждый раз тратить драгоценные секунды на запуск приложения, если можно один раз запустить, сохранить чекпоинт и открывать его уже в запущенном виде, в полной боевой готовности? Причём открывать при необходимости на нескольких машинах сразу?


О том, как использовать Checkpoint Restore в Java, расскажет Кристин Флад из Red Hat, которая работает над языками и рантаймами уже более двадцати лет.




Real World JFR: Experiences building and deploying a continuous profiler at scale, Jean-Philippe Bempel


JDK Flight Recorder позволяет профилировать непрерывно и прямо в продакшне. Но это же не может быть бесплатно, да? Важно понимать, чем придётся пожертвовать и какие будут накладные расходы.


Разобраться в этом поможет Жан-Филипп Бемпель он принимал непосредственное участие в реализации непрерывной профилировки в JFR.




GC optimizations you never knew existed, Igor Henrique Nicacio Braga, Jonathan Oommen


Какой JPoint без докладов про сборщики мусора! Тут выступление для тех, кто уже что-то знает по теме объяснять совсем азы не станут. Но и загружать суперхардкором с первой минуты тоже не станут. Сначала будет подготовительная часть, где Игор Брага и Джонатан Оммен рассмотрят два подхода к GC в виртуальной машине OpenJ9: balanced и gencon.


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




Adding generational support to Shenandoah GC, Kelvin Nilsen


И ещё о сборке мусора. На JPoint 2018 о Shenandoah GC рассказывал Алексей Шипилёв (Red Hat), а теперь доклад от совсем другого спикера Келвина Нилсена из Amazon, где тоже работают над этим сборщиком мусора.


Подход Shenandoah позволяет сократить паузы на сборку мусора менее чем до 10 миллисекунд, но за это приходится расплачиваться большим размером хипа (потому что его утилизация оказывается заметно ниже, чем у традиционных GC). А можно ли сделать так, чтобы и волки были сыты, и овцы целы? В Amazon для этого решили добавить поддержку поколений, и в докладе поделятся результатами.




Производительность: Нюансы против очевидностей, Сергей Цыпанов


Производительность тема, где с простым соседствует сложное. Даже в простых с виду частях кода могут обнаруживаться сюрпризы и подводные камни, а то, что казалось очевидным улучшением, может оборачиваться ухудшением производительности. Всё это и будет рассматривать в своём докладе Сергей Цыпанов, в своём профессиональном развитии сосредоточенный как раз на задачах производительности.




Why user-mode threads are (often) the right answer, Ron Pressler


Многопоточное программирование в Java поддерживается с версии 1.0, но за 25 лет в этой части языка почти ничего не поменялось, а вот требования выросли. Серверам требуется работать с сотнями тысяч, и даже миллионами потоков, а стандартное решение в JVM на тредах операционной системы не может так масштабироваться, поэтому Project Loom это одна из самых долгожданных фич языка.


Ранее у нас уже был доклад про Loom от Алана Бейтмана (мы делали расшифровку для Хабра), а теперь и technical lead этого проекта Рон Пресслер рассмотрит разные решения для работы с многопоточностью и подход, который используется в Loom.




Тулинг и фреймворки


Как мы строили высокопроизводительную систему на Akka с нуля: Джентльменский набор и грабли, Кирилл Данилов


Кирилл расскажет про опыт создания платежной системы с использованием Akka от обучения с нуля до построения кластера и интеграции этой платформы с более привычными и удобными в своей нише технологиями, например, Spring Boot, Hazelcast, Kafka.


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




Jakarta EE 9 and beyond, Ivar Grimstad, Tanja Obradovi


Jakarta EE 9 несет множество изменений, которые затронут большое количество библиотек и фреймворков для Java. Чтобы понять, как эти изменения отразятся на ваших проектах, приходите на доклад Ивана Гримстада и Тани Обрадович.


Ивар Jakarta EE Developer Advocate, а Таня Jakarta EE Program Manager, поэтому вы узнаете о самых важных изменениях и планах на будущее из первых рук.




Чтения из Cassandra внутреннее устройство и производительность, Дмитрий Константинов


В качестве хранилища данных очень часто используется Cassandra, и всем, кто ее использует, будет полезно знать, как она устроена внутри, и от чего зависит её производительность на чтение.


Об этом расскажет системный архитектор и практикующий разработчик из NetCracker Дмитрий Константинов.




The DGS framework by Netflix GraphQL for Spring Boot made easy, Paul Bakker


В Netflix разработали DGS Framework для работы с GraphQL. Он работает поверх graphql-java и позволяет работать с GraphQL, используя привычные модели Spring Boot. И, что приятно, он опенсорсный, стабильный и готов к использованию в продакшне.


Пол Баккер один из авторов DGS. Он расскажет и про GraphQL, и про то, как работать с DGS, и про то, как это используется в Netflix.




Качественный код в тестах не просто приятный бонус, Sebastian Daschner


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


Предыдущие доклады Себастьяна на наших конференциях были англоязычными, но участвуя в российских мероприятиях, он так влился в местное Java-сообщество, что теперь попробует провести доклад на русском!




Why you should upgrade your Java for containers, Ben Evans


Статистика от New Relic говорит, что примерно 62% Java на продакшне в 2021 запущено в контейнерах. Но в большинстве из этих случаев до сих пор используют Java 8 а эта версия подходит для контейнеризации не лучшим образом. Почему? Бен Эванс рассмотрит, в чём проблемы с ней, что улучшилось с Java 11, и как измерить эффективность и расходы.


Хотя в основном речь пойдёт о проблемах, актуальных именно в случае с контейнерами, часть сказанного будет применима и к Java-разработке в целом.




Разошлись как в море корабли: Кафка без Zookeeper, Виктор Гамов


Совсем скоро придет тот день, о котором грезили Kafka-опсы и Apache Kafka больше не будет нуждаться в ZooKeeper! С KIP-500 в Kafka будет доступен свой встроенный механизм консенсуса (на основе алгоритма Raft), полностью удалив зависимость от ZooKeeper. Начиная с Apache Kafka 2.8.0. вы сможете получить доступ к новому коду и разрабатывать свои приложения для Kafka без ZooKeeper.


В конечном счете, удаление ZooKeeper упрощает общее проектирование инфраструктуры и рабочие процессы вокруг Кафки. В этом докладе Виктор сделает краткий обзор того, какие ограничения Zookeeper накладывал на архитектуру Кафки и приложений, что будет изменено, а также текущий статус удаления Zookeeper.




Spring


Spring Data Рostроитель (Spark it!), Евгений Борисов


Товарищ, знай! Чтоб использовать Spark,
Scala тебе не друг и не враг.
Впрочем, и Spark ты можешь не знать,
Spring-data-spark-starter лишь надо создать!


Этот доклад не про Spark и не про Big Data. Его скорее можно отнести к серии потрошителей и построителей. Что будем строить и параллельно потрошить сегодня? Spring Data. Она незаметно просочилась в большинство проектов, подкупая своей простотой и удобным стандартом, который избавляет нас от необходимости каждый раз изучать новый синтаксис и подходы разных механизмов работы с данными.


Хотите разобраться, как Spring Data творит свою магию? Давайте попробуем написать свой аналог. Для чего ещё не написана Spring Data? JPA, Mongo, Cassandra, Elastic, Neo4j и остальные популярные движки уже имеют свой стартер для Spring Data, а вот Spark, как-то забыли. Давайте заодно исправим эту несправедливость. Не факт, что получится что-то полезное, но как работает Spring Data мы точно поймём.




Spring Cloud в эру Kubernetes, Алексей Нестеров


Когда-то давно, много JavaScript-фреймворков назад, когда микросервисы еще были монолитами, в мире существовало много разных инструментов для разработки Cloud Native приложений. Spring Cloud был одним из главных в реалиях Spring и объединял в себе целый набор полезных проектов от Netflix, команды Spring и многих других вендоров.


Казалось бы, в наши дни, когда весь мир захвачен Kubernetes и он уже давно стал универсальным решением любой проблемы, важность Spring Cloud должна неизбежно сойти на нет. Но не все так просто, и в этом докладе Алексей покажет, какие компоненты Spring Cloud могут быть полезны в Kubernetes, чем эти два проекта друг друга дополняют, в каких аспектах пересекаются, ну и самое главное, постарается ответить на вопрос в чем же ценность Spring Cloud в эру Kubernetes?




Reactive Spring, Josh Long


Джош Лонг расскажет про фичи Spring Framework 5.0 для реактивного программирования: Spring WebFlux, Spring Data Kay, Spring Security 5.0, Spring Boot 2.0, Spring Cloud Finchley и это только часть!


Может показаться многовато для одного доклада, но мы-то знаем, что Джош Spring Developer Advocate с 2010 года. Уж кто-кто, а он-то знает, как рассказать всё быстро и по делу.




Inner loop development with Spring Boot on Kubernetes, David Syer


Мы живем во время облачных технологий и чтобы эффективнее перейти от принципа works on my machine к works on my/dev cluster нужен набор инструментов для автоматизация загрузки кода на лету.
Доклад Дэвида Сайера будет про то, как и с помощью каких инструментов Spring Boot и Kubernetes построить этот процесс удобно.
Ускорение первой фазы доставки это тот DevOps, который нужен разработчикам, поэтому всем, кто живет в k8s или хотя бы делает системы из нескольких компонентов этот доклад пригодится.




Люби свою IDE


IntelliJ productivity tips The secrets of the fastest developers on Earth, Victor Rentea


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


Виктор Рента покажет много трюков для работы с IntelliJ IDEA, которые помогут писать код быстрее, экспериментировать больше и делать откат результатов неудачных экспериментов проще.




Многоступенчатые рефакторинги в IntelliJ IDEA, Анна Козлова


В IntelliJ IDEA есть ограниченное количество основных рефакторингов: Rename, Move, Inline, Extract. Пользователи часто просят добавить еще, но чаще всего это можно сделать комбинацией уже существующих, просто это не всегда очевидно.


На JPoint 2021 вы сможете получить практические рекомендации по рефакторингу от человека, который разрабатывает рефакторинги: о самых важных приемах расскажет коммитер 1 в IntelliJ IDEA Community Edition Анна Козлова.




С какими языками дружат IDE?, Петр Громов


Разработчики всё чаще выбирают языки с выразительным и кратким синтаксисом, совсем не задумываясь на тем, как долго их код будет компилироваться, и сможет ли IDE помочь им с рефакторингом. В докладе Петра Громова будут интересные идеи о сложности современных языков программирования с точки зрения разработчиков IDE.


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




Java и JVM-языки


Type inference: Friend or foe?, Venkat Subramaniam


Не все могут объяснять так, как это делает Венкат Субраманиам, поэтому мы любим приглашать его на конференции.


На JPoint 2021 он выступит с докладом про type inference. Хотя тема сама по себе не новая, нюансов в ней хватает, а развитие языков делает её лишь более актуальной (вспоминается доклад Романа Елизарова с TechTrain, где он рассматривал, как ситуация с типами и их выводом менялась со временем). Так что стоит лучше понять, в чём вывод типов помогает, а в чём мешает для этого и рекомендуем сходить на этот доклад.




Babashka: A native Clojure interpreter for scripting, Michiel Borkent


Babashka интерпретатор Clojure для скриптов. Он мгновенно запускается, делая Clojure актуальной заменой для bash. У Babashka из коробки есть набор полезных библиотек, дающих доступ из командной строки к большому количеству фич Clojure и JVM. Сам интерпретатор написан на Clojure и скомпилирован с помощью GraalVM Native Image. В докладе работу с ним наглядно покажут с помощью демо.




Getting the most from modern Java, Simon Ritter


Недавно вышла JDK 16, и это значит, что мы получили 8 (прописью: ВОСЕМЬ) версий Java менее чем за четыре года. Разработчики теперь получают фичи быстрее, чем когда-либо в истории языка.


Так что теперь попросту уследить бы за всем происходящим. Если вы до сих пор сидите на Java 8, на что из появившегося позже стоит обратить внимание и чем это вам будет полезно? В этом поможет доклад Саймона Риттера, где он поговорит о некоторых нововведениях JDK 12-15 и о том, когда их исследовать, а когда нет:


  • Switch expressions (JDK 12);
  • Text blocks (JDK 13);
  • Records (JDK 14);
  • Pattern matching for instanceof (JDK 14);
  • Sealed classes and changes to Records (JDK 15).


Про Scala 3, Олег Нижников


Обзор языка Scala 3 и грядущей работы по переходу. Обсудим, в какую сторону двигается язык, откуда черпает вдохновение, и пройдёмся по фичам.




Java Records for the intrigued, Piotr Przybyl


В Java 14 появились в превью-статусе Records, а с Java 16 они стали стандартной фичей. Для многих это было поводом сказать что-то вроде Lombok мёртв или не нужна больше кодогенерация JavaBeans. Так ли это на самом деле? Что можно сделать с помощью Records, а чего нельзя? Что насчёт рефлексии и сериализации? Разберём в этом докладе.




Жизнь после прототипа


Восстанавливаем утраченную экспертизу по сервису, Анна Абрамова


Представьте ситуацию: вы приходите на работу в компанию, где до вас уже написано многое на Java и вспомогательных технологиях. У вас горят глаза, чешутся руки и вы хотите работать. И тут вас кидают в море легаси, где вместо документации крошки на клавиатуре от вашего предшественника.


Анна Абрамова на примере вымышленной системы расскажет, какими методами можно восстановить сакральные знания.




Что такое Работающий Продукт и как его делать, Антон Кекс


Антон Кекс регулярно сталкивается с проектами, у которых проблемы даже с элементарным запуском. Поэтому целый доклад на JPoint он посвятит тому, как делать на самом деле работающие продукты, которыми можно пользоваться и легко поддерживать как создателю, так и его коллегам.


Если ваша точка зрения не совпадет можно будет всё обсудить с Антоном в дискуссионной зоне. Вероятно, там будет жарко.




Enum в API коварство иллюзорной простоты, Илья Сазонов и Фёдор Сазонов


Вы уверены, что если добавить один маленький enum в API, то ничего страшного не произойдет? Или наоборот уверены, что так делать не стоит, но никто вас не слушает?
Рекомендуем вам доклад Ильи и Федора Сазоновых, пропитанный тяжелой болью по поводу бесконечных обновлений контрактов микросервисов.
Обычно подобные темы не выходят за пределы локального холивара в курилке, но нельзя же вечно добавлять новые значения в enum?




Dismantling technical debt and hubris, Shelley Lambert


Многие разработчики любят строить велосипеды, потому что свой велосипед он же надежнее, быстрее и удобнее, чем всё остальное. А потом все эти велосипеды складываются в один большой технический долг, с которым нужно что-то делать.
Шелли Ламберт научит вас не писать собственные решения для всего подряд, а правильно выбирать существующие.




Подводя итог


Надеемся, по программе ясность появилась, но напоследок подчеркнём, что онлайн-конференция это не только слушать доклады. Это ещё и:
Вопросы спикерам в чатах и видеосозвонах
Общение зрителей (на нашей виртуальной площадке можно подойти к другому зрителю и увидеть его по видеосвязи)
Виртуальные стенды партнёров с различными активностями


Напоминаем, поучаствовать во всём этом можно будет с 13 по 17 апреля в онлайне. Вся дополнительная информация и билеты на сайте.

Подробнее..

Перевод Развеиваем мифы об управлении памятью в JVM

28.05.2021 16:10:14 | Автор: admin

В серии статей я хочу опровергнуть заблуждения, связанные с управлением памятью, и глубже рассмотреть её устройство в некоторых современных языках программирования Java, Kotlin, Scala, Groovy и Clojure. Надеюсь, эта статья поможет вам разобраться, что происходит под капотом этих языков. Сначала мы рассмотрим управление памятью в виртуальной машине Java (JVM), которая используется в Java, Kotlin, Scala, Clojure, Groovy и других языках. В первой статье я рассказал и разнице между стеком и кучей, что полезно для понимания этой статьи.

Структура памяти JVM


Сначала давайте посмотрим на структуру памяти JVM. Эта структура применяется начиная с JDK 11. Вот какая память доступна процессу JVM, она выделяется операционной системой:


Это нативная память, выделяемая ОС, и её размер зависит от системы, процессор и JRE. Какие области и для чего предназначены?

Куча (heap)


Здесь JVM хранит объекты и динамические данные. Это самая крупная область памяти, в ней работает сборщик мусора. Размером кучи можно управлять с помощью флагов Xms (начальный размер) и Xmx (максимальный размер). Куча не передаётся виртуальной машине целиком, какая-то часть резервируется в качестве виртуального пространства, за счёт которого куча может в будущем расти. Куча делится на пространства молодого и старого поколения.

  • Молодое поколение, или новое пространство: область, в которой живут новые объекты. Она делится на рай (Eden Space) и область выживших (Survivor Space). Областью молодого поколения управляет младший сборщик мусора (Minor GC), который также называют молодым (Young GC).
    • Рай: здесь выделяется память, когда мы создаём новые объекты.
    • Область выживших: здесь хранятся объекты, которые остались после работы младшего сборщика мусора. Область делится на две половины, S0 и S1.
  • Старое поколение, или хранилище (Tenured Space): сюда попадают объекты, которые достигли максимального порога хранения в ходе жизни младшего сборщика мусора. Этим пространством управляет старший сборщик (Major GC).

Стеки потоков исполнения


Это стековая область, в которой на один поток выделяется один стек. Здесь хранятся специфические для потоков статичные данные, в том числе фреймы методов и функций, указатели на объекты. Размер стековой памяти можно задать с помощью флага Xss.

Метапространство


Это часть нативной памяти, по умолчанию у неё нет верхней границы. В более ранних версиях JVM эта память называлась пространством постоянного поколения (Permanent Generation (PermGen) Space). Загрузчики классов хранили в нём определения классов. Если это пространство растёт, то ОС может переместить хранящиеся здесь данные из оперативной в виртуальную память, что может замедлить работу приложения. Избежать этого можно, задав размер метапространства с помощью флагов XX:MetaspaceSize и -XX:MaxMetaspaceSize, в этом случае приложение может выдавать ошибки памяти.

Кеш кода


Здесь компилятор Just In Time (JIT) хранит скомпилированные блоки кода, к которым приходится часто обращаться. Обычно JVM интерпретирует байткод в нативный машинный код, однако код, скомпилированный JIT-компилятором, не нужно интерпретировать, он уже представлен в нативном формате и закеширован в этой области памяти.

Общие библиотеки


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

Использование памяти JVM: стек и куча


Теперь давайте посмотрим, как исполняемая программа использует самые важные части памяти. Воспользуемся нижеприведённым кодом. Он не оптимизирован с точки зрения корректности, так что игнорируйте проблемы вроде ненужных промежуточных переменных, некорректных модификаторов и прочего. Его задача визуализировать использование стека и кучи.

class Employee {    String name;    Integer salary;    Integer sales;    Integer bonus;    public Employee(String name, Integer salary, Integer sales) {        this.name = name;        this.salary = salary;        this.sales = sales;    }}public class Test {    static int BONUS_PERCENTAGE = 10;    static int getBonusPercentage(int salary) {        int percentage = salary * BONUS_PERCENTAGE / 100;        return percentage;    }    static int findEmployeeBonus(int salary, int noOfSales) {        int bonusPercentage = getBonusPercentage(salary);        int bonus = bonusPercentage * noOfSales;        return bonus;    }    public static void main(String[] args) {        Employee john = new Employee("John", 5000, 5);        john.bonus = findEmployeeBonus(john.salary, john.sales);        System.out.println(john.bonus);    }}

Здесь вы можете увидеть, как исполняется вышеприведённая программа и как используются стек и куча:

https://files.speakerdeck.com/presentations/9780d352c95f4361bd8c6fa164554afc/JVM_memory_use.pdf

Как видите:

  • Каждый вызов функции добавляется в стек потока исполнения в качестве фреймового блока.
  • Все локальные переменные, включая аргументы и возвращаемые значения, сохраняются в стеке внутри фреймовых блоков функций.
  • Все примитивные типы вроде int хранятся прямо в стеке.
  • Все типы объектов вроде Employee, Integer или String создаются в куче, а затем на них ссылаются с помощью стековых указателей. Это верно и для статичных данных.
  • Функции, которые вызываются из текущей функции, попадают наверх стека.
  • Когда функция возвращает данные, её фрейм удаляется из стека.
  • После завершения основного процесса объекты в куче больше не имеют стековых указателей и становятся потерянными (сиротами).
  • Пока вы явно не сделаете копию, все ссылки на объекты внутри других объектов делаются с помощью указателей.

Стеком автоматически управляет операционная система, а не JVM. Поэтому не нужно особо заботиться о нём. А вот куча уже так не управляется, и поскольку это самая большая область памяти, содержащая динамические данные, она может расти экспоненциально, и программа способна со временем занять всю память. Кроме того, постепенно куча фрагментируется, тем самым замедляя работу приложений. В решении этих проблем поможет JVM. Она автоматически управляет кучей с помощью сборки мусора.

Управление памятью JVM: сборка мусора


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

JVM управляет куче с помощью сборки мусора. Чтобы освободить место для создания нового объекта, JVM очищает память, занятую потерянными объектами, то есть объектами, на которые больше нет прямых или опосредованных ссылок из стека.


Сборщик мусора в JVM отвечает за:

  • Получение памяти от ОС и возвращение её ОС.
  • Передачу выделенной памяти приложению по его запросу.
  • Определение, какие части выделенной памяти ещё используются приложением.
  • Затребование неиспользованной памяти для использования приложением.

Сборщики мусора в JVM работают по принципу поколений (объекты в куче группируются по возрасту и очищаются во время разных этапов). Есть много разных алгоритмов сборки мусора, но чаще всего применяют Mark & Sweep.

Сборщик мусора Mark & Sweep


JVM использует отдельный поток демона, который работает в фоне для сборки мусора. Этот процесс запускается при выполнении определённых условий. Сборщик Mark & Sweep обычно работает в два этапа, иногда добавляют третий, в зависимости от используемого алгоритма.


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

Такой тип сборщиков ещё называют stop-the-world, потому что пока они убираются, возникают паузы в работе приложения.

JVM предлагает на выбор несколько разных алгоритмов сборки мусора, и в зависимости от вашего JDK может быть ещё больше вариантов (например, сборщик Shenandoah в OpenJDK). Авторы разных реализаций стремятся к разным целям:

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

Сборщики в JDK 11


JDK 11 это текущая версия LTE. Ниже приведён список доступных в ней сборщиков мусора, и JVM выбирает по умолчанию один из них в зависимости от текущего оборудования и операционной системы. Мы всегда можем принудительно выбрать какой-либо сборщик с помощью переключателя -XX.

  • Серийный сборщик: использует один поток, эффективен для приложений с небольшим количеством данных, наиболее удобен для однопроцессорных машин. Его можно выбрать с помощью -XX:+UseSerialGC.
  • Параллельный сборщик: нацелен на высокую пропускную способность и использует несколько потоков, чтобы ускорить процесс сборки. Предназначен для приложений со средним или большим количеством данных, исполняемых на многопоточном/многопроцессорном оборудовании. Его можно выбрать с помощью -XX:+UseParallelGC.
  • Сборщик Garbage-First (G1): работает по большей части многопоточно (то есть многопоточно выполняются только объёмные задачи). Предназначен для многопроцессорных машин с большим объёмом памяти, по умолчанию используется на большинстве современных компьютеров и ОС. Нацелен на короткие паузы и высокую пропускную способность. Его можно выбрать с помощью -XX:+UseG1GC.
  • Сборщик Z: новый, экспериментальный, появился в JDK11. Это масштабируемый сборщик с низкой задержкой. Многопоточный и не останавливает исполнение потоков приложения, то есть не относится к stop-the-world. Предназначен для приложений, которым необходима низкая задержка и/или очень большая куча (на несколько терабайтов). го можно выбрать с помощью -XX:+UseZGC.

Процесс сборки мусора


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

Младший сборщик


Он поддерживает чистоту и компактность пространства молодого поколения. Запускается тогда, когда JVM не может получить в раю необходимую память для размещения нового объекта. Изначально все области кучи пусты. Рай заполняется первым, за ним область выживших, и в конце хранилище.

Здесь вы можете увидеть процесс работы этого сборщика:

https://files.speakerdeck.com/presentations/f4783404769145f4b990154d0cc05629/JVM_minor_GC.pdf

  1. Допустим, в раю уже есть объекты (блоки с 01 по 06 помечены как используемые).
  2. Приложение создаёт новый объект (07).
  3. JVM пытается получить необходимую память в раю, но там уже нет места для размещения нового объекта, поэтому JVM запускает младший сборщик.
  4. Он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные как мусор (потерянные).
  5. JVM случайно выбирает один блок из S0 и S1 в качестве целевого пространства (To Space), пусть это будет S0. Теперь сборщик перемещает все живые объекты в целевое пространство, которое было пустым, когда мы начали работу, и повышает их возраст на единицу.
  6. Затем сборщик очищает рай, и в нём выделяется память для нового объекта.
  7. Допустим, прошло какое-то время, и в раю стало больше объектов (блоки с 07 по 13 помечены как используемые).
  8. Приложение создаёт новый объект (14).
  9. JVM пытается получить в раю нужную память, но там нет свободного места для нового объекта, поэтому JVM снова запускает младший сборщик.
  10. Повторяется этап разметки, который охватывает и те объекты, что находятся в пространстве выживших в целевом пространстве.
  11. Теперь JVM выбирает в качестве целевого свободный блок S1, а S0 становится исходным. Сборщик перемещает все живые объекты из рая и исходного в целевое (S1), которое было пустым, и повысил возраст объектов на единицу. Поскольку некоторые объекты сюда не поместились, сборщик переносит их в хранилище, ведь область выживших не может увеличиваться, и этот процесс называют преждевременным продвижением (premature promotion). Такое может происходить, даже если свободна одна из областей выживших.
  12. Теперь сборщик очищает рай и исходное пространство (S0), а новый объект размещается в раю.
  13. Так повторяется при каждой сессии младшего сборщика, выжившие перемещаются между S0 и S1, а их возраст увеличивается. Когда он достигает заданного максимального порога, по умолчанию это 15, объект перемещается в хранилище.

Мы рассмотрели, как младший сборщик очищает память в пространстве молодого поколения. Это процесс типа stop-the-world, но он настолько быстрый, что его длительностью обычно можно пренебречь.

Старший сборщик


Следит за чистотой и компактностью пространства старого поколения (хранилищем). Запускается при одном из таких условий:

  • Разработчик вызывает в программе System.gc() или Runtime.getRunTime().gc().
  • JVM решает, что в хранилище недостаточно памяти, потому что оно заполнено в результате прошлых сессий младшего сборщика.
  • Если во время работы младшего сборщика JVM не может получить достаточно памяти в раю или области выживших.
  • Если мы задали в JVM параметр MaxMetaspaceSize и для загрузки новых классов не хватает памяти.

Процесс работы старшего сборщика попроще, чем младшего:

  1. Допустим, прошло уже много сессий младшего сборщика и хранилище почти заполнено. JVM решает запустить старший сборщик.
  2. В хранилище он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные как мусор (потерянные). Если старший сборщик запустили в ходе работы младшего сборщика, то его работа охватывает пространство молодого поколения (рай и область выживших) и хранилище.
  3. Сборщик убирает все потерянные объекты и возвращает память.
  4. Если в ходе работы старшего сборщика в куче не осталось объектов, JVM также возвращает память из метапространства, убирая из него загруженные классы, если это относится к полной сборке мусора.

Заключение


Мы рассмотрели структуру и управление памятью JVM. Это не исчерпывающая статья, мы не говорили о многих более сложных концепциях и способах настройки под особые сценарии использования. Подробнее вы можете почитать здесь.

Но для большинства JVM-разработчиков (Java, Kotlin, Scala, Clojure, JRuby, Jython) этого объёма информации будет достаточно. Надеюсь, теперь вы сможете писать более качественный код, создавать более производительные приложения, избегая различных проблем с утечкой памяти.

Ссылки


Подробнее..

7 вещей, которые нужно проработать, прежде чем запускать OpenShift в продакшн

24.12.2020 16:21:15 | Автор: admin
Взрывной рост использования контейнеров на предприятиях впечатляет. Контейнеры идеально совпали с ожиданиями и потребностями тех, кто хочет снизить затраты, расширить свои технические возможности и продвинуться вперед по пути agile и devops. Контейнерная революция открывает новые возможности и для тех, кто подзадержался с обновлением ИТ-систем. Контейнеры и Kubernetes это абсолютно и принципиально новый способ управления приложениями и ИТ-инфраструктурой.



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

Многие решают ускорить переход на контейнеры с помощью Red Hat OpenShift Container Platform, ведущей отраслевой Kubernetes-платформы для корпоративного сектора. Это решение автоматически берет на себя множество задач первого дня и предлагает лучшую Kubernetes-экосистему на базе единой, тщательно протестированной и высоко защищенной платформы. Это наиболее комплексное и функциональное решение для предприятий, которое содержит всё необходимое для начала работы и устраняет массу технических барьеров и сложностей при построении Kubernetes-платформы.

Тем не менее, OpenShift это не волшебная палочка, которая решает все проблемы сама. Да, благодаря своим возможностям, эта платформа способна принести и приносит своим заказчикам массу пользы и быстро окупается, но при условии, что на момент ее запуска у вас есть хорошо продуманный план. Чтобы добиться успеха, надо тщательно проработать семь областей, прежде чем приступать к переносу каких-либо рабочих нагрузок на OpenShift.

1. Стандартизация правил именования и метаданных


В компьютерных науках есть только две трудные вещи: аннулирование кэша и именование сущностей.
Фил Карлтон (Phil Karlton)

У всякой сущности в OpenShift и Kubernetes есть свое имя. И у каждого сервиса должно быть свое DNS-имя, единственное ограничение здесь правила именования DNS. А теперь представьте, что монолитное приложение разложилось на 100500 отдельных микросервисов, каждый с собственной базой данных. И да, в OpenShift всё является либо иерархическим, связанным, либо должно соответствовать шаблону. Так что именовать придется массу и массу всего. И если заранее не подготовить стандарты, это получится настоящий Дикий Запад.

Вы уже распланировали схему реализации сервисов? Допустим, это будет одно большое пространство имен, например, databases, в котором все будут размещать свои базы данных. OK, и даже допустим, что все так и будут делать, но потом-то они начнут размещать свои кластеры Kafka в своих собственных пространствах имен. Да, а нужно ли заводить пространство имен middleware? Или лучше назвать его messaging? И как обычно, в какой-то момент появляются ребята, которые всегда идут своим путем и считают себя особенными, и говорят, что им нужны собственные пространства имен. И слушайте, у нас же в организации 17 подразделений, может надо приделать ко всем пространствам имен наши стандартные префиксы подразделений?

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

Другая мегаполезная вещь это метаданные. Стандартизируйте, какие активы хотите отслеживать, и убедитесь, что на соответствующих ресурсах прописаны нужные метаданные. Начните с рекомендованных меток. Например, аннотация support_email в метаданных пространства имен может сэкономить драгоценное время при выходе на техподдержку второго уровня в случае серьезного отказа. Кроме того, метаданные можно использовать, чтобы сократить имена ресурсов до вменяемой длинны, а не прописывать туда всю необходимую информацию через дефис. Привлеките всех, от архитекторов приложений до ИТ-эксплуатантов, устройте мозговой штурм и просчитайте наперед, что может здесь понадобиться, чтобы иметь продуманные стандарты к моменту запуска OpenShift.

2. Стандартизация корпоративных базовых образов


Одна из ключевых фишек контейнеров это возможность миксовать и подбирать все составляющие программного стека. Можно, конечно, взять любимую разновидность ОС и строить все на ней, но действуя подобным образом организация упускает огромные возможности. Ведь что по-настоящему круто в контейнерных образах? Многослойность. Вы можете снять с разработчиков массу критичных задач и решать их за счет стандартизации образов.

Возьмем, к примеру, базовое java-приложение. Ваши разработчики вряд ли ошибутся с выбором OpenJDK, а вот с управлением уязвимостями, обновлением библиотек и прочими вопросами ИТ-гигиены вполне могут. Мы все знаем, что бизнес-задачи зачастую решаются ценой технических компромиссов, вроде намеренного использования старых версий Java. К счастью, такие задачи легко автоматизируются и управляются на уровне предприятия. Вы по-прежнему может использовать базовые образы вендора, но одновременно задавать и контролировать свои циклы обновления, создавая собственные базовые образы.

Возвращаясь к примеру выше, допустим, разработчикам нужна Java 11, а вам, соответственно, надо, чтобы они всегда использовали самую последнюю версию Java 11. Тогда вы создаете корпоративный базовый образ (registry.yourcompany.io/java11), используя в качестве отправной точки базовый образ от вендора ОС (registry.redhat.io/ubi8/openjdk-11). А когда этот базовый образ обновляется, вы автоматом помогаете разработчикам задействовать последние обновления. К тому же, таким образом реализуется уровень абстракции, позволяющий бесшовно дополнять стандартный образ необходимыми библиотеками или Linux-пакетами.

3. Стандартизация проверок работоспособности и готовности


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

  • Запущено ли приложение (health check работоспособность).
  • Готово ли приложение (readiness check готовность).

Существует масса и других метрик, чтобы облегчить мониторинг приложений, но вот эти две это основа основ не только мониторинга, но и масштабирования. Работоспособность обычно определяется наличием сетевого подключения и способностью узла, на котором выполняется приложение, отозваться на запрос. Что касается готовности, то здесь уже каждое приложение должно реагировать на запросы по своим стандартам. Например, запуск приложения с очень низкими задержками может сопровождаться длительным обновлением кэша или прогревом JVM. И соответственно, пауза между ответами Запущено и Готово может достигать нескольких минут. А вот, например, для stateless REST API с реляционной базой данных эти ответы будут приходить одновременно.

Самое главное в этих проверках не отходить от сугубо двоичной логики. Запущено значит запущено, без всяких там как бы запущено. Готово значит готово, и никаких градаций, вроде на такие запросы готово отвечать, а на такие нет. Принцип простой: всё или ничего.

Второй аспект таких проверок это стандартизация. Как проверить готовность? Если у вас нет стандартов, то даже такой простой вопрос может стать настоящим кошмаром для мониторинга. Просто сравните, как разошлись друг от друга стандарты Quarkus и стандарты Spring Boot. А ведь никто этого не хотел, но со стандартами всегда так. Единственная разница в том, что теперь ваша организация сама имеет власть разрабатывать и вводить стандарты.
Примечание на полях. Не изобретайте свои стандарты. Просто найдите и используйте какой-нибудь готовый.

4. Стандартизация логов


Продолжая тему мониторинга, отметим, что сочетание недорогих хранилищ и решений класса big data породило на предприятиях нового монстра тотальное журналирование. Раньше это были неструктурированные и архаичные консольным логи, которые жили недолго и создавались от случая к случаю. Теперь норовят запротоколировать всё подряд и выстроить датасайнс с машинным обучением, чтобы самым революционным образом оптимизировать операции и мониторинг. Увы, надо признать очевидное: любые попытки начать собирать логи сотен приложений, не имея при этом абсолютно никаких стандартов и даже не задумываясь о них, неизменно приводят к бессмысленным и непомерным тратам на инструменты для управления логами и трансформации данных лишь для того, чтобы только начать работу. То есть еще до того, как вы поймете, что сообщения Выполнен переход или Этот блок сработал вряд имеют хоть какое-то отношение к вашим операциям.

Стандартизировать надо структуру. Повторимся: целостность стандартов важнее их правильности. Вы должны быть способы написать отдельный лог-парсер для каждого приложения, которое есть на предприятии. Да, это будут сугубо штучные, не тиражируемые вещи. Да, у вас будет куча исключений, которые нельзя контролировать, особенно для коробочных приложений. Но не выплескивайте ребенка вместе с водой, уделите внимание деталям: например, временная метка в каждом логе должна отвечать соответствующему стандарту ISO; сам вывод должен быть в формате UTC с точностью до 5-го знака в микросекундах (2018-11-07T00:25:00.07387Z). Уровни журнала должны быть оформлены CAPS-ом и там должны быть элементы TRACE, DEBUG, INFO, WARN, ERROR. В общем, задайте структуру, а уже затем разбирайтесь с подробностями.

Стандартизация структуры заставит всех придерживаться одних правил и использовать одни и те же архитектурные шаблоны. Это верно для логов как приложений, так и платформ. И не отклоняйтесь от готового решения без крайней нужды. EFK-стек (Elasticsearch, Fluentd и Kibana) платформы OpenShift должен быть в состоянии обработать все ваши сценарии. Он ведь вошел в состав платформы не просто так, и при ее обновлении это еще одна вещь, о которой не надо беспокоиться.

5. Переход на GitOps


Одна из главных прелестей OpenShift заключается в том, что здесь всё буквально: всё в конечном является либо конфигурацией, либо кодом, а значит, может контролироваться через систему управления версиями. Это позволяет революционизировать способы доставки и избавиться от бюрократии при запуске в продакшн.

В частности, традиционную схему на основе тикетов можно полностью заменить на модель с pull-запросами git. Допустим, владелец приложения хочет подкорректировать выделяемые приложению ресурсы после реализации в нем новых функций, например, увеличить память с 8 до 16 ГБ. В рамках традиционной схемы разработчику для этого надо создать тикет и ждать, пока кто-то другой выполнит соответствующую задачу. Этим кем-то другим чаще всего оказывается ИТ-эсплуатант, который лишь вносит ощутимую задержку в процесс реализации изменений, никак не повышая ценность этого процесса, или хуже того, навешивая на этот процесс лишние дополнительные циклы. В самом деле, у эсплуатанта есть два варианта действий. Первое: он рассматривает заявку и решает ее выполнить, для чего входит в продакшн-среду, вносит затребованные изменения вручную и перезапускает приложение.
Помимо времени на проведение самой работы здесь возникает и дополнительная задержка, поскольку у эксплуатанта, как правило, всегда есть целая очередь заявок на выполнение. Кроме того, возникает риск человеческой ошибки, например, ввод 160 ГБ вместо 16 ГБ. Второй вариант: эксплуатант ставит заявку под сомнение и тем самым запускает цепную реакцию по выяснению причин и последствий запрашиваемых изменений, да так, что иногда уже приходится вмешиваться начальству.

Теперь посмотрим, как это делается в GitOps. Запрос на изменения попадает в репозиторий git и превращается в pull-запрос. После чего разработчик может выставить этот pull-запрос (особенно, если это изменения в продакшн-среде) для утверждения причастными сторонами. Таким образом, специалисты по безопасности могут подключиться уже на ранней стадии, и всегда есть возможность отследить последовательность изменений. Стандарты в этой области можно внедрять программно, используя соответствующие средства в инструментальной цепочке CI/CD. После того, как его утвердили, pull-запрос версионируется и легко поддается аудиту. Кроме того, его можно протестировать в среде pre-production в рамках стандартного процесса, полностью устранив риск человеческой ошибки.

Как видим, изменения радикальные. Но в новинку они будут не столько разработчикам, которым не привыкать к системам управления версиями, сколько системным администраторам и специалистам по безопасности. Но как только те вникнут в новую парадигму и оценят ее силу и простоту, идея зайдет на ура.

6. Схемы приложений (Blueprints)


Переход от монолитных приложений к микросервисам усиливает роль шаблонов проектирования (паттернов) приложений. В самом деле, типичное монолитное приложение не особо поддается классификации. Как правило, там есть и REST API, и пакетная обработка, и событиями оно управляется. HTTP, FTP, kafka, JMS и Infinispan? Да пожалуйста, а еще оно одновременно работает с тремя разными базами данных. И как прикажете создавать схему, когда здесь намешана целая куча шаблонов интеграции корпоративных приложений? Да никак.

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

  • REST API для управления данными в СУБД.
  • Пакетная обработка, которая проверят FTP-сервер на предмет обновления данных и отправляет их в топик kafka.
  • Camelадаптер, берущий данные из этого kafka-топика и отправляющий их в REST API
  • REST API, которые выдают обобщенную информацию, собираемую из Data Grid, которая действует как конечный автомат.

Итак, теперь у нас есть схемы, а схемы уже можно стандартизировать. REST API должны отвечать стандартам Open API. Пакетные задания будут управляться как пакетные задания OpenShift. Интеграции будут использовать Camel. Схемы можно создавать для API, для пакетных заданий, для AI/ML, для multicast-приложений, да для чего угодно. А затем уже можно определять, как развертывать эти схемы, как их конфигурировать, какие шаблоны использовать. Имея такие стандарты, не надо будет каждый раз изобретать колесо, и вы сможете лучше сфокусироваться на действительно важных задачах, вроде создания нового бизнес-функционала. Проработка схем может показаться пустой тратой времени, но затраченные усилия сторицей вернутся в будущем.

7. Подготовьтесь к API


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

Во-первых, здесь опять понадобятся стандарты. В качестве отправной точки можно взять стандарты Open API, но придется углубиться в дебри. Хотя здесь важно соблюсти баланс и не впасть в чрезмерную зарегламентированность с кучей ограничений. Посмотрите на эти вопросы: когда новая сущность создается с помощью POST, что надо возвращать, 201 или 200? разрешается ли обновлять сущности с помощью POST, а не PUT? В чем разница между 400-ми и 500-ми ответами? примерно такой уровень детализации вам нужен.

Во-вторых, понадобится сервисная сетка service mesh. Это реально сильная вещь и со временем она станет неотъемлемой частью Kubernetes. Почему? Потому что трафик рано или поздно превратится в проблему, и вам захочется управлять им как внутри дата-центра (т.н. трафик восток-запад), так и между дата-центром и внешним по отношению к нему миром (север-юг). Вам захочется вытащить из приложений аутентификацию и авторизацию и вывести их на уровень платформы. Вам понадобятся возможности Kiali по визуализации трафика внутри service mesh, а также сине-зеленые и канареечные схемы развертывания приложений, или, к примеру, динамический контроль трафика. В общем, service mesh без вопросов входит в категорию задач первого дня.

В-третьих, вам понадобится решение для централизованного управления API. Вам захочется иметь одно окно для поиска и повторного использования API. Разработчикам понадобится возможность зайти в магазин API, найти там нужный API и получить документацию по его использованию. Вы захотите единообразно управлять версиями и deprecation-ами. Если вы создаете API для внешних потребителей, то такое решение может стать конечной точкой север-юг во всем, что касается безопасности и управления нагрузкой. 3Scale даже может помочь с монетизицией API. Ну и рано или поздно ваше руководство захочет получить отчет, отвечающий на вопрос Какие у нас есть API?.

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

Однако, надежда есть, и она заключается в автоматизации. Любой из перечисленных выше стандартов можно внедрить с помощью автоматизации. Процесс GitOps может проверять, что во всех соответствующих yaml-файлах присутствуют все требуемые метки и аннотации. Процесс CI/CD может контролировать соблюдение стандартов на корпоративные образы. Все может быть кодифицировано, проверено и приведено в соответствие. Кроме того, автоматизацию можно доработать, когда вы вводите новые стандарты или меняете существующие. Безусловное преимущество стандартизации через автоматизацию заключается в том, что компьютер не избегает конфликтов, а просто констатирует факты. Следовательно, при достаточной проработанности и инвестициях в автоматизацию, платформа, в которую вы вкладываете столько средств сегодня, может принести гораздо больший возврат инвестиций в будущем в виде повышения производительности и стабильности.
Подробнее..

Почему JVM это ОС и больше чем Кубер

05.01.2021 08:15:09 | Автор: admin

Я давно работаю с платформой Java и прекрасно знаю её сильные и слабые стороны. В этой статье я хочу рассказать, как могла бы повернуться история, если бы не бы. Ведь мы могли бы вместо докер-систем использовать ява-машину. А сама ява-машина вполне могла целиком заменить ОС.

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

Итак Ява-машина это ОС. Даже круче чем ОС местами. На самом деле это не такое уж заявление из ряда вон. Ведь всем прекрасно известен пример полноценной ОС, значительно основанной (изначально) на Ява Андроид. Кроме того, существуют и ОС в классическом понимании полностью на базе JVM.

Итак, какие признаки ОС мы имеем у JVM? Управление памятью - несомненно. Управление потоками - да, но как правило на базе существующих местных потоков базовой ОС. Тем не менее, потоки являются важной неотъемлемой и очень развитой подсистемой машины, предоставляя гораздо больше сервисных средств, чем базовые потоки ОС.

Ввод-вывод также очень развит, если иметь в виду всю инфраструктуру Ява, со всеми серверами и библиотеками. В этом смысле ввод-вывод базовой ОС - примерно как старый Биос для последней, осуществляет низкоуровневые операции.

У Ява есть философия. Если в Юникс - всё файл, то в Ява всё (почти) есть объект.

Есть важная часть системы, про которую многие либо не знают, либо забывают. Ява среда с мощнейшими средствами разграничения доступа. Именно поэтому в том числе её широко применяют в банковской сфере.

Наличие этих средств вкупе с полноценной многопоточностью на уровне языка создаёт предпосылки для создания многозадачной И многопользовательской среды исполнения. Про многопоточность знают многие. Что касается разграничения доступа, остановимся подробнее.

Во-первых, JVM управляемая (managed) среда. Это не только означает безопасность исполнения кода. Это также модель разграничения, аналогичная выделению ядра в большинстве ОС в отдельный контекст привилегированного исполнения. Т.н. нативный контекст исполнения, в котором работает сама машина - прямой аналог реального (или подобного) режима исполнения процессором ядра ОС. Сама машина имеет полный контроль над всеми процессами внутри неё. Байткоду достается уже сильно ограниченная, защищённая среда. Степень свободы загружаемого байткода определяется Ява-машиной и её рантайм-библиотекой. Более того, сам механизм загрузки байткода (классов в первую очередь) иерархичен и подразумевает разделение прав и ответственности ветвление прав. Это ветвление достигается за счёт применения отдельных загрузчиков классов. При этом создаётся иерархия областей видимости, код, загруженный в одном контексте не имеет доступа к другому независимому контексту. При этом нельзя получить указатель на произвольную область памяти, нет доступа к произвольным полям объектов, даже через механизм рефлексии, даже к целым отдельным объектам. Этот механизм встроен в язык (ключевые слова private, protected и т.п.) и в платформу уже названные загрузчики и конечно менеджеры безопасности, о которых тоже не забудем. Такие механизмы обеспечивают разделение контекстов выполнения аналогично процессам классических ОС. Я бы даже сказал более строгое и надёжное разделение.

Загрузчики классов совместно с менеджерами безопасности (SecurityManager) обеспечивают полный контроль над тем, что может попасть внутрь среды исполнения Ява, а что не может. Механизм этот необычайно гибкий. При этом, в отличие от нативного кода, загружаемый байткод проходит полную проверку на валидность (он не может затем вызвать непредсказуемый сбой) и безопасность - так как возможные варианты поведения ограничены теми же загрузчиком+менеджером безопасности. Вы слышали когда-нибудь о вирусах на Яве?

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

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

Круче чем докер

Почему Ява-машина круче чем докер? Потому что, как я уже сказал, она позволяет создавать множество независимых изолированных контекстов читай пространств имён на базе общего ядра Ява-машины и рантайм-библиотеки. И эти пространства могут настраиваться более гибко, обеспечивая различные уровни изоляции (доступа). Они могут дополнительно компоноваться. Отличным примером этого являются мощные сервера приложений. Они предлагают всё то, что обещает нам докер и его оркестраторы отказоустойчивость, балансировка нагрузки, отличная модульность (суть микросервисности), сервисы и вебсервисы и даже самые микро и нано-модули лямбды в виде супер легковесных ejb, решение проблем совместимости версий библиотек, удалённый вызов процедур в качестве альтернативы protoBuf & gRPC RMI и его развития Corba + rmi-iiop. И это всё в виде стандартов и множества реализаций

Единственное, чего не хватает - красивых картинок и графиков (хотя тут от реализации зависит) и развертывания инфраструктуры из нарисованной диаграммки. Но последнее и в коробку с Кубером никто бесплатно не положит.

И для иллюстрации посмотрим на стандартную модульность сервера приложений. Есть иерархия загрузки: система -> сервер -> приложение -> модуль приложения.

Что ж, на этом пока всё. Порадуемся выходу очередной версии Jakarta EE 9 и пожелаем им успехов.

Подробнее..

Программа Joker 2020 Java изнутри и снаружи

13.10.2020 12:18:27 | Автор: admin


До конференции Joker меньше полутора месяцев, и пришло время рассказать Хабру, о чём будут её доклады.


Если говорить в целом, то так. Помимо докладов, будут воркшопы: они хорошо подходят онлайн-формату. Будут интересные новые спикеры вроде Питера Лори (на Stack Overflow второй в мире по тегу jvm). Конечно, будут и хорошо знакомые имена: Тагир Валеев, Евгений Борисов и не только. Докладов по Spring в этот раз набралось на целый блок.


А за конкретикой приглашаем под кат там описан каждый доклад.


Чтобы быть уверенными, что конференция будет полезной, про каждый доклад мы задаём программному комитету несколько вопросов:


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

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


А вот блоки, на которые можно поделить программу:


Вне категории
Воркшопы
VM/runtime
Тулинг и фреймворки
Spring
Языки
и не только


Вне категории


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


Novel but practical uses of Java, Peter Lawrey


Если вы пользуетесь Stack Overflow, возможно, Питер Лори уже не раз вам помог: он один из топовых отвечающих по тегам jvm и java. А ещё он архитектор опенсорсных библиотек OpenHFT про Chronicle Queue и Chronicle Map вы могли слышать. Но, в отличие от многих других Java-звёзд, Питер ни разу не прилетал на российские конференции. К счастью, для участия в онлайн-формате лететь не требуется, и теперь он впервые выступит на Joker.


Доклад будет как раз связан с OpenHFT. В этих библиотеках не раз прибегали к нестандартному использованию Java, пользуясь довольно экзотическими фичами. И на Joker Питер расскажет в подробностях об этой экзотике.




Заменят ли роботы программистов, Тагир Валеев


С Идеей кто к нам входит в дом? Джависту каждому знаком? Если Питер Лоури будет на Joker впервые, то доклады Тагира Валеева (lany) здесь давно знают и любят. Только недавно мы расшифровали его предыдущий доклад о маленьких оптимизациях в Java 9-16, а у него уже готов новый: в этот раз на более общую тему, чем обычно.


Разработчики любят всё автоматизировать. Не случится ли в итоге так, что софт начнёт писать себя сам, а мы все окажемся на улице? Может показаться, что раз этой дискуссии десятки лет, любое новое высказывание это воду в ступе толочь. Но интересно, что Тагир работает над инспекциями в IDE то есть как раз ему должно быть лучше других видно, что машины могут понять про код, а что не могут.




Воркшопы


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


Хватит писать тесты, пора писать спецификации, Алексей Нестеров


В этом воркшопе-стриме-live-сессии Алексей покажет, как с нуля создать и запустить TDD-цикл для Spring Boot-приложения, с примерами на JUnit 5.


Чем интересна тема: Все любят рассказывать, как нужно тестировать, насколько полезно TDD. А вот показать, да так, чтобы было понятно, сложная задача. В этом воркшопе участники смогут ощутить реальную пользу от написания тестов, на практике познакомиться с подходами TDD на примере Spring-приложений и даже обсудить со спикером тонкости и скользкие моменты.
Чем ценен спикер: У Алексея огромный опыт в разработке enterprise приложений на Java. Он разработчик в Spring Cloud Services в VMware, долгое время занимался консалтингом, и точно знает, как нужно писать и тестировать приложения на современных Java0технологиях. Алексей может быть известен широкой публике как один из голосов подкаста Радио-Т.




GraalVM, Thomas Wuerthinger / Олег Шелаев


Чем хороша тема: GraalVM один из самых интересных проектов в экосистеме, которая может повлиять на практически все проекты. От улучшения производительности до популяризации АОТ-компиляции, до открытия экосистем других языков программирования. Каждый должен знать хотя бы базовые вещи про GraalVM. Поэтому воркшоп, где участники могут потрогать GraalVM сами и задать вопросы людям из команды GraalVM отличная возможность для этого.
Чем ценны спикеры: Томас оригинальный автор GraalVM-компилятора, основатель проекта и его лидер. А Олег, пожалуй, больше всех в мире рассказывает про GraalVM, поэтому знает, какие практические вопросы возникают у людей и какие объяснения оказываются самыми понятными. В общем, кто, если не они?




VM/runtime


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


Have you really taken the time to know me: A G1 GC saga, Monica Beckwith


Многие заинтересовались сборщиком мусора G1, когда в Java 9 он стал дефолтным. Но с тех пор легко было не следить за его развитием, а со времён девятки в нём появилось немало нового.


Моника Беквит (обладательница звания Java Champion, эксперт в области GC) поможет наверстать, рассказав всё. Тем, у кого в продакшне Java 11+, доклад может моментально помочь правильнее подкрутить ручки, а остальным даст полезную информацию на будущее.




Project Loom: Modern scalable concurrency for the Java platform, Alan Bateman


Чем интересна тема: Эффективное использование многочисленных ядер современных процессоров сложная, но всё более важная задача. Java была одним из первых языков программирования со встроенной поддержкой concurrency. Ее concurrency-модель, основанная на нативных тредах, хорошо масштабируется для тысяч параллельно выполняющихся стримов, но оказывается слишком тяжеловесной для современного реактивного программирования с сотнями тысяч параллельных потоков.


Ответ на эту проблему Project Loom. Он определяет и реализует в Java новые легковесные параллельные примитивы.


Чем ценен спикер: Алан Бейтман, руководитель проекта OpenJDK Core Libraries Project, потратил большую часть последних лет на проектирование Loom таким образом, чтобы он естественно и органично вписывался в богатый набор существующих библиотек Java и парадигм программирования.




Thread Safety with Phaser, StampedLock and VarHandle, Heinz Kabutz / John Green


Чем хороша тема: Компьютер многое умеет делать параллельно, а программистам интересно выжимать из компьютеров максимальные возможности.
Чем ценны спикеры: Хайнц известен многим любителям Java как автор популярного блога javaspecialists.eu, его доклады всегда в топе мировых Java-конференций. С докладом выступает совместно со своим коллегой Джоном Грином.
Кому будет полезно: Программистам, заинтересованным в многопоточном программировании на JVM.




Coordinated Restore at Checkpoint: Быстрый старт OpenJDK, Антон Козлов


Цель проекта Coordinated Restore at Checkpoint запускать Java-приложения за десятки миллисекунд. Проект не связан с GraalVM Native Image, поэтому не имеет свойственных ему проблем для использования, зато приносит новые. Сейчас он в активной разработке.


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


Чем интересна тема: Тема быстрого старта Java-процесса проходит красной нитью по всем докладам 2020 года. То Micronaut, то Spring все как один говорят, что они легковесные, быстрозапускаемые и т.п.
Чем ценен спикер: Антон является автором/коммитером технологии, про которую говорит.




JVM-профайлер, который смог (стать кроссплатформенным), Кирилл Тимофеев


В JetBrains пару лет назад добавили поддержку async-profiler для Mac и Linux. Начали им пользоваться и поняли, что нужен async-profiler, работающий на Windows. На Linux и Mac async-profiler использует механизм POSIX-сигналов и нативную раскрутку стеков. Команде нужно было разобраться, как конкретно работает AsyncGetCallTraces, сравнить его внутреннее устройство с JFR. А если окажется, что их устраивает работа AsyncGetCallTraces, то нужно научиться эмулировать механизм сигналов и раскручивать нативные стеки. Кроме этого нужно решить разные ОС-специфичные проблемы, которые возникнут по пути.


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




Docker Who: Маленькие контейнеры сквозь время и пространство, Дмитрий Чуйко


Ваш проект недавно перешел на микросервисную архитектуру? Деплоймент значительно ускорился, но и затраты от передачи данных выросли? Размер самих контейнеров здесь важнее всего. Уменьшить их значит сильно сэкономить на трафике.


Инженеры BellSoft разработали решение полной поддержки Alpine Linux в OpenJDK. А вместе с этим и настоящий ТАРДИС: контейнеры, которые занимают на диске несколько мегабайт, но внутри несут огромный потенциал. С выходом JDK 16, в рамках JEP 386 проект Portola интегрируется в основную ветку OpenJDK. Необходимость в костыльном слое glibc отпадет, все процессы встанут на свои места. Ваша компания сможет пользоваться крошечными образами контейнеров вне зависимости от поставщика дистрибутива. Они доступны уже давно, но официальный статус порта HotSpot для библиотеки musl расширит область его применения и упростит разработки.


В своем докладе Дмитрий (BellSoft) расскажет, какие преимущества принесет поддержка Alpine Linux сообществу OpenJDK и объяснит, как бесплатно оптимизировать Docker образы, поменяв всего пару строк кода. А в конце предложит инструмент для выбора оптимального контейнера под нужды вашего проекта.




Тулинг и фреймворки


Тут, в отличие от блока VM/runtime, никаких вопросов о применимости сразу не возникает: это о том, чем Java-разработчики непосредственно пользуются. Кто-то из спикеров лично создал технологию, о которой будет рассказывать, а кто-то не имеет к ней личного отношения, зато отлично умеет ей пользоваться.


Hidden pearls for high-performance-persistence in Java, Sven Ruppert


В этом выступлении Свен покажет несколько технологических стеков, помогающих справляться с персистентными данными, не заставляя иметь дело с классическими технологическими монстрами горизонтального масштабирования, такими как Kubernetes, Hadoop и многими другими.


Чем интересна тема: Вопрос о том, как разместить данные из памяти JVM в более персистентное хранилище, всегда актуален.
Чем ценен спикер: Свен опытный специалист: говорит чётко, понятно и по теме. А ещё у него очень красивые предзаписи скоро сами увидите!
Кому будет полезно: Будет полезно большинству практикующих инженеров доклад расширит кругозор о доступных решениях для persistence.




Keeping growing software projects under control with Gradle, Jendrik Johannes


Доклад рассмотрит на практическом примере, как Gradle могает справляться со сложностями, возникающими при росте проекта.


Чем интересна тема: Чем больше проект, тем сложнее скрипты сборки. Бытует миф, что Gradle это всегда императивные скрипты, и что разобраться в них невозможно. Мифы нужно развенчивать. Spring Framework, Spring Boot, Hibernate ORM, Micronaut Core, Kotlin что объединяет все эти проекты? Правильно: они собираются Gradle-скриптами.
Чем хорош спикер: Работает в Gradle, видел много разных Gradle-скриптов, и он автор проекта Idiomatic Gradle, где показаны подходы по разделению скрипта сборки на независимые части.
Почему здесь и сейчас: Это новый доклад. В очередной версии Gradle 6.7 как раз идут улучшения и в отношении возможностей, и в отношении документации для многомодульных проектов, поэтому самое время узнать из первых уст и попробовать на практике.




Аерон. High performance-транспорт для low latency-микросервисов, Иван Землянский


Aeron новый транспорт от Real Logic, в состав которого входит небезызвестный автор disruptor Мартин Томпсон. После многих лет работы в сфере HFT он собрал команду и сделал новый транспорт с минимальными накладными расходами.


В докладе Иван хочет раскрыть границы применения этой технологии, основные моменты использования. Вместе мы постараемся понять, какие именно аспекты реализации дают такой хороший перфоманс.




Kafka streams testing: A deep dive, John Roesler / Иван Пономарёв


Хотя Kafka Streams поставляется в комплекте с TopologyTestDriver, что делает юнит-тестирование стриминговых приложений весьма приятным, все не так просто: есть ограничения и определенные классы дефектов, которые данная технология игнорирует. С помощью интеграционных тестов можно проверить поведение реального кластера, но это еще менее просто: мы вступаем на скользкий путь асинхронных тестов с таймаутами, ненадежностью и неразрешимыми вопросами.


Вывод: необходимо использовать оба подхода и голову.


Чем ценен спикер: Кто лучше может рассказать о правильном использовании технологии, чем один из коренных ее разработчиков John Roesler и инженер со шрамами от ее использования Иван Пономарёв?
Чем интересна тема: Ложное чувство уверенности, что твой код протестирован и готов для выкладки на бой, может привести к катастрофическим последствиям, если не знать, как правильно писать тесты для нетривиальной технологии. Этот доклад как раз об этом.
Кому будет полезно: Всем, кто использует Kafka Streams и любит себя достаточно, чтобы писать тесты на свой код.




Change data capture pipelines with Debezium and Kafka streams, Gunnar Morling


В докладе вместе с Гуннаром мы выведем CDC на новый уровень, изучая преимущества интеграции Debezium с потоковыми запросами через Kafka Streams.


Чем интересна тема: Комбинацией Kafka Streams, Debezium, Quarkus
Чем ценен спикер: Доклад от создателя Debezium
Кому будет полезно: Всем энтерпрайз-разработчикам.




Как мы делали SQL в Hazelcast, Владимир Озеров


В этом докладе Владимир расскажет, как он и его команда создавали Hazelcast Mustang новый распределенный SQL-движок, который появился в последней версии Hazelcast IMDG. Будут подробно рассмотрены ключевые технические решения, принятые командой, затрагивающие оптимизатор, сетевой протокол и параллельное выполнение запросов.


Чем интересна тема: Задача подключать SQL к своему приложению довольно актуальна. За недавнее время SQL-запросы появились (или существенно доработались) у очень многих продуктов: Hazelcast, Apache Ignite, Kafka, и т.п. В Apache Calcite сейчас довольно бурно идёт работа от разных вендоров.
Чем ценен спикер: Владимир принимал непосредственное участие в реализации SQL в Hazelcast.
Кому будет полезно: Тем, кто интересуется, как прикручивать SQL-движки к своим хранилищам, особенно на базе Apache Calcite.




Writing test driven apps with http4k, David Denton & Ivan Sanchez


Чем интересна тема: Тема микрофреймворков популярна, и зачастую возиникает вопрос: А нужно ли тащить Spring Boot / Micronaut / далее по списку ради простейшего сервиса? И, правильно, нужно это не всегда. Библиотека http4k интересна по множеству факторов: легковесность, Kotlin, тестируемость.
Чем ценны спикеры David Denton и Ivan Sanchez это два ключевых разработчика в проекте http4k. Они могут и хорошо рассказать, и на вопросы про свой опыт ответить.
Кому будет полезно: На доклад стоит идти тем, кто видел Kotlin и ещё не использовал http4k. Те, кто уже использовал http4k не факт, что узнают много нового именно на самом докладе, но им, безусловно, стоит заглянуть, т.к. задать вопросы авторам и решить свою проблему это дорогого стоит.




Spring


Доклады, так или иначе связанные со Spring, можно было бы включить в блок тулинг/фреймворки. Но в этот раз их набралось столько, что это тянет на отдельный блок!


Spring Patterns для взрослых, Евгений Борисов


Тем, кто работает со Spring, представлять Евгения Борисова не требуется его майский доклад Spring-построитель набрал уже больше 40 000 просмотров. Распотрошив Spring и собрав его заново, теперь он наконец научит нас правильно им пользоваться. На фестивале TechTrain была лайтовая версия этого доклада, а теперь будет для взрослых.


Сколько дизайн-паттернов вы знаете? 24? 36? 100? А сколько из них вы применяете в реальной жизни? 3? 5? 10?


Евгений покажет, как при помощи Spring можно легко и просто реализовывать наиболее популярные паттерны, с которыми мы сталкиваемся в повседневной жизни. Chain of responsibility, strategy, command, lazy initialization, scala traits, AOP, proxy, decorator, и прочие паттерны и концепции, внедренные при помощи Spring, сделают ваш код мягким и шелковистым. А перхоть вашего boilerplate в виде switch-ей, статических методов, наследования, и прочей устаревшей шелухи, посыпется с вашего проекта под радостные крики сонара. Код станет более читабельным, гибким и поддерживаемым. Такой код проще обкладывать тестами и, наконец, это просто красиво.




The path towards Spring Boot native applications, Sbastien Deleuze


Чем интересна тема: GraalVM native image очень модный способ для улучшения производительности Java в serverless или маленьких эфемерных контейнерах. Об этом все говорят, но так как все используют Spring, который изначально сталкивался с некоторыми трудностями конфигурации динамических частей для работы в native image, широко технология ещё не применяется. Этот доклад рассказывает про текущую разработку поддержки GraalVM native image в Spring-приложениях, важных моментах в этой интеграции, как подходить к запуску Spring на native image, что сейчас работает и когда будет работать остальное.
Чем ценен спикер: Кто, если не Себастьян? Лид проекта Spring GraalVM native, коммитер Спринг и один из двоих людей (второй Энди Клемент), лучше всего понимающих, как и почему работает (или ещё не работает) эта комбинация.
Кому будет полезно: Если вы используете Spring и деплоите приложения в облака вам стоит посетить этот доклад. Если вы когда-нибудь думали, а не бросить ли Spring и переписать всё на Quarkus, Micronaut и так далее, вам сюда. И, на самом деле, этот доклад очень хорошо помогает понять некоторые детали GraalVM native image, которые помогут вам разобраться с ним даже без Spring.
Почему здесь и сейчас: Джокер в конце ноября, релиз GraalVM 20.3 релиз 18-го, следующий 0.х релиз Spring GraalVM native скорее всего, через пару дней после. То есть новости про это всё будут самыми свежими. Задать вопросы Себастьяну многого стоит! Спринг и native image может вполне когда-нибудь стать каждодневной реальностью к этому надо быть готовым заранее.




Spring: Your next Java micro-framework, Алексей Нестеров


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


Алексей покажет, что вовсе не обязательно бросать горячо любимый Spring Boot, чтобы получить многие фичи, которые предлагают микрофреймворки! Быстрый запуск и перезапуск, LiveReload, запуск и удаленная разработка прямо в контейнере, компиляция в нативный код, конфигурация приложения без аннотаций и многое другое, что вы ожидаете от микрофреймворка.




Rsocket + Spring: A full throttle introduction, Mark Heckler / Олег Докука


Чем интересна тема: Реактивное программирование новый тренд, ну а Спринг всегда в моде! Марк держит руку на пульсе первого и является экспертом второго. Он поделится инсайдами в работе свеженького реактивного протокола RSocket в Спринге.
Чем ценны спикеры: Марк Developer Advocate в Spring, и его задача разбираться во всем, что так или иначе связано с данным фреймворком. А у Олега на наших конференциях был уже целый ряд успешных докладов, связанных с реактивным программированием. Раз тут тема одновременно про Spring и реактивщину, тут они явно нашли друг друга!




Spring Boot fat JAR: Тонкие части толстого артефакта, Владимир Плизга


Одна из известнейших фич Spring Boot упаковка целого приложения в т.н. толстый JAR, который потом just runs. Это реально работает, и для многих ситуаций этого достаточно. Но если вы не доверяете магии и/или столкнулись с проблемами при развертывании толстого JAR, то вам пора вникнуть в устройство этого механизма.


И тут выясняется, что just runs обходится далеко не бесплатно: есть ограничения по загрузке классов, вопросы к скорости запуска, конфликты со встроенными утилитами JDK, отличия в режимах dev/test/prod, а в некоторых случаях применение этой фичи и вовсе излишне.


В этих и других тонкостях толстого JAR мы и разберемся в докладе. Заглянем в его устройство и поймём, в каких случаях он хорош, а в каких лучше обойтись без него (и что тогда выбрать вместо). Особое внимание уделим развертыванию в контейнерах.


Доклад рассчитан на практикующих инженеров, поставляющих приложения на Spring Boot в production.




Работа с шардированными данными в памяти со вкусом Spring Data, Алексей Кузин


Spring Data мощная и широко используемая часть фреймворка Spring, которая предоставляет абстракции поверх работы с разными хранилищами данных. В сети есть масса информации по использованию готовых модулей Spring Data, но для внесения изменений или разработки своего модуля приходится глубоко залезать в исходники. На примере коннектора для Tarantool Алексей покажет, как разработать свою реализацию коннектора для распределённого хранилища данных в памяти, какие проблемы с производительностью могут возникнуть, и как их решать.


Доклад предназначен для тех, кто интересуется деталями реализации фреймворка Spring и хочет узнать больше о деталях работы с NoSQL БД.


Чем ценен спикер: У Алексея огромный опыт в Java, и огромный опыт с базой Tarantool. Он разработчик интеграции SpringData Tarantool, и получается рассказ из первых уст.




JVM-языки


Может ли Java-конференция обойтись без холивара за каким JVM-языком будущее? Если серьёзно, то мы не за пустые холивары, а за обоснованный конструктив и за то, чтобы уделять внимание всей экосистеме.


Интервью и Q&A: Эволюция Java и Kotlin. Что нас ждет?, Роман Елизаров


Java эволюционирует. Каждые полгода мы видим какие-то новые фишки, как например: text blocks, sealed classes, records, switch instanceof все те удобства, которые раньше были доступны только пользователям других, более современных языков на JVM, например, Kotlin.


Что же будет делать Kotlin? Сидеть и ждать, надеясь на те удобства, где Java не сможет его никогда догнать, или же будет продолжать двигаться вперед? Что можно еще улучшать в языке, который вырос благодаря массивному упрощению количества воды, которую приходится писать в Java-коде? Есть ли запас? От чего вообще страдают программисты и где язык програмирования может улучшать их жизнь?


Об этом всём мы расспросим Романа Елизарова, который отвечал и за дизайн корутин в Kotlin, и за доклады о них: например, его доклад с JPoint набрал больше 20 000 просмотров.




Back from the 70s the Concurnas concurrency model!, Jason Tatton


Concurrency это трудно, а справляться с concurrency в гетерогенных средах CPU/GPU еще сложнее! Это относится и к Java со ее встроенной поддержкой concurrency-программирования. Concurnas это новый и свежий подход к решению этой проблемы на JVM.


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


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




и не только


А кроме всего перечисленного, есть ещё и такие доклады:


  • Ещё не анонсированные: сейчас остаются несколько последних слотов. Чтобы узнать их судьбу, можно заглядывать на сайт Joker или подписаться на конференцию в соцсетях (Telegram, ВК, FB, Twitter).


  • С соседних конференций. Тут такая история. Мы проводим целый сезон из 8 IT-конференций по разным темам (от тестирования до DevOps). И видим, что многим интересно разное: С Java-конференции хочу многое послушать, с девопсовой кое-что тоже, а с .NET-конференции послушал бы про Domain-Driven Design. Поэтому мы сделали билет-абонемент Full Pass, дающий доступ ко всем конференциям сезона сразу: конечно, никто не станет смотреть все восемь от и до, но вот выборочно подключаться к интересным докладам можно.



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

Подробнее..

Релиз Spring Native Beta

13.03.2021 20:20:43 | Автор: admin

Недавно команда, занимающаяся портированием Spring для GraalVM, выпустила первый крупный релиз - Spring Native Beta. Вместе с создателями GraalVM они смогли пофиксить множество багов как в самом компиляторе так и спринге. Теперь у проекта появилась официальная поддержка, свой цикл релизов и его можно щупать :)


Самым главным препятствием при переносе кода из JVM в бинарники является проблема использования фишек, присущих только java - рефлексия, работа с classpath, динамическая загрузка классов и т.д.

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

  • Статический анализ всего приложения выполняется во время сборки.

  • Неиспользуемые компоненты удаляются во время сборки.

  • Рефлексия, ресурсы и динамические прокси могут быть настроены только с помощью дополнительных конфигураций.

  • На время сборки фиксируются все компоненты в Classpath.

  • Нет ленивой загрузки класса: при загрузке все, что поставляется в исполняемых файлах, будет загружено в память. Например, чтобы вызов Class.forName ("myClass") отработал верно, нужно иметь myClass в файле конфигурации. Если в файле конфигурации не будет найден класс, который запрашивается для динамической загрузки класса, будет выбрано исключение ClassNotFoundException

  • Часть кода будет запущена во время сборки, чтобы правильно связать компоненты. Например, тесты.

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

В ходе исследований был создан новый компонент Spring AOT, который отвечает за все необходимые преобразования вашего кода в удобоваримый для Graal VM формат.

Spring AOT анализирует код и на основе него создает файлы конфигурации такие как native-image.properties, reflection-config.json, proxy-config.json или resource-config.json.

Так как Graal VM поддерживает первоначальную настройку через статические файлы, эти файлы помещаются при сборке в каталог META-INF/native-image.

Для каждого сборщика выпущен свой плагин, который активирует работу Spring AOT. Для maven это spring-aot-maven-plugin, соответственно для gradle - spring-aot-gradle-plugin.Для того, чтобы добавить gradle плагин в свой проект нужна всего одна строка:

plugins {id 'org.springframework.experimental.aot' version '0.9.0'}

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

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

Например, для случаев реализации компонентов с помощью WebClient можно использовать аннотацию из пакета org.springframework.nativex.hint, чтобы указать какой тип мы будем обрабатывать:

@TypeHint(types = Data.class, typeNames = "com.example.webclient.Data$SuperHero")@SpringBootApplicationpublic class WebClientApplication {// ...}

Здесь мы указываем, что будем сериализовать класс Data, в котором есть подкласс SuperHero. Во время сборки для нас заранее создадут клиент, который сможет работать с этим типом данных.

Так как graavlvm не поддерживает работу с динамическими прокси, то для поддержки работы с java.lang.reflect.Proxy создана аннотация @ProxyHint.

Применять ее можно, например, так:

@ProxyHint(types = {     org.hibernate.Session.class,     org.springframework.orm.jpa.EntityManagerProxy.class})

Если необходимо подтянуть какие-либо ресурсы в образ, то необходимо воспользоваться аннотацией@ResourceHint.Например, таким образом:

@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties")

Чтобы указать какие классы / пакеты должны быть инициализированы явно во время сборки или выполнения, нужно воспользоваться аннотацией @InitializationHint:

@InitializationHint(types = org.h2.util.Bits.class,    initTime = InitializationTime.BUILD)

Для того, чтобы компактно собрать все эти аннотации воедино создана аннотация @NativeHint:

@Repeatable(NativeHints.class)@Retention(RetentionPolicy.RUNTIME)public @interface NativeHint

Все вместе это будет выглядеть, например, вот так:

@NativeHint(    trigger = Driver.class,    options = "--enable-all-security-services",    types = @TypeHint(types = {       FailoverConnectionUrl.class,       FailoverDnsSrvConnectionUrl.class,       // ...    }), resources = {@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties"),@ResourceHint(patterns = "com.mysql.cj.LocalizedErrorMessages",                      isBundle = true)})

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

Все активные аннотации учитываются во время компиляции и преобразуются в конфигурацию Graal VM плагином Spring AOT.

Spring Native уже включена в релизный цикл, забрать шаблон можно прямо со start.spring.io. Так как поддержка JPA и прочих spring компонентов уже реализована, то собрать простое CRUD приложение можно сразу. Если необходимо указать дополнительные параметры Graal VM при сборке, их можно добавить с помощью переменной среды BP_NATIVE_IMAGE_BUILD_ARGUMENTS в плагине Spring AOT, если сборка идет через Buildpacks, или с помощью элемента конфигурации <buildArgs> в pom.xml, если вы собираете через плагин native-image-maven-plugin.

Собственно, выполняем команды mvn spring-boot: build-image или gradle bootBuildImage - и начнется сборка образа. Стоит отметить, что сборщику нужно более 7 Гб памяти, для того сборка завершилась успешно. На моей машине сборка, вместе с загрузкой образов заняла не более 5 минут. При этом образ получился очень компактным, всего 60 Мб. Стартовало приложение за 0.022 секунды! Это невероятный результат. Учитывая, что все большее количество компаний переходит на K8s и старт приложения, так же как и используемые ресурсы очень важны в современном мире, то данная технология позволяет Spring сделать фреймворком номер один для всех типов микросервисов, даже для реализаций FaaS, где очень важна скорость холодного старта.

Подробнее..
Категории: Микросервисы , Java , Spring , Graalvm , Jvm , Graal

Как мы переходили на Java 15, или история одного бага в jvm длинной в 6 лет

12.02.2021 10:14:08 | Автор: admin

Мы готовились к выходу Java 15 ради некоторых её новых возможностей. В частности текстовых блоков. Да, они появились в Java 14 (о новых функциях в Java 14 можно посмотреть здесь), но только как превью-фича, а, начиная с Java 15, она стала доступна в виде окончательно готовой функции.

Мы в hh.ru привыкли внедрять и использовать самые современные технологии в разработке ПО. Пробовать что-то новое одна из ключевых задач команды Архитектура. Пока многие пишут на Java 8, мы уже близки к тому, чтобы отправить на свалку истории Java 11.

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

Переезд с Java 14 на Java 15. Что-то пошло не так

Дождавшись выхода новый Java, мы приступили к переезду. Не мудрствуя лукаво, выбрали один из нагруженных сервисов, который уже крутился на Java 14. В теории никаких сложностей при переходе не должно было возникнуть, на практике так и получилось. Обновление Java 14 на Java 15 не тоже самое, что обновление Java 8 на Java 11.

hh и в продакшн сервис обновлён, работа выполнена. Что дальше? А дальше мониторинг работы. Для сбора метрик мы используем okmeter. С его помощью мы наблюдали за поведением обновленного сервиса. Никаких аномалий по сравнению с предыдущей версией Java не было, кроме одной нативная память. В частности, зона Code Cache выросла почти в 2 раза!

До конца 17 ноября Java 14, после Java 15До конца 17 ноября Java 14, после Java 15

Для сервиса с большим количеством инстансов каждый мегабайт памяти на счету, и, помимо её резкого роста, на графике просматривается возрастающий тренд. Кажется мы имеем дело с утечкой нативной памяти в Code Cache.

Что такое вообще этот ваш Code Cache?

Code Cache область нативной памяти, где хранится интерпретатор байткода Java, JIT-компиляторы C1 и C2, и оптимизированный ими код. Основным пользователем является JIT. Весь перекомпилированный им код будет сохранятся в Code Cache.

Начиная с Java 9 Code Cache поделен на три отдельных сегмента, в каждом из которых хранится свой тип оптимизированного кода (JEP 197). Но на графике выше видно только одну выделенную область, несмотря на то что там Java 14 и Java 15. Почему одну?

Дело в том, что мы тонко настраивали размеры памяти при переводе сервисов в Docker (о том, как это было, можно почитать тут) и умышленно установили флаг размера Code Cache (ReservedCodeCacheSize) равным 72МБ в этом сервисе.

Три сегмента можно получить двумя путями: оставить значение ReservedCodeCacheSize по умолчанию (256Мб) или использовать ключ SegmentedCodeCache. Вот как эти зоны выглядят на графике с другого нашего сервиса:

Поиск утечки нативной памяти в Code Cache

С чего начать расследование? Первое что приходит на ум использовать Native Memory Tracking, функцию виртуальной машины HotSpot, позволяющую отслеживать изменение нативной памяти по конкретным зонам. В нашем случае использовать Native Memory Tracking нет необходимости, так как благодаря собранным метрикам, мы уже выяснили, что проблема в Code Cache. Поэтому мы решаем сделать следующее запустить инстансы сервиса с Java 14 и Java 15 вместе. Так как у нас уже три дня сервис работает на "пятнашке", добавляем один инстанс на 14-ой.

Мы решаем продолжить поиск утечки с помощью утилит Java. Начнем с jcmd. Так как мы знаем, что "течет" у нас Code Cache, мы обращаемся к нему. Если сервис запущен в Docker, можно выполнить команду таким образом для каждого инстанса:

docker exec <container_id> jcmd 1 Compiler.CodeHeap_Analytics

Получаем два очень длинных и подробных отчета о состоянии Code Cache. Кропотливо сравнив их, мы обратили внимание на следующий интересный факт, связанный с очисткой Code Cache:

// Java 14Code cache sweeper statistics:Total sweep time: 9999 msTotal number of full sweeps: 17833Total number of flushed methods: 10681 (thereof 1017 C2 methods)Total size of flushed methods: 20180 kB// Java 15Code cache sweeper statistics:Total sweep time: 5592 msTotal number of full sweeps: 236 Total number of flushed methods: 11925 (thereof 1146 C2 methods)Total size of flushed methods: 44598 kB

Обратите внимание на количество циклов полной очистки Total number of full sweeps. Вспомним, что сервис на Java 15 работает 3 дня, а на Java 14 всего 20 минут. Но количество полных очисток Code Cache поразительно разнится почти 18 тысяч за 20 минут, против 236 за трое суток.

Как работает очистка Code Cache

Пришло время углубиться в детали. За очистку Code Cache отвечает отдельный поток jvm CodeCacheSweeperThread, который вызывается с определенной эвристикой. Поток реализован как бесконечный цикл while, внутри которого он блокируется, пока не истечет 24-часовой таймаут, либо не будет снята блокировка вызовом:

CodeSweeper_lock->notify();

После того, как блокировка снята, поток проверяет, истек ли таймаут и имеет ли хотя бы один из двух флагов, запускающих очистку Code Cache, значение true. Только при выполнении этих условий, поток вызовет очистку Code Cache методом sweep(). Давайте подробнее разберем флаги:

should_sweep. Этот флаг отвечает за две стратегии очистки Code Cache нормальную и агрессивную. О стратегиях поговорим дальше.

force_sweep. Этот флаг устанавливается в true при необходимости принудительно очистить Code Cache без выполнения условий нормальной и агрессивной стратегий очистки. Используется в тестовых классах jdk.

Нормальная очистка

  1. Во время вызова GC хранящиеся в Code Cache методы могут изменить свое состояние по следующему сценарию: alive -> notentrant -> zombie. Методы не-alive помечаются как "должны быть удалены из Code Cache при следующем запуске потока очистки".

  2. В конце своей работы GC передает ссылку на все не-alive объекты в метод report_state_change.

  3. Далее в специальную переменную bytes_changed инкрементируется суммарный размер объектов, помеченных как не-alive в этом проходе GC.

  4. Когда bytes_changed достигает порога, задаваемого в переменной sweep_threshold_bytes, флаг should_sweep помечается как true и блокировка потока очистки снимается.

  5. Запускается алгоритм очистки Code Cache, в начале которого значение bytes_changed сбрасывается. Сам он состоит из двух фаз: сканирование стека на наличие активных методов, удаление из Code Cache неактивных. На этом нормальная очистка завершена.

Начиная с Java 15 пороговым значением можно управлять с помощью флага jvm SweeperThreshold он принимает значение в процентах от общего количества памяти Code Cache, заданном флагом ReservedCodeCacheSize.

Агрессивная очистка

Этот тип очистки появился еще в Java 9, как один из способов борьбы с переполнением Code Cache. Выполняется в тот момент, когда свободного места в памяти Code Cache становится меньше заранее установленного процента. Этот процент можно установить самостоятельно, используя ключ StartAggressiveSweepingAt, по умолчанию он равен 10.

В отличие от нормальной очистки, где мы ждем наполнения буфера "мертвыми" методами, проверка на старт агрессивной очистки выполняется при каждой попытке аллокации памяти в Code Cache. Другими словами, когда JIT-компилятор хочет положить новые оптимизированные методы в Code Cache, запускается проверка на необходимость запуска очистки перед аллокацией. Проверка эта довольно простая, если свободного места меньше, чем указано в StartAggressiveSweepingAt, очистка запускается принудительно. Алгоритм очистки такой же, как и при нормальной стратегии. И только после выполнения очистки, JIT сможет положить новые методы в Code Cache.

Что у нас?

В нашем случае размер Code Cache был ограничен 72 МБ, а флаг StartAggressiveSweepingAt мы не задавали, значит по умолчанию он равен 10. Если взглянуть на статистику очистки Code Cache, может показаться, что на Java 14 работает именно агрессивная стратегия. Дополнительно убедиться в этом нам помог тот же график, но с увеличенным масштабом:

Java 14Java 14

Он имеет зубчатую структуру, которая говорит о том, что очистка происходит часто, и, вероятно, методы по кругу выгружаются из Code Cache, в следующей итерации JIT-компиляции вновь попадают обратно, после снова удаляются etc.

Но как это возможно? Почему работает агрессивная стратегия очистки? По умолчанию она должна запускаться в тот момент, когда свободного места в Code Cache менее 10%, в нашем случаем только при достижении 65 мегабайт, но мы видим, что она происходит и при 30-35 мегабайтах занятой памяти.

Для сравнения, график с запущенной Java 15 выглядит иначе:

Java 15Java 15

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

Утечка не утечка

Так как работой Code Cache управляет jvm, мы отправились искать ответы в исходниках openJDK, сравнивая версии Java 14 и Java 15. В процессе поисков мы обнаружили интересный баг. Там сказано, что агрессивная очистка Code Cache работает неправильно с того момента, как ее внедрили в Java 9. Вместо старта агрессивной очистки при 10% свободного места, она вызывалась при 90% свободного места, то есть почти всегда. Другими словами, оставляя опцию StartAggressiveSweepingAt = 10, на деле мы оставляли StartAggressiveSweepingAt = 90. Баг был исправлен 3 июля 2020 года. А все дело было в одной строчке:

Этот фикс вошел во все версии Java после 9-ки. Но почему тогда его нет в нашей Java 14? Оказывается, наш docker-образ Java 14 был собран 15 апреля 2020 года, и тогда становится понятно, почему фикс туда не вошел:

Так значит и утечки нативной памяти в Code Cache нет? Просто всё время очистка работала неправильно, впустую потребляя ресурсы cpu. Понаблюдав еще несколько дней за сервисом на Java 15, мы сделали вывод, что так и есть. Общий график нативной памяти вышел на плато и перестал показывать тренд к росту:

скачок на графике - это переход на java 15скачок на графике - это переход на java 15

Выводы

  1. Как можно чаще обновляйте свою Java. Это касается не только мажорных версий, но и патчевых. Там могут содержаться важные фиксы

  2. Разумное использование метрик помогает обнаружить потенциальные проблемы и аномалии

  3. Переходите на Java 15, оно того стоит. Вот тут список всех фич, которые появились в пятнашке

  4. Если вы используете Java 8, то у вас проблемы агрессивной очистки Code Cache нет, за отсутствием этого функционала как такового. Однако существует риск, что Code Cache может переполниться и JIT-компиляция будет принудительно отключена

Подробнее..

JVM в Docker контейнере. Сборник диаграмм по управлению памятью

07.05.2021 12:20:51 | Автор: admin

План статьи:

1. Мотивация
2. Развлекательная часть (можно пропустить):
2.1. Теоретическая ситуация
2.2. Теоретическая проблема
2.3. Теоретическое решение
3. Основная часть:
3.1. Диаграммы
3.2. Заключение

Мотивация

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

Развлекательная часть (Можно пропустить)

Ситуация
Вы ведете разработку ПО, к примеру, для автоматизированного управления плантацией картофеля на планете Марс. ПО должно работать на легких, энерго эффективных портативных устройствах, типа Raspberry Pi. Также данное ПО не должно выходить за рамки выделенных ресурсов, так как на одном устройстве запущены иные ресурсоёмкие процессы. (Все требования перечислять не буду)... В результате изучения различных вариантов и проведения сравнительных испытаний, Вы приняли решение начать разработку ПО, которое будет запускаться на JVM в Docker контейнерах, с установленными ограничениями на использование ресурсов системы.

Проблема
В процессе тестов производительности Вы столкнулись с ситуацией, когда Docker контейнер крашится и перезапускается по причине превышения установленных лимитов по потреблению памяти, несмотря на то что размер Java Heap остается в допустимых пределах.

Решение
Необходимо разобраться, что может быть причиной потребления памяти в данном Docker контейнере, помимо Java Heap, настроить мониторинг, найти и устранить данную причину. В этом нам помогут диаграммы указанные ниже.
(В данном теоретическом примере, причинами утечки памяти были неверно сконфигурирована embedded RocksDB, вызываемая через rocksdbjni и самописная библиотека на C++, которая вызывалась через JNI)

Основная часть

Диаграммы

Упрощенная диаграмма использования памяти JVM в Docker контейнере:

Упрощенная диаграмма использования памяти JVM в Docker контейнереУпрощенная диаграмма использования памяти JVM в Docker контейнере

Детализированная диаграмма использования памяти JVM в Docker контейнере с некоторыми параметрами:

Заключение

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

Конструктивная критика, предложения и замечания приветствуются.

Подробнее..
Категории: Devops , Java , Docker , Jvm , Memory management , Jvm options

Перевод Как написать (игрушечную) JVM

15.12.2020 20:09:12 | Автор: admin
Статья про KVM оказалась интересной для читателей, поэтому сегодня публикуем новый перевод статьи Serge Zaitsev: как работает Java Virtual Machine под капотом.

Нравится нам это или нет, но Java один из наиболее распространенных и широко используемых языков программирования. Однако не каждый Java-разработчик настолько любопытен, чтобы заглянуть под капот и посмотреть, как устроена JVM.

Я попытаюсь написать игрушечную (и неполную) JVM, чтобы показать основные принципы ее работы. Надеюсь, эта статья вызовет у вас интерес и вдохновит на дальнейшее изучение JVM.

Наша скромная цель


Начнем с простого:

public class Add {  public static int add(int a, int b) {    return a + b;  }}

Мы компилируем наш класс с javac Add.java, и в результате получается Add.class. Этот class файл является бинарным файлом, который JVM может выполнять. Всё, что нам осталось сделать, создать такую JVM, которая бы выполняла его корректно.

Если мы заглянем внутрь Add.class с помощью шестнадцатеричного дампа мы, скорее всего, будем не слишком впечатлены:

00000000 ca fe ba be 00 00 00 34 00 0f 0a 00 03 00 0c 07 |.......4........|
00000010 00 0d 07 00 0e 01 00 06 3c 69 6e 69 74 3e 01 00 |........<init>..|
00000020 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 |.()V...Code...Li|
00000030 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 03 |neNumberTable...|
00000040 61 64 64 01 00 05 28 49 49 29 49 01 00 0a 53 6f |add...(II)I...So|
00000050 75 72 63 65 46 69 6c 65 01 00 08 41 64 64 2e 6a |urceFile...Add.j|
00000060 61 76 61 0c 00 04 00 05 01 00 03 41 64 64 01 00 |ava........Add..|
00000070 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 |.java/lang/Objec|
00000080 74 00 21 00 02 00 03 00 00 00 00 00 02 00 01 00 |t.!.............|
00000090 04 00 05 00 01 00 06 00 00 00 1d 00 01 00 01 00 |................|
000000a0 00 00 05 2a b7 00 01 b1 00 00 00 01 00 07 00 00 |...*............|
000000b0 00 06 00 01 00 00 00 01 00 09 00 08 00 09 00 01 |................|
000000c0 00 06 00 00 00 1c 00 02 00 02 00 00 00 04 1a 1b |................|
000000d0 60 ac 00 00 00 01 00 07 00 00 00 06 00 01 00 00 |`...............|
000000e0 00 03 00 01 00 0a 00 00 00 02 00 0b |............|


Хотя мы еще не видим здесь четкой структуры, нам нужно найти способ ее разобрать: что значит ()V и (II)I, <init> и почему файл начинается с cafe babe?

Вероятно, вы знакомы с другим способом выгрузить class файлы, часто он оказывается более полезным:

$ javap -c Add
Compiled from "Add.java"
public class Add {
public Add();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}


Теперь мы видим наш класс, его конструктор и метод. И конструктор, и метод содержат несколько инструкций. Становится более-менее ясно, что делает наш метод add(): он загружает два аргумента (iload_0 и iload_1), суммирует их и возвращает результат. JVM это стековая машина, поэтому в ней нет регистров, все аргументы инструкций хранятся во внутреннем стеке, и результаты также помещаются в стек.

Class loader


Как нам добиться того же результата, который показала программа javap? Как разобрать class файл?

Если обратиться к спецификации JVM, мы узнаем о структуре файлов формата class. Он всегда начинается с 4-байтовой подписи (CAFEBABE), затем 2 + 2 байта для версии. Звучит просто!

Поскольку нам пришлось бы читать байты, short, int и байтовые последовательности из бинарного файла, начнем реализацию нашего загрузчика следующим образом:

type loader struct {r   io.Readererr error}func (l *loader) bytes(n int) []byte {b := make([]byte, n, n)        // мы не хотим обрабатывать ошибки на каждом этапе,        // поэтому просто сохраним первую найденную ошибку до конца        // и ничего не делаем, если ошибка была найденаif l.err == nil {_, l.err = io.ReadFull(l.r, b)}return b}func (l *loader) u1() uint8  { return l.bytes(1)[0] }func (l *loader) u2() uint16 { return binary.BigEndian.Uint16(l.bytes(2)) }func (l *loader) u4() uint32 { return binary.BigEndian.Uint32(l.bytes(4)) }func (l *loader) u8() uint64 { return binary.BigEndian.Uint64(l.bytes(8)) }// Usage:f, _ := os.Open("Add.class")loader := &loader{r: f}cafebabe := loader.u4()major := loader.u2()minor := loader.u2()

Затем спецификация сообщает нам, что необходимо распарсить пул констант. Что это? Так называется специальная часть class файла, которая содержит константы, необходимые для запуска класса. Все строки, числовые константы и ссылки хранятся там, и каждая имеет уникальный индекс uint16 (таким образом, класс может иметь до 64К констант).

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

  • UTF8: простой строковый литерал
  • Class: индекс строки, содержащей имя класса (косвенная ссылка)
  • Name and type: индекс имени типа и дескриптор, используемый для полей и методов
  • Field and method references: индексы, относящиеся к классам и константам типа name and type.

Как видите, константы в пуле часто ссылаются друг на друга. Поскольку мы пишем JVM на языке Go и здесь нет объединения типов (union types), давайте создадим тип Const, который будет содержать в себе различные возможные поля констант:

type Const struct {Tag              byteNameIndex        uint16ClassIndex       uint16NameAndTypeIndex uint16StringIndex      uint16DescIndex        uint16String           string}type ConstPool []Const

Затем, следуя спецификации JVM, мы могли бы извлечь данные пула констант следующим образом:

func (l *loader) cpinfo() (constPool ConstPool) {constPoolCount := l.u2()// Валидные индексы пула констант начинаются с 1for i := uint16(1); i < constPoolCount; i++ {c := Const{Tag: l.u1()}switch c.Tag {case 0x01: // UTF8 string literal, 2 bytes length + datac.String = string(l.bytes(int(l.u2())))case 0x07: // Class indexc.NameIndex = l.u2()case 0x08: // String reference indexc.StringIndex = l.u2()case 0x09, 0x0a: // Field and method: class index + NaT indexc.ClassIndex = l.u2()c.NameAndTypeIndex = l.u2()case 0x0c: // Name-and-typec.NameIndex, c.DescIndex = l.u2(), l.u2()default:l.err = fmt.Errorf("unsupported tag: %d", c.Tag)}constPool = append(constPool, c)}return constPool}

Сейчас мы сильно упрощаем себе задачу, но в реальной JVM нам пришлось бы обрабатывать типы констант long и double единообразно, вставляя дополнительный неиспользуемый постоянный элемент, как сообщает нам спецификация JVM (поскольку постоянные элементы считаются 32-битными).

Чтобы упростить получение строковых литералов по индексам, реализуем метод Resolve(index uint16) string:

func (cp ConstPool) Resolve(index uint16) string {if cp[index-1].Tag == 0x01 {return cp[index-1].String}return ""}

Теперь нам нужно добавить похожие помощники для анализа списка интерфейсов, полей и методов классов и их атрибутов:

func (l *loader) interfaces(cp ConstPool) (interfaces []string) {interfaceCount := l.u2()for i := uint16(0); i < interfaceCount; i++ {interfaces = append(interfaces, cp.Resolve(l.u2()))}return interfaces}// Тип field используется и для полей и методовtype Field struct {Flags      uint16Name       stringDescriptor string Attributes []Attribute }// Атрибуты содержат дополнительную информацию о полях и классах// Самый полезный - это атрибут Code, который содержит актуальный байтовый кодtype Attribute struct {Name stringData []byte}func (l *loader) fields(cp ConstPool) (fields []Field) {fieldsCount := l.u2()for i := uint16(0); i < fieldsCount; i++ {fields = append(fields, Field{Flags:      l.u2(),Name:       cp.Resolve(l.u2()),Descriptor: cp.Resolve(l.u2()),Attributes: l.attrs(cp),})}return fields}func (l *loader) attrs(cp ConstPool) (attrs []Attribute) {attributesCount := l.u2()for i := uint16(0); i < attributesCount; i++ {attrs = append(attrs, Attribute{Name: cp.Resolve(l.u2()),Data: l.bytes(int(l.u4())),})}return attrs}

И поля, и методы представлены как Fields, это очень удобно, позволяет сэкономить время. Наконец, мы можем собрать всё это вместе и полноценно распарсить наш класс:

type Class struct {ConstPool  ConstPoolName       stringSuper      stringFlags      uint16Interfaces []stringFields     []FieldMethods    []FieldAttributes []Attribute}func Load(r io.Reader) (Class, error) {loader := &loader{r: r}c := Class{}loader.u8()           // magic (u32), minor (u16), major (u16)cp := loader.cpinfo() // const pool infoc.ConstPool = cpc.Flags = loader.u2()             // access flagsc.Name = cp.Resolve(loader.u2())  // this classc.Super = cp.Resolve(loader.u2()) // super classc.Interfaces = loader.interfaces(cp)c.Fields = loader.fields(cp)    // fieldsc.Methods = loader.fields(cp)   // methodsc.Attributes = loader.attrs(cp) // methodsreturn c, loader.err}

Теперь, если мы посмотрим на полученную информацию о классе, мы увидим, что у него нет полей и два метода <init>:()V и add:(II)I. Что за римские числа со скобками? Это дескрипторы. Они определяют, какие типы аргументов принимает метод и что он возвращает. В этом случае <init> (синтетический метод, используемый для инициализации объектов при их создании) не принимает аргументов и ничего не возвращает (V=void), в то время как метод add принимает два типа данных int (I=int32) и возвращает целое число.

Байт-код


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

<init>:
[0 1 0 1 0 0 0 5 42 183 0 1 177 0 0 0 1 0 7 0 0 0 6 0 1 0 0 0 1]
add:
[0 2 0 2 0 0 0 4 26 27 96 172 0 0 0 1 0 7 0 0 0 6 0 1 0 0 0 3]


В спецификации, на этот раз в разделе bytecode, мы прочтем, что атрибут Code начинается со значения maxstack (2 байта), затем maxlocals (2 байта), длина кода (4 байта) и затем фактический код. Итак, наши атрибуты можно читать так:

<init>: maxstack: 1, maxlocals: 1, code: [42 183 0 1 177]
add: maxstack: 2, maxlocals: 2, code: [26 27 96 172]


Да, у нас есть только 4 и 5 байтов кода в каждом методе. Что означают эти байты?

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

26 = iload_0
27 = iload_1
96 = iadd
172 = ireturn


Точно такие же, как мы видели в выводе javap в начале! Но как это сделать?

Фреймы JVM


Когда метод выполняется внутри JVM, у него есть собственный стек для временных операндов, свои локальные переменные и фрагмент кода для выполнения. Все эти параметры хранятся в едином фрейме выполнения. Кроме того, фреймы содержат указатель текущей инструкции (насколько далеко мы продвинулись при выполнении байт-кода) и указатель на класс, в котором содержится метод. Последний нужен для доступа к пулу констант класса, а также к другим деталям.

Давайте создадим метод, который создает фрейм для конкретного метода, вызванного с заданными аргументами. Я буду использовать здесь тип interface{} в качестве типа Value, хотя использование правильного объединения типов (union types), конечно, было бы более безопасным.

type Frame struct {Class  ClassIP     uint32Code   []byteLocals []interface{}Stack  []interface{}}func (c Class) Frame(method string, args ...interface{}) Frame {for _, m := range c.Methods {if m.Name == method {for _, a := range m.Attributes {if a.Name == "Code" && len(a.Data) > 8 {maxLocals := binary.BigEndian.Uint16(a.Data[2:4])frame := Frame{Class:  c,Code:   a.Data[8:],Locals: make([]interface{}, maxLocals, maxLocals),}for i := 0; i < len(args); i++ {frame.Locals[i] = args[i]}return frame}}}}panic("method not found")}

Итак, мы получили Frame с инициализированными локальными переменными, пустым стеком и предварительно загруженным байт-кодом. Пришло время выполнить байт-код:

func Exec(f Frame) interface{} {for {op := f.Code[f.IP]log.Printf("OP:%02x STACK:%v", op, f.Stack)n := len(f.Stack)switch op {case 26: // iload_0f.Stack = append(f.Stack, f.Locals[0])case 27: // iload_1f.Stack = append(f.Stack, f.Locals[1])case 96:a := f.Stack[n-1].(int32)b := f.Stack[n-2].(int32)f.Stack[n-2] = a + bf.Stack = f.Stack[:n-1]case 172: // ireturnv := f.Stack[n-1]f.Stack = f.Stack[:n-1]return v}f.IP++}}

Наконец, мы можем собрать все вместе и запустить, вызвав наш метод add():

f, _ := os.Open("Add.class")class, _ := Load(f)frame := class.Frame("add", int32(2), int32(3))result := Exec(frame)log.Println(result)// OUTPUT:OP:1a STACK:[]OP:1b STACK:[2]OP:60 STACK:[2 3]OP:ac STACK:[5]5

Итак, всё работает. Да, это очень слабая JVM на минималках, но все же она делает то, что и должна JVM: загружает байт-код и интерпретирует его (конечно, настоящая JVM делает гораздо большее).

Чего не хватает?


Оставшихся 200 инструкций, среды выполнения, системы типов ООП и еще нескольких вещей.

Существует 11 групп инструкций, большинство из которых банальны:

  • Constants (помещает null, small number или значения из пула констант на стек).
  • Loads (помещает в стек локальные переменные). Подобных инструкций 32.
  • Stores (перемещают из стека в локальные переменные). Еще 32 скучных инструкции.
  • Stack (pop/dup/swap), как в любой стековой машине.
  • Math (add/sub/div/mul/rem/shift/logic). Для разных типов значений, всего 36 инструкций.
  • Conversions (int в short, int в float и т.д.).
  • Comparisons (eq/ne/le/). Полезно для создания условных выражений, например с if/else.
  • Control (goto/return). Пригодится для циклов и подпрограмм.
  • References. Самая интересная часть, поля и методы, исключения и мониторы объекта.
  • Extended. На первый взгляд выглядит не очень красивым решением. И, вероятно, со временем это не изменится.
  • Reserved. Здесь находится инструкция точки останова 0xca.

Большинство инструкций несложно реализовать: они берут один или два аргумента из стека, выполняют над ними некоторую операцию и отправляют результат. Единственное, что здесь следует иметь в виду, инструкции для типов long и double предполагают, что каждое значение занимает два слота в стеке, поэтому вам могут потребоваться дополнительные push() и pop(), что затрудняет группировку инструкций.

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

  • invokestatic: вызвать статический метод в классе, никаких сюрпризов.
  • invokespecial: вызвать метод экземпляра напрямую, в основном используется для синтетических методов, таких как <init> или приватных методов.
  • invokevirtual: вызвать метод экземпляра на основе иерархии классов.
  • invokeinterface: вызывает метод интерфейса, аналогичный invokevirtual, но выполняет другие проверки и оптимизацию.
  • invokedynamic: вызвать динамически вычисляемый call site, в новой версии Java 7, полезно для динамических методов и MethodHandles.

Если вы создаете JVM на языке без сборки мусора, в таком случае вам следует подумать, как ее выполнить: подсчет ссылок, алгоритм пометок (mark-and-sweep) и т. д. Обработка исключений путем реализации athrow, их распространения через фреймы и обработка с помощью таблиц исключений еще одна интересная тема.

Наконец, ваша JVM останется бесполезной, если нет runtime классов. Без java/lang/Object вы вряд ли даже увидите, как работает new инструкция при создании новых объектов. Ваша среда выполнения может предоставлять некоторые общие классы JRE из пакетов java.lang, java.io и java.util, или это может быть что-то более специфичное для домена. Скорее всего, некоторые методы в классах должны быть реализованы изначально, а не на Java. Это поднимет вопрос о том, как найти и выполнить такие методы, и станет еще одним крайним случаем для вашей JVM.

Другими словами, создать правильную JVM не так уж и просто, однако разобраться, как именно она работает, не сложно.

У меня, например, на это ушли всего одни летние выходные. Моей JVM еще предстоит долгий путь, но структура выглядит более или менее ясной: https://github.com/zserge/tojvm (замечания всегда приветствуются!)

Фактические фрагменты кода из этой статьи еще меньше и доступны здесь как gist.

Если появилось желание изучить вопрос лучше, вы можете подумать об использовании небольших JVM:


Надеюсь, эта статья не отбила ваш интерес к Java. Виртуальные машины это весело, а виртуальная машина Java действительно заслуживает внимательного рассмотрения.

Надеюсь, вам понравилась моя статья. Вы можете следить за моей работой через Github или Twitter, а также подписываться через rss.
Подробнее..

Recovery mode Обработка финансовой информации в продуктах Oracle OFSS предусматривает постепенный отказ от использования JVM в Oracle

09.04.2021 16:11:40 | Автор: admin

Из-за множества вопросов по сертификации продуктов Oracle OFSS (Oracle Financial Services Software) наша техническая поддержка сообщает, что в ноте OFSS Certified Configurations (Doc ID 2636856.1) сделана попытка сертифицировать набор релизов OFSS, находящихся в Premier Support новых версий по технологическим компонентам, таким, например, как база данных, версия компонент промежуточного слоя, операционных систем, браузеров и других компонент.

Практически, все сводится к тому, что вендор выставляет рекомендуемые конфигурации продуктов к использованию FMW 12.2.1.4 и DB 19.6. Давайте рассмотрим, на какие продукты OFSS и как это повлияет.

Как известно, в стек продуктов OFSS включены продукты:

  • Oracle FLEXCUBE Universal Banking;

  • Oracle Banking Payments;

  • Oracle Banking Corporate Lending;

  • Oracle Banking APIs;

  • Oracle Banking Digital Experience;

  • Oracle FLEXCUBE Enterprise Limits and Collateral Management;

  • Oracle Financial Services Lending and Leasing;

  • Oracle FLEXCUBE Core Banking;

  • Oracle FLEXCUBE Investor Servicing;

  • Oracle Banking Liquidity Management;

  • Oracle Banking Virtual Account Management;

  • Oracle Banking Corporate Lending Process Management;

  • Oracle Banking Credit Facilities Process Management.

Напомним, что продукты Oracle Financial Services Software в нашей стране есть в банковском секторе персонального и корпоративного уровня, страховых компаниях. Есть внедрения решений по управлению рисками и проверке законодательных требований к финансовым операциям, проверки работоспособности приложений для бухгалтерского, управленческого учета, управления бизнес-процессами и человеческими ресурсами, инструментами управления закупочной деятельностью.

Памятуя о том, что тема сертификации всех продуктов и технологий практически неисчерпаема даже для одного релиза OFSS, в год 25-летия языка Java мы, кроме сугубо технической информации, обнаружили одно очень важное замечание в самом конце этой ноты.

Чтобы упростить понимание ситуации до выпуска ноты, заметим, что нынешняя архитектура базы с настраиваемыми технологиями поддержки языка Java, других языков высокого уровня, собственной JVM с настраиваемым объемом Java Pool Size, загрузчиком классов приложений, обработчиками XSD, XML, JSON, Xpath и ряда других вспомогательных технологий обеспечивает технологический стек для любого приложения FMW (Oracle Fusion Middleware), работающего внутри базы данных. C версии сервера старше 12, Oracle полностью соблюдает стандарты обработки XML.

Если же вы до сих пор работаете на 11.2.0.4, то мы напоминаем, что по документу Release Schedule of Current Database Releases (Doc ID 742060.1) поддержка версии закончилась с января 2021 года, при этом объявлен дополнительный приобретаемый уровень поддержки Market Driven Support (MDS), оказываемый только Oracle. По нашему мнению, работа на современных серверах с количеством SGA не более 70GB в начале третьего десятилетия 21 века выглядит немного устаревшей.

Продолжим обсуждение ноты Doc ID 2636856.1. При последовательном переводе ноты содержащийся в ней комментарий означает, что продукты OFSS версий до v14.0 задействовали возможности Java-машины, встроенной в СУБД для поддержки специфической функциональности. С момента выпуска ноты Oracle больше не рекомендуется использовать Java внутри базы данных (для задач OFSS, прим. автора). Сертификация последнего технологического стека выполнена на основе того, что поддержка ограничена техническими возможностями Java, работающей внутри сервера Oracle.

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

К сожалению, требуется указать, что ряд явных багов Java-машины в версиях Oracle Server 18с и 19с имеют непубличный статус, хотя могут существенно повлиять на обработку XML, XSD и Unicode символов. Например, Bug 24008722, отраженный в документе Doc ID 2450530.1, может не позволить работать с тэгами вида <xs:pattern Value="(doc|docx)"/> в некоторых XSD схемах, он же может стать причиной невозможности обработки символов вне US7ASCII (Bug 29715647).

Следовательно, если у вас есть код, выполняющий указанные операции на Oracle Server 18с и 19с, есть смысл еще раз проверить его работоспособность. Для 12-х версий можно скачать патчи.

Данную заметку можно рассматривать как краткий ответ на вопрос - Так можно ли использовать Java-машину Aurora внутри сервера Oracle и для каких целей.

Подробнее..
Категории: Oracle , Jvm , Ofss , Mig , Realese , Oracle fusion middleware

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru