Как определить, копирует ли String.substring данные символа

Я знаю, что для Oracle Java 1.7 обновление 6 и новее при использовании String.substring, внутренний массив символов String копируется, а для более старых версий - общий. Но я не нашел официального API, который бы сказал мне текущее поведение.

Использовать регистр

Мой вариант использования: В синтаксическом анализаторе мне нравится обнаруживать, что String.substring копирует или разделяет основной массив символов. Проблема в том, что если массив символов является общим, тогда мой парсер должен явно "разделить", используя new String(s), чтобы избежать проблемы с памятью. Однако, если String.substring в любом случае копирует данные, тогда это необязательно, и можно явно избежать копирования данных в синтаксическом анализаторе. Случай использования:

// possibly the query is very very large
String query = "select * from test ...";
// the identifier is used outside of the parser
String identifier = query.substring(14, 18);

// avoid if possible for speed,
// but needed if identifier internally 
// references the large query char array
identifier = new String(identifier);

Что мне нужно

В принципе, я хотел бы иметь статический метод boolean isSubstringCopyingForSure(), который бы обнаружил, что new String(..) не требуется. Я в порядке, если обнаружение не работает, если есть SecurityManager. В принципе, обнаружение должно быть консервативным (чтобы избежать проблем с памятью, я предпочел бы использовать new String(..), даже если это не обязательно).

Функции

У меня есть несколько вариантов, но я не уверен, что они надежны, особенно для JVM без Oracle:

Проверка поля String.offset

/**
 * @return true if substring is copying, false if not or if it is not clear
 */
static boolean isSubstringCopyingForSure() {
    if (System.getSecurityManager() != null) {
        // we can not reliably check it
        return false;
    }
    try {
        for (Field f : String.class.getDeclaredFields()) {
            if ("offset".equals(f.getName())) {
                return false;
            }
        }
        return true;
    } catch (Exception e) {
        // weird, we do have a security manager?
    }
    return false;
}

Проверка версии JVM

static boolean isSubstringCopyingForSure() {
    // but what about non-Oracle JREs?
    return System.getProperty("java.vendor").startsWith("Oracle") &&
           System.getProperty("java.version").compareTo("1.7.0_45") >= 0;
}

Проверка поведения Есть два варианта: они довольно сложны. Один из них создает строку с использованием пользовательской кодировки, затем создает новую строку b с помощью подстроки, затем модифицирует исходную строку и проверяет, также ли b также изменен. Вторым вариантом является создание огромной строки, затем нескольких подстрок и проверка использования памяти.

Ответ 1

Право, действительно, это изменение было сделано в 7u6. Для этого нет изменений API, поскольку это изменение является строго изменением реализации, а не изменением API, и нет API для определения того, какое поведение имеет JDK. Тем не менее, возможно, приложения могут заметить разницу в производительности или использовании памяти из-за изменения. На самом деле, нетрудно написать программу, которая работает в 7u4, но терпит неудачу в 7u6 и наоборот. Мы ожидаем, что компромисс будет благоприятным для большинства приложений, но, несомненно, есть приложения, которые пострадают от этого изменения.

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

В любом случае дело в том, что нужно измерить, а не гадать!

Сначала сравните производительность вашего приложения между аналогичными JDK с изменением или без него, например. 7u4 и 7u6. Вероятно, вы должны смотреть на журналы GC или другие инструменты мониторинга памяти. Если разница приемлема, все готово!

Предполагая, что общие значения строк до 7u6 вызывают проблему, следующим шагом является попытка простого обходного пути new String(s.substring(...)), чтобы принудительное изменение значения строки. Тогда измерьте это. Опять же, если производительность приемлема для обоих JDK, все готово!

Если окажется, что в нераскрытом случае дополнительный вызов new String() неприемлем, то, вероятно, лучший способ обнаружить этот случай и сделать условный условный вызов "нерезкий", чтобы отразить поле String value, который является char[] и получает его длину:

int getValueLength(String s) throws Exception {
    Field field = String.class.getDeclaredField("value");
    field.setAccessible(true);
    return ((char[])field.get(s)).length;
}

Рассмотрим строку, вызванную вызовом substring(), которая возвращает строку, меньшую, чем оригинал. В общем случае подстрока length() будет отличаться от длины массива value, полученной, как показано выше. В нераскрытом случае они будут одинаковыми. Например:

String s = "abcdefghij".substring(2, 5);
int logicalLength = s.length();
int valueLength = getValueLength(s);

System.out.printf("%d %d ", logicalLength, valueLength);
if (logicalLength != valueLength) {
    System.out.println("shared");
else
    System.out.println("unshared");

В JDK старше 7u6 длина значения будет 10, тогда как на 7u6 или более поздняя длина значения будет равна 3. В обоих случаях, конечно, логическая длина будет 3.

Ответ 2

Это не деталь, о которой вы должны заботиться. Нет! Просто позвоните identifier = new String(identifier) в обоих случаях (JDK6 и JDK7). В JDK6 он создаст копию (по желанию). В JDK7, поскольку подстрока уже является уникальной строкой, конструктор по существу не работает (копия не выполняется - читайте код). Конечно, есть небольшие накладные расходы на создание объекта, но из-за повторного использования объекта в поколении младшего я предлагаю вам квалифицировать разницу в производительности.

Ответ 3

В старых версиях Java String.substring(..) будет использовать тот же массив char в качестве оригинала, с другими offset и count.

В последних версиях Java (согласно комментарию Томаса Мюллера: начиная с версии 1.7 Update 6) это изменилось, а подстроки теперь создаются с помощью нового массива char.

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

String identifier = query.substring(14, 18);
// older Java versions: backed by same char array, different offset and count
// newer Java versions: copy of the desired run of the original char array

identifier = new String(identifier);
// older Java versions: when the backed char array is larger than count, a copy of the desired run will be made
// newer Java versions: trivial operation, create a new String instance which is backed by the same char array, no copy needed.

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

Ответ 4

Вы уверены, что создание строковой копии действительно дорого? Я верю, что оптимизатор JVM обладает встроенными функциями и избегает ненужных копий. Кроме того, в больших текстах анализируются однопроходные алгоритмы, такие как автоматы LALR, созданные компиляторами компилятора. Таким образом, ввод парсера обычно представляет собой java.io.Reader или другой интерфейс потоковой передачи, а не сплошной String. Парсинг сам по себе дорог, все еще не так дорого, как проверка типов. Я не думаю, что копирование строк является настоящим узким местом. Перед вашими предположениями лучше работать с профилировщиком и микрочипами.