Как определить, копирует ли 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. Второй вариант - создать огромную строку, затем несколько подстрок и проверить использование памяти.

4 ответа

Решение

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

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

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

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

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

Если окажется, что в неразделенном случае, дополнительный вызов new String() неприемлемо, тогда, вероятно, лучший способ выявить этот случай и сделать условный вызов "unarsaring" состоит в отражении строки 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.

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

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

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

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

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.

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

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

Другие вопросы по тегам