Всем привет. Эта статья — вольный перевод поста StringBuffer, and how hard it is to get rid of legacy code. Как-то очень он мне запал в душу, поэтому решил перевести. Поехали.

В 2006-м, в 5-й java появился StringBuilder. Более легковесная и разумная альтернатива StringBuffer. Вот, что говорит официальная документация по StringBuffer:

Этот класс дополнен аналогичным классом предназначенным для использования в одном потоке — StringBuilder. В общем случае нужно отдавать предпочтение классу StringBuilder, так как он поддерживает все те же операции, что и этот (StringBuffer), но быстрее, так как не выполняет никаких синхронизаций.

Иметь synchronized в StringBuffer вообще никогда не было хорошей идеей. Основная проблема в том, что одной операции никогда не достаточно. Одиночная конкатенация .append(x) бесполезная без других операций, таких как .append(y) и .toString(). В то время, когда каждый конкретный метод потокобезопасный, вы не можете сделать несколько вызовов без конкуренции между потоками. Ваша единственная опция — внешняя синхронизация.

Так, что? Получается, 10 лет спустя уже никто не использует StringBuffer!? Ну, по крайней мере, точно не для нового функционала!?

Сколько объектов создает этот код?


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

public class Main {
    public static void main(String... args) {
        System.out.println("Hello " + "world");
    }
}

Oracle JVM 8-й версии создает приблизительно 10_000 объектов для выполнения этой программы.

Сколько же это создается StringBuffers?


Итак, чтобы запустить JVM нужно создать много объектов, но старые классы, у которых есть более быстрая альтернатива, которой уже 10 лет не должны быть использованы, так ведь?

public class Main {
    public static void main(String[] args) throws IOException {
        System.out.println("Waiting");
        System.in.read();
    }
}

Пока процесс запущен, мы можем выполнить следующую команду:

jmap -histo {pid} | grep StringBuffer

и получаем:

  18:           129           3096  java.lang.StringBuffer

129 это количество объектов StringBuffer которые создала Java 8 Update 121. Это меньше, чем в прошлый раз, когда я проверял, но всё равно, немножко удивительно.

(Проверил у себя на Java 8 update 131, получил всего 14 объектов).

А как на счет новых фич — лямбд и стримов? Они были созданы явно в последние 10 лет и используют некоторые сторонние библиотеки, вроде Objectwebs ASM. И эти разработчики точно знают внутренности виртуальной машины и должны были спроектировать новые фичи максимально легковесными.

public class Main {
    public static void main(String[] args) throws IOException {
        IntStream.range(0, 4).forEach(System.out::println);
        System.out.println("Waiting");
        System.in.read();
    }
}

Запускаем jmap опять и что мы видим?

  17:           545          13080  java.lang.StringBuffer

Дополнительные 416 объектов для простейшей лямбды и стрима!

(Опять проверил у себя на Java 8 update 131 и получил 430 объектов, то есть разница в те же 416 объектов. Похоже, что стримы и лямбды не подчистили).

Кстати, программа выше в целом создает:

Total         35486        4027224

или 35_486 объектов!

Выводы


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

P. S.
В оригинальном посте оставили ссылку на тикет очистки от StringBuffer в 9-ке.
Так что вполне возможно через 3 месяца мы останемся без наследия :), правда речь лишь о StringBuffer. Не забываем про Vector, Hashtable и прочие приятности.
Поделиться с друзьями
-->

Комментарии (33)


  1. gltrinix
    02.06.2017 10:40
    +1

    А есть где-нибудь более подробный разбор, что за 10к и 35к объектов создаются при запуске Hello World на пару строк?


    1. doom369
      02.06.2017 10:57

      Статьи не встречал, но вот что показывает jmap:

      Много классов
      num #instances #bytes class name
      ----------------------------------------------
      1: 402 4903520 [I
      2: 1621 158344 [C
      3: 455 52056 java.lang.Class
      4: 194 49728 [B
      5: 1263 30312 java.lang.String
      6: 515 26088 [Ljava.lang.Object;
      7: 115 8280 java.lang.reflect.Field
      8: 258 4128 java.lang.Integer
      9: 94 3760 java.lang.ref.SoftReference
      10: 116 3712 java.util.Hashtable$Entry
      11: 126 3024 java.lang.StringBuilder
      12: 8 3008 java.lang.Thread
      13: 74 2576 [Ljava.lang.String;
      14: 61 1952 java.io.File
      15: 38 1824 sun.util.locale.LocaleObjectCache$CacheEntry
      16: 12 1760 [Ljava.util.Hashtable$Entry;
      17: 53 1696 java.util.concurrent.ConcurrentHashMap$Node
      18: 23 1472 java.net.URL
      19: 14 1120 [S
      20: 2 1064 [Ljava.lang.invoke.MethodHandle;
      21: 1 1040 [Ljava.lang.Integer;
      22: 26 1040 java.io.ObjectStreamField
      23: 12 1024 [Ljava.util.HashMap$Node;
      24: 30 960 java.util.HashMap$Node
      25: 20 800 sun.util.locale.BaseLocale$Key
      26: 15 720 java.util.HashMap
      27: 18 720 java.util.LinkedHashMap$Entry
      28: 12 672 java.lang.Class$ReflectionData
      29: 40 640 java.lang.Object
      30: 19 608 java.util.Locale
      31: 19 608 sun.util.locale.BaseLocale
      32: 9 560 [Ljava.lang.reflect.Field;
      33: 10 560 sun.misc.URLClassPath$JarLoader
      34: 5 528 [Ljava.util.concurrent.ConcurrentHashMap$Node;
      35: 21 504 java.net.Parts
      36: 21 504 sun.security.action.GetPropertyAction
      37: 6 480 java.lang.reflect.Constructor
      38: 12 480 java.security.AccessControlContext
      39: 20 480 java.util.Locale$LocaleKey
      40: 18 432 java.io.ExpiringCache$Entry
      41: 1 384 java.lang.ref.Finalizer$FinalizerThread
      42: 6 384 java.nio.DirectByteBuffer
      43: 6 384 java.util.concurrent.ConcurrentHashMap
      44: 1 376 java.lang.ref.Reference$ReferenceHandler
      45: 14 336 java.lang.StringBuffer
      46: 6 336 java.nio.DirectLongBufferU
      47: 10 320 java.lang.OutOfMemoryError
      48: 10 288 [Ljava.io.ObjectStreamField;
      49: 7 280 java.lang.ref.Finalizer
      50: 11 264 sun.misc.URLClassPath$3
      51: 14 256 [Ljava.lang.Class;
      52: 3 240 [Ljava.util.WeakHashMap$Entry;
      53: 10 240 sun.misc.MetaIndex
      54: 7 224 java.lang.ref.ReferenceQueue
      55: 7 224 java.util.Vector
      56: 5 200 java.util.WeakHashMap$Entry
      57: 6 192 java.io.FileDescriptor
      58: 4 192 java.nio.HeapCharBuffer
      59: 4 192 java.util.Hashtable
      60: 2 176 java.lang.reflect.Method
      61: 7 168 java.util.ArrayList
      62: 3 168 sun.nio.cs.UTF_8$Encoder
      63: 9 144 java.lang.ref.ReferenceQueue$Lock
      64: 3 144 java.nio.HeapByteBuffer
      65: 3 144 java.util.WeakHashMap
      66: 6 144 sun.misc.PerfCounter
      67: 2 128 java.io.ExpiringCache$1
      68: 3 120 java.security.ProtectionDomain
      69: 1 104 sun.net.www.protocol.file.FileURLConnection
      70: 3 96 java.io.FileInputStream
      71: 3 96 java.io.FileOutputStream
      72: 2 96 java.lang.ThreadGroup
      73: 3 96 java.security.CodeSource
      74: 2 96 java.util.Properties
      75: 3 96 java.util.Stack
      76: 2 96 java.util.StringTokenizer
      77: 1 96 sun.misc.Launcher$AppClassLoader
      78: 2 96 sun.misc.URLClassPath
      79: 2 96 sun.nio.cs.StreamEncoder
      80: 4 96 sun.usagetracker.UsageTrackerClient$1
      81: 1 88 sun.misc.Launcher$ExtClassLoader
      82: 1 80 [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;
      83: 2 80 [Ljava.net.URL;
      84: 2 80 java.io.BufferedWriter
      85: 2 80 java.io.ExpiringCache
      86: 5 80 java.lang.Class$3
      87: 2 80 sun.nio.cs.UTF_8$Decoder
      88: 3 72 [Ljava.lang.reflect.Constructor;
      89: 3 72 java.lang.Class$1
      90: 3 72 java.lang.RuntimePermission
      91: 3 72 java.util.Collections$SynchronizedSet
      92: 3 72 java.util.concurrent.atomic.AtomicLong
      93: 3 72 sun.misc.Signal
      94: 3 72 sun.reflect.NativeConstructorAccessorImpl
      95: 2 64 [Ljava.lang.Thread;
      96: 2 64 java.io.FilePermission
      97: 2 64 java.io.PrintStream
      98: 2 64 java.lang.ClassValue$Entry
      99: 2 64 java.lang.NoSuchMethodError
      100: 2 64 java.lang.ThreadLocal$ThreadLocalMap$Entry
      101: 2 64 java.lang.VirtualMachineError
      102: 2 64 java.lang.ref.ReferenceQueue$Null
      103: 1 48 [J
      104: 2 48 [Ljava.io.File;
      105: 2 48 [Ljava.lang.reflect.Method;
      106: 3 48 [Ljava.security.Principal;
      107: 2 48 java.io.BufferedOutputStream



      1. OlegZH
        02.06.2017 12:07

        Впечатляет! Много говорит о внутреннем устройстве. О том, почему всё именно так, как есть.

        Вот, если бы программист мог бы как-то влиять на внутренние объекты и задавать, например, определённую модель памяти (по типу того, как это делалось в стародавние времена: TINY, SMALL, LARGE, HUGE) или, просто, явно в коде определять дисциплину управления памяти…


        1. doom369
          02.06.2017 12:12

          Ну джаве 3-й десяток лет пошел как никак :). Так что можно простить. Другое дело, что фикс довольно простой и сводится к банальной автозамене, если посмотреть коммиты.
          Я встречал статьи, где говорилось про постоянные улучшения по скорости старта JVM, в то же время такие банальности оставили вне фокуса. Немножко странно.


          1. OlegZH
            02.06.2017 12:33

            Это же какой получается слоёный пирог из «багов» и «фич», если рассматривать каждое конкретное ПО! Тут, ведь, на в каждом слое — своя долгая история («так исторически сложилось»).

            Не означает ли это, что весь исходный код нужно всегда тщательно переписывать? Переписывать! А не плодить новые классы взамен старых.


        1. komitet
          02.06.2017 12:31

          Топ консьюмер в виде [I, скорее всего, является спецэффектом для обеспечения heap parsability.
          Почитать можно тут: https://shipilev.net/jvm-anatomy-park/5-tlabs-and-heap-parsability/


  1. Bunny_74
    02.06.2017 11:12

    интересно было бы сравнить насколько ускорится тестовая программа на JDK8 и JDK9, где JDK-8041679 уже поправлен. Понятно, что в 9ке есть не только эти изменения и оптимизации, поэтому чистого сравнения не получится. В Java делали оптимизации и в synchronized блоке и сейчас он работает быстрее, чем, допустим, в JDK 1.4.


    1. doom369
      02.06.2017 11:18

      В Java делали оптимизации и в synchronized блоке и сейчас он работает быстрее, чем, допустим, в JDK 1.4.


      Да, компилятор может удалять sync блоки, если может доказать что они не нужны. Но не факт, что это сработает/срабатывает именно на старте JVM.


  1. fzn7
    02.06.2017 11:12

    Откуда берутся объекты понятно, просто запихнуть переменные среды в System и вот вам уже ~200 штук. А по syncronized проблема не проблема, т.к. если я правильно помню, то jvm делает ни к чему не обязывающий biased lock. Насколько здесь деградирует производительность мне сложно сказать, но не думаю, что будет сильно заметно. Резюмируя, не в обиду, проблемы кажутся высосанными из пальца


    1. doom369
      02.06.2017 11:20
      +1

      Конечно, проблемы особой нету. Но sync это больше байткода, больше работы для JVM как ни крути. Пусть и речь о наносекундах. В общем случае — чем проще и меньше кода, тем лучше. Тем более когда эти sync блоки вообще не несут никакой пользы.


      1. vektory79
        02.06.2017 12:16
        +2

        Ну тут как сказать… У меня на практике был случай когда одно лишнее обращение к полю класса ломало длинную цепочку оптимизаций JIT и производительность проседала вдвое. Так что моё личное мнение таково, что библиотеки ядра системы должны быть максимально оптимизированными, т.к. никогда не знаешь где и как они будут использоваться.


        В этом свете удивляют комментарии некоторых разработчиков, мол тут мы оставим так, потому что JIT потом всё вычистит. Ага… Где-то вычистит а где-то и подавится. Т.к. эффект на оптимизации в подавляющем большинстве случаев нелокален.


        1. doom369
          02.06.2017 12:21

          Так что моё личное мнение таково, что библиотеки ядра системы должны быть максимально оптимизированными


          Тут я с вами согласен на 100%. Но нужно учитывать возраст явы. Легаси есть легаси, к сожалению.

          Про проседание в 2 раза было бы интересно почитать.


          1. vektory79
            02.06.2017 12:32
            +1

            Ну это было достаточно давно и в закрытом коде. Увы. Но и без меня много кто про это рассказывает. Вот, рекомендую:



        1. OlegZH
          02.06.2017 12:59
          +1

          А что будет, если иметь возможность давать явные указания оптимизатору («туда нельзя», «сюда нельзя», «никуда нельзя») непосредственно в исходном коде программы?

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

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

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

          Но это всё — мои заметки на полях.)))


          1. Lachrimae
            02.06.2017 17:03

            Скажем так, я как разработчик могу, предположим, дать своему пользователю выбор формы оптимизации — под экономию ресурсов, под скорострельность и так далее. Потому что:
            а) в своем приложении я — царь и бог;
            б) за мной, с высокой долей вероятности, не тянется (или тянется, но несоизмеримо меньший) шлейф легаси-кода;
            в) коммьюнити моих пользователей, вероятнее всего — однородно, они все — конечные пользователи (не берем в расчет случай, когда я создаю, к примеру, LUA-песочницу).
            Разработчики JVM в этих отношениях ограничены: не могут выкинуть ворох старых библиотек, от которых у них самих кровища из глаз хлещет, потому что тогда при ближайшем обновлении часть продуктов на базе их решения превратится в тыкву. И попытки пометить всю старую рухлядь как deprecated и поверх нее напилить новые решения неизбежно приведут к тому, что с каждым разом общая архитектура машины будет все ближе к хрестоматийным фотографиям инженерных сетей в Индии.


      1. Maccimo
        03.06.2017 19:08

        Объявление метода как synchronized количество байткода не увеличивает, в отличие от synchronized (obj) {} блоков.


    1. Bunny_74
      02.06.2017 11:59
      +1

      просто никому не нравится legacy)


    1. SmallSharky
      03.06.2017 11:14

      Интересно, а почему запихивание переменных среды происходит именно при запуске, а не в момент первого обращения к конкретной переменной из участка кода? Да уж, количество объектов в HelloWorld напрочь отбивает желание работать с VM-based языками.


      1. fzn7
        03.06.2017 11:38
        +3

        Вероятно по той причине, что jvm конфигурирует себя под окружение перед началом работы. Не работайте с java, прошу вас, мне не нужны конкуренты на рынке труда


        1. SmallSharky
          05.06.2017 00:41
          -2

          Упаси, Боже! По джаве я только учу других. А сам предпочитаю C/C++, ибо там обычно предполагается исполнение под реальной, hardware, машиной, а вопрос кроссплатформенности лечится использованием платформонезависимых компонентов.


  1. krupt
    02.06.2017 12:31

    В JVM синхронизация через synchronized оптимизирована хорошо. При захвате блокировки одним и тем же потоком, потери производительности минимальны, а то и совсем отсутствуют. А ведь в большинстве своём StringBuffer используется в одном потоке. Соответственно, потери производительности от использования StringBuffer в сравнении с StringBuilder минимальны, а то и совсем отсутствуют.
    Как-то еще сравнивал JHM'ем StringBuffer vs StringBuilder. Результаты меня поразили. StringBuffer оказался быстрее на очень маленькое значение.


    1. krupt
      02.06.2017 13:06

      Извиняюсь, что-то впарился. Отключил «прогрев» JVM в бенчмарках JHM. После этого StringBuilder начал чуть-чуть выигрывать у StringBuffer'а. Как раз таки StringBuffer сдал немного из-за времени на оптимизацию блокировок.


    1. leventov
      03.06.2017 06:34
      +1

      Это если включен biased locking, а для сколь нибудь нацеленных на latency приложениях biased locking лучше отключать — дешевле выйдет. Тогда каждый вход и выход в метод StringBuffer это будет CAS со сбросом read/write буффера процессора.


      Даже если biased locking включен, каждый вход и выход в StringBuffer все равно означает сброс закешированных значений полей исполняемого кода на уровне JVM


      1. vladimir_dolzhenko
        03.06.2017 21:42

        Можно чуть более развёрнуто (с примерами) из-за который biased locking начинает мешать?


        1. leventov
          04.06.2017 02:16

          В одном нашем приложении чуть ли не больше половины пауз было из-за RevokeBias. Паузы до 1 секунды, много — по 200-300 мс.


  1. stuf4ik
    02.06.2017 14:40

    Вот кстати статья с бенчмарками StringBuffer vs StringBuilder
    StringBuffer and StringBuilder performance with JMH


    1. krupt
      05.06.2017 07:32

      Не нравится мне этот тест. Он создает и использует StringBuffer внутри метода. В этом случае, по идее, даже и блокировок создавать JVM не нужно. Потому что ссылка на объект не «вылезет» никуда.


      1. vladimir_dolzhenko
        05.06.2017 13:04

        они называют это escape analysis — можно, например, добавить -prof:gc, чтобы убедится в этом.


  1. sergey-b
    02.06.2017 23:49
    -1

    Насколько я помню, StringBuffer спрятан внутри StringWriter. В нем не сказано, что он устаревший или неэффективный. А интерфейс у него удобный. Вот все и пользуются.


    1. avost
      03.06.2017 01:16

      А интерфейс у него удобный

      Но ведь не удобнее, чем у StringBuilder'а? ;)


      1. sergey-b
        03.06.2017 01:29

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


        1. avost
          03.06.2017 10:09

          А, вы про Writer. Ну, тогда это проблема райтера, что внутри него стрингбуфер на стрингбилдер до сих пор не поменяли...


          1. sergey-b
            03.06.2017 16:25

            Я именно так и написал. StringBuffer используется в StringWriter. На StringBuilder его там никогда не заменят. Придется новый Writer делать.