Java 6 introdujo un mecanismo para almacenar caracteres ASCII en byte[] en lugar de en char[]. Esta característica se eliminó de nuevo en Java 7. Aún así, volverá en Java 9, pero en esta ocasión, por defecto está habilitada la compresión y se usa siempre byte[].
Éste artículo es una traducción al castellano de la entrada original publicada, en inglés, por Dr. Heinz Kabutz en su número 237 del JavaSpecialists newsletter. Puedes consultar el texto original en Javaspecialists’ Newsletter #237: String compaction
Este artículo se publica en Adictos al Trabajo, con permiso del autor, traducido por David Gómez García, (@dgomezg) consultor tecnológico en Autentia, colaborador de Javaspecialists e instructor certificado para impartir los cursos de Javaspecialists en Español.
[/box]
Strings compactos
Estuve una semana entera instruyendo a un grupo de buenos desarrolladores Java de Provinzial Rheinland Versicherung AG sobre las sutilezas de los patrones de diseño en Java. Os sorprendería saber que mi Patterns Course es, de todos, el más popular. Ni el Advanced concurrency and performance for Java 8 (aunque también está bastante solicitado), ni el curso de introdución a Java (no he impartido ninguno en los últimos años), ni siquiera el Advanced Topics in Java. No, mi humilde curso de patrones de diseño que escribí ya en 2001 es todavía el más demandando en la actualidad.
Normalmente, cuando imparto mi curso de patrones, vemos todo tipo de temas relacionados en Java. De esta forma, los asistentes aprenden mucho más de lo que encontrarían en cualquier libro: pueden ver dónde se utilizan los patrones en la propia JDK; aprenden buenos principios de diseño; conocen las últimas mejoras de Java 8, aunque aún estén anclados en JDK 6 o 7; incluso tocamos un poquito de concurrencia. Este es el curso que, una vez que una empresa ha inscrito a sus programadores, habitualmente siguen enviando a más y más de ellos. Esta es la razón por la que, 15 años después de escribir la primera versión, aún es popular entre las empresas.
En una de esas jornadas, estábamos revisando el patrón Flyweight, que tiene una estructura de clases bastante extraña. No es realmente un patrón de diseño. Sin embargo, como el Facade, es un mal necesario en los patrones de diseño. Me explico. Un buen diseño orientado a objetos produce sistemas altamente configurables y reducen la duplicidad de código. Y esto es bueno, pero también implica a veces bastante trabajo para utilizar el sistema. El patrón Facade hace que un subsistema complejo sea más fácil de utilizar. ¿Por qué es complejo?. Porque normalmente tenemos muchas formas de usarlo, gracias a un uso generoso de los propios patrones de diseño. Flyweight tiene una razón de ser parecida. Normalmente, los buenos diseños en orientación a objetos tienen muchos más objetos (e instancias) que los diseños monolíticos, donde todo es un Singleton. Flyweight trata de reducir el número de objetos, compartiendo las instancias de aquellos que son iguales; lo que es posible si desde fuera no dependemos (o modificamos) el estado interno de los mismos.
Estábamos viendo la fusión de Strings en clase, y cómo el char[] interno en String se reemplaza con un char[] compartido cuando tenemos varios String que contienen el mismo valor. Para ello hay que utilizar G1 como Garbage Collector (-XX:+UseG1GC) y también activar la característica de String deduplication (-XX:+UseStringDeduplication). Funciona muy bien en Java 8. Pero quería comprobar si estaba activo por defecto en Java 9, dado que G1 es su Garbage Collector por defecto. Me sorprendió que mi código ahora produjese un ClassCastException al intentar convertir el campo value de String a char[].
En algún momento con Java 6, aparecieron los String comprimidos. Dicho comportamiento estaba desactivado por defecto, aunque se podía activar con -XX:+UseCompressedStrings. Al activarlo, los Strings que contienen únicamente caracteres ASCII (codificables en 7-bits) pasan automáticamente a tener un byte[] como estructura interna. Con sólo un carácter en el String que necesite más de 7 bits para codificarlo, el String utilizaba de nuevo un char[]. Más curioso es cuando el String contiene caracteres UTF-16, como los del alfabeto Hindi Devanagari, porque en ese caso se crean objetos adicionales y al final tenemos una tasa de creación de objetos aún mayor que sin la compresión de Strings. Pero para el caso de los caracteres US ASCII, es perfecto. Por alguna razón, esta característica de Java 6 se descartó en Java 7 y la opción para activarlo se eliminó definitivamente en Java 8.
Pero con Java 9, aparece la nueva opción -XX:+CompactStrings, activa por defecto. Si examinas internamente la clase String, verás que siempre almacena los caracteres de la cadena en un byte[]. También hay un nuevo campo de tipo byte que almacena el encoding, que de momento puede ser Latin1 (0) o UTF-16 (1). En el futuro podrían añadirse otros valores. Así que, si tus caracteres son Latin1, tu String ocupará menos memoria.
Para probarlo, he escrito un pequeño programa en Java que puede ejecutarse con Java 6, Java 7 y Java 9 para ver las diferencias:
import java.lang.reflect.*; public class StringCompactionTest { private static Field valueField; static { try { valueField = String.class.getDeclaredField("value"); valueField.setAccessible(true); } catch (NoSuchFieldException e) { throw new ExceptionInInitializerError(e); } } public static void main(String... args) throws IllegalAccessException { showGoryDetails("hello world"); showGoryDetails("hello w\u00f8rld"); // Scandinavian o showGoryDetails("he\u03bb\u03bbo wor\u03bbd"); // Greek l } private static void showGoryDetails(String s) throws IllegalAccessException { s = "" + s; System.out.printf("Details of String \"%s\"\n", s); System.out.printf("Identity Hash of String: 0x%x%n", System.identityHashCode(s)); Object value = valueField.get(s); System.out.println("Type of value field: " + value.getClass().getSimpleName()); System.out.println("Length of value field: " + Array.getLength(value)); System.out.printf("Identity Hash of value: 0x%x%n", System.identityHashCode(value)); System.out.println(); } }
Esta es la primera ejecución, con Java 6 y el flag -XX:-UseCompressedStrings (por defecto). Observa como cada uno de los Strings contiene un char[].
Java6 no compaction java version "1.6.0_65" Details of String "hello world" Identity Hash of String: 0x7b1ddcde Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x6c6e70c7 Details of String "hello wørld" Identity Hash of String: 0x46ae506e Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x5e228a02 Details of String "heλλo worλd" Identity Hash of String: 0x2d92b996 Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x7bd63e39
La segunda vez, lo ejecutamos con Java 6 y -XX:+UseCompressedStrings. La cadena «hello world» contiene un byte[] y las otras dos un char[]. Sólo se comprimen los caracteres US ASCII (de 7 bits).
Java6 compaction java version "1.6.0_65" Details of String "hello world" Identity Hash of String: 0x46ae506e Type of value field: byte[] Length of value field: 11 Identity Hash of value: 0x7bd63e39 Details of String "hello wørld" Identity Hash of String: 0x42b988a6 Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x22ba6c83 Details of String "heλλo worλd" Identity Hash of String: 0x7d2a1e44 Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x5829428e
En Java 7 el flag se ignora. En Java 8 se eliminó, por tanto, la JVM con -XX:+UseCompressedStrings no arrancará. Por supuesto, todos los Strings contienen un char[].
Java7 compaction Java HotSpot(TM) 64-Bit Server VM warning: ignoring option UseCompressedStrings; support was removed in 7.0 java version "1.7.0_80" Details of String "hello world" Identity Hash of String: 0xa89848d Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x57fd54c4 Details of String "hello wørld" Identity Hash of String: 0x38c83cfd Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x621c232a Details of String "heλλo worλd" Identity Hash of String: 0x2548ccb8 Type of value field: char[] Length of value field: 11 Identity Hash of value: 0x4e785727
En Java 9 tenemos el nuevo flag -XX:+CompactStrings, activo por defecto. Los Strings ahora almacenan siempre su carga en un byte[], independientenmente del encoding. Por ejemplo, vemos que para Latin1, todos los bytes están comprimidos.
Java9 compaction java version "9-ea" Details of String "hello world" Identity Hash of String: 0x77f03bb1 Type of value field: byte[] Length of value field: 11 Identity Hash of value: 0x7a92922 Details of String "hello wørld" Identity Hash of String: 0x71f2a7d5 Type of value field: byte[] Length of value field: 11 Identity Hash of value: 0x2cfb4a64 Details of String "heλλo worλd" Identity Hash of String: 0x5474c6c Type of value field: byte[] Length of value field: 22 Identity Hash of value: 0x4b6995df
Naturalmente, podemos desactivar esta característica de Java 9 con -XX:-CompactStrings. Sin embargo, la estructura de String ha cambiado así que, independientemente de lo que hagas, el campo value sigue siendo un byte[].
Java9 no compaction java version "9-ea" Details of String "hello world" Identity Hash of String: 0x21a06946 Type of value field: byte[] Length of value field: 22 Identity Hash of value: 0x25618e91 Details of String "hello wørld" Identity Hash of String: 0x7a92922 Type of value field: byte[] Length of value field: 22 Identity Hash of value: 0x71f2a7d5 Details of String "heλλo worλd" Identity Hash of String: 0x2cfb4a64 Type of value field: byte[] Length of value field: 22 Identity Hash of value: 0x5474c6c
Cualquiera que utilice introspección para acceder a las tripas de String, podría obtener ahora un ClassCastException. Esperemos que la cantidad de esos programadores sea infinitamente pequeño.
Más preocupante es el rendimiento. Métodos como String.charAt(int)
solían ser rápidos como la pólvora. Puedo notar una ralentización en Java 9. Si recorres habitualmente las cadenas con charAt()
, será mejor que evalues algunas alternativas, ¡aunque estoy seguro de cuáles!. O quizás lo arreglen en la release final de Java 9, al fin y al cabo estoy trabajando con una Early Release (EA).
Me contaron un truquito de Peter Lawrey en una de las JCrete Unconferences: String tiene un constructor que recibe como parámetros un char[] y un boolean. El parámetro boolean no se utiliza nunca y se supone que debes pasarlo como true
, indicando que el char[] se utilizará directamente como value y no se copiará. Este es el código:
String(char[] value, boolean share) { // assert share : "unshared not supported"; this.value = value; }
El truco de Lawrey consiste en crear Strings de forma muy rápida desde un char[] utilizando directamente este constructor. No estoy seguro de los detalles, pero lo más probable es que utilice JavaLangAccess que nos proporciona la clase SharedSecrets. Antes de Java 9, esta clase estaba en el paquete sun.misc.package. Desde Java 9, está en jdk.internal.misc. Espero que no estés utilizando éste constructor directamente, porque tendrás que cambiar tu código cuando actualices a Java 9.
Aquí tienes el código. Tendrás que cambiar los imports en función de la versión de Java que uses
//import sun.misc.*; // prior to Java 9, use this import jdk.internal.misc.*; // since Java 9, use this instead public class StringUnsafeTest { private static String s; public static void main(String... args) { char[] chars = "hello world".toCharArray(); JavaLangAccess javaLang = SharedSecrets.getJavaLangAccess(); long time = System.currentTimeMillis(); for (int i = 0; i < 100 * 1000 * 1000; i++) { s = javaLang.newStringUnsafe(chars); } time = System.currentTimeMillis() - time; System.out.println("time = " + time); } }
En resumen, si hablas inglés, alemán, francés o español, tus Strings son ahora mucho más ligeros. Para los griegos y los chinos, siguen siendo lo mismo. Para todos, seguramente resultarán un poco más lentos.
Saludos desde el aeropuerto de Tesalónica.
Heinz.