que - Las características de bytecode no están disponibles en el lenguaje Java
que es la maquina virtual de java (9)
Algo que puede hacer con el código de bytes, en lugar del código Java simple, es generar código que puede cargarse y ejecutarse sin un compilador. Muchos sistemas tienen JRE en lugar de JDK y, si desea generar código de forma dinámica, puede ser mejor, si no más fácil, generar código de bytes en lugar de compilar el código de Java antes de poder utilizarlo.
¿Hay actualmente (Java 6) cosas que pueda hacer en bytecode de Java que no pueda hacer desde el lenguaje Java?
Sé que ambos están completos, así que lea "puedo hacer" ya que "puedo hacer mucho más rápido / mejor, o simplemente de una manera diferente".
Estoy pensando en invokedynamic
adicionales como invokedynamic
, que no se pueden generar con Java, excepto que uno específico es para una versión futura.
Aquí hay algunas características que se pueden hacer en bytecode de Java pero no en código fuente de Java:
Lanzar una excepción marcada de un método sin declarar que el método lo arroja. Las excepciones marcadas y no revisadas son algo que solo se comprueba mediante el compilador de Java, no la JVM. Debido a esto, por ejemplo, Scala puede arrojar excepciones comprobadas de los métodos sin declararlos. Sin embargo, con los genéricos de Java hay una solución alternativa llamada tiro furtivo .
Tener dos métodos en una clase que solo difieren en el tipo de retorno, como ya se mencionó en la respuesta de Joachim : La especificación del lenguaje Java no permite dos métodos en la misma clase cuando difieren solo en su tipo de retorno (es decir, mismo nombre, misma lista de argumentos, ...). Sin embargo, la especificación de JVM no tiene tal restricción, por lo que un archivo de clase puede contener dos de estos métodos, simplemente no hay forma de producir dicho archivo de clase utilizando el compilador de Java normal. Hay un buen ejemplo / explicación en esta respuesta .
Después de trabajar con el código de bytes de Java durante bastante tiempo y hacer algunas investigaciones adicionales sobre este asunto, aquí hay un resumen de mis hallazgos:
Ejecutar código en un constructor antes de llamar a un súper constructor o constructor auxiliar
En el lenguaje de programación Java (JPL), la primera declaración de un constructor debe ser una invocación de un super constructor u otro constructor de la misma clase. Esto no es cierto para el código de bytes Java (JBC). Dentro del código de bytes, es absolutamente legítimo ejecutar cualquier código antes de un constructor, siempre que:
- Se llama a otro constructor compatible en algún momento después de este bloque de código.
- Esta llamada no está dentro de una declaración condicional.
- Antes de esta llamada al constructor, no se lee ningún campo de la instancia construida y no se invoca ninguno de sus métodos. Esto implica el siguiente artículo.
Establecer campos de instancia antes de llamar a un super constructor o constructor auxiliar
Como se mencionó anteriormente, es perfectamente legal establecer un valor de campo de una instancia antes de llamar a otro constructor. Incluso existe un truco heredado que le permite explotar esta "característica" en las versiones de Java anteriores a 6:
class Foo {
public String s;
public Foo() {
System.out.println(s);
}
}
class Bar extends Foo {
public Bar() {
this(s = "Hello World!");
}
private Bar(String helper) {
super();
}
}
De esta manera, un campo podría establecerse antes de que se invoque el superconstructor, que, sin embargo, ya no es posible. En JBC, este comportamiento aún se puede implementar.
Branch una llamada súper constructor
En Java, no es posible definir una llamada de constructor como
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
Hasta Java 7u23, el verificador de la máquina virtual de HotSpot sin embargo omitió esta comprobación y es por eso que fue posible. Esto fue utilizado por varias herramientas de generación de código como una especie de truco, pero ya no es legal implementar una clase como esta.
Este último fue simplemente un error en esta versión del compilador. En las versiones de compilador más nuevas, esto nuevamente es posible.
Definir una clase sin ningún constructor
El compilador de Java siempre implementará al menos un constructor para cualquier clase. En el código de bytes de Java, esto no es obligatorio. Esto permite la creación de clases que no se pueden construir incluso cuando se utiliza la reflexión. Sin embargo, el uso de sun.misc.Unsafe
aún permite la creación de tales instancias.
Definir métodos con firma idéntica pero con diferente tipo de devolución
En el JPL, un método se identifica como único por su nombre y sus tipos de parámetros brutos. En JBC, el tipo de devolución sin procesar también se considera.
Defina campos que no difieren por nombre pero solo por tipo
Un archivo de clase puede contener varios campos del mismo nombre siempre que declare un tipo de campo diferente. La JVM siempre se refiere a un campo como una tupla de nombre y tipo.
Lanza excepciones comprobadas no declaradas sin capturarlas
El tiempo de ejecución de Java y el código de bytes de Java no conocen el concepto de excepciones comprobadas. Solo el compilador de Java verifica que las excepciones comprobadas siempre se capturan o declaran si se lanzan.
Usar la invocación de método dinámico fuera de las expresiones lambda
La llamada invocación de método dinámico se puede utilizar para cualquier cosa, no solo para las expresiones lambda de Java. El uso de esta función permite, por ejemplo, cambiar la lógica de ejecución en el tiempo de ejecución. Muchos lenguajes de programación dinámicos que se reducen a JBC mejoraron su rendimiento al usar esta instrucción. En el código de bytes Java, también podría emular las expresiones lambda en Java 7, donde el compilador aún no permitía el uso de la invocación de métodos dinámicos mientras la JVM ya entendía las instrucciones.
Use identificadores que normalmente no se consideran legales
¿Alguna vez imaginó usar espacios y un salto de línea en el nombre de su método? Crea tu propio JBC y buena suerte para la revisión del código. Los únicos caracteres ilegales para los identificadores son .
, ;
, [
y /
. Además, los métodos que no se denominan <init>
o <clinit>
no pueden contener <
y >
.
Volver a asignar los parámetros final
o la referencia de this
final
parámetros final
no existen en JBC y, por lo tanto, pueden reasignarse. Cualquier parámetro, incluida la referencia, solo se almacena en una matriz simple dentro de la JVM, lo que permite reasignar this
referencia en el índice 0
dentro de un único marco de método.
Reasignar campos final
Siempre que se asigne un campo final dentro de un constructor, es legal reasignar este valor o incluso no asignar ningún valor. Por lo tanto, los siguientes dos constructores son legales:
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
Para campos static final
, incluso se permite reasignar los campos fuera del inicializador de clase.
Trate a los constructores y al inicializador de clase como si fueran métodos
Esto es más una característica conceptual, pero los constructores no son tratados de manera diferente dentro de JBC que los métodos normales. Solo el verificador de JVM asegura que los constructores llamen a otro constructor legal. Aparte de eso, es simplemente una convención de nomenclatura de Java que los constructores deben llamarse <init>
y que el inicializador de clase se llama <clinit>
. Además de esta diferencia, la representación de métodos y constructores es idéntica. Como señaló Holger en un comentario, incluso puede definir constructores con tipos de retorno distintos de void
o un inicializador de clase con argumentos, aunque no es posible llamar a estos métodos.
Llamar a cualquier método super (hasta Java 1.1)
Sin embargo, esto solo es posible para las versiones Java 1 y 1.1. En JBC, los métodos siempre se envían en un tipo de objetivo explícito. Esto significa que para
class Foo {
void baz() { System.out.println("Foo"); }
}
class Bar extends Foo {
@Override
void baz() { System.out.println("Bar"); }
}
class Qux extends Bar {
@Override
void baz() { System.out.println("Qux"); }
}
fue posible implementar Qux#baz
para invocar a Foo#baz
mientras saltaba sobre Bar#baz
. Si bien aún es posible definir una invocación explícita para llamar a otra implementación de supermétodo que la de la superclase directa, esto ya no tiene ningún efecto en las versiones de Java posteriores a 1.1. En Java 1.1, este comportamiento se controló estableciendo el indicador ACC_SUPER
que habilitaría el mismo comportamiento que solo llama a la implementación directa de la ACC_SUPER
.
Definir una llamada no virtual de un método declarado en la misma clase
En Java, no es posible definir una clase
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
El código anterior siempre dará como resultado una RuntimeException
cuando foo
se invoca en una instancia de Bar
. No es posible definir el método Foo::foo
para invocar su propio método de bar
que se define en Foo
. Como bar
es un método de instancia no privada, la llamada es siempre virtual. Sin embargo, con el código de bytes, uno puede definir la invocación para usar el INVOKESPECIAL
operación INVOKESPECIAL
que vincula directamente la llamada de método de bar
en Foo::foo
a la versión de Foo
. Este código de operación se usa normalmente para implementar invocaciones de supermétodo, pero puede reutilizar el código de operación para implementar el comportamiento descrito.
Anotaciones de tipo de grano fino
En Java, las anotaciones se aplican según su @Target
que @Target
las anotaciones. Usando la manipulación de código de byte, es posible definir anotaciones independientemente de este control. Además, es posible, por ejemplo, anotar un tipo de parámetro sin anotar el parámetro, incluso si la anotación @Target
aplica a ambos elementos.
Definir cualquier atributo para un tipo o sus miembros
Dentro del lenguaje Java, solo es posible definir anotaciones para campos, métodos o clases. En JBC, básicamente puedes insertar cualquier información en las clases de Java. Sin embargo, para utilizar esta información, ya no puede confiar en el mecanismo de carga de clases de Java, pero necesita extraer la metainformación usted mismo.
Desbordamiento y asigna implícitamente valores byte
, short
, char
y boolean
Los últimos tipos primitivos no se conocen normalmente en JBC, pero solo se definen para tipos de matriz o para descriptores de campo y método. Dentro de las instrucciones del código de bytes, todos los tipos nombrados toman el espacio de 32 bits que permite representarlos como int
. Oficialmente, solo existen los tipos int
, float
, long
y double
en el código byte, que necesitan una conversión explícita según la regla del verificador de la JVM.
No lanzar un monitor
Un bloque synchronized
realidad está compuesto por dos instrucciones, una para adquirir y otra para liberar un monitor. En JBC, puedes adquirir uno sin soltarlo.
Nota : En implementaciones recientes de HotSpot, esto en cambio lleva a IllegalMonitorStateException
al final de un método o a una versión implícita si el método es terminado por una excepción en sí misma.
Agregue más de una declaración de return
a un inicializador de tipo
En Java, incluso un inicializador de tipo trivial como
class Foo {
static {
return;
}
}
es ilegal. En el código de bytes, el inicializador de tipo se trata como cualquier otro método, es decir, declaraciones de retorno se pueden definir en cualquier lugar.
Crear bucles irreducibles
El compilador de Java convierte bucles en goto en código de bytes Java. Tales declaraciones se pueden usar para crear bucles irreducibles, lo que el compilador de Java nunca hace.
Definir un bloque de catch recursivo
En el código de bytes de Java, puede definir un bloque:
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
Una declaración similar se crea implícitamente cuando se utiliza un bloque synchronized
en Java donde cualquier excepción al liberar un monitor vuelve a la instrucción para liberar este monitor. Normalmente, no debería haber ninguna excepción en tal instrucción, pero si lo hiciera (por ejemplo, el ThreadDeath
desuso), el monitor aún se lanzaría.
Llamar a cualquier método predeterminado
El compilador de Java requiere que se cumplan varias condiciones para permitir la invocación de un método predeterminado:
- El método debe ser el más específico (no debe ser anulado por una interfaz secundaria que se implemente por cualquier tipo, incluidos los súper tipos).
- El tipo de interfaz del método predeterminado debe ser implementado directamente por la clase que llama al método predeterminado. Sin embargo, si la interfaz
B
extiende la interfazA
pero no anula un método enA
, el método aún puede invocarse.
Para el código de bytes de Java, solo la segunda condición cuenta. El primero es sin embargo irrelevante.
Invocar un supermétodo en una instancia que no sea this
El compilador de Java solo permite invocar un método super (o interfaz predeterminada) en instancias de this
. En el código de bytes, sin embargo, también es posible invocar el método super en una instancia del mismo tipo similar a la siguiente:
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
Acceso a miembros sintéticos
En el código de bytes de Java, es posible acceder a miembros sintéticos directamente. Por ejemplo, considere cómo en el siguiente ejemplo se accede a la instancia externa de otra instancia de Bar
:
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
Esto es generalmente cierto para cualquier campo sintético, clase o método.
Definir información de tipo genérico fuera de sincronización
Si bien el tiempo de ejecución de Java no procesa los tipos genéricos (después de que el compilador de Java aplica el borrado de tipo), esta información sigue atada a una clase compilada como metainformación y se hace accesible a través de la API de reflexión.
El verificador no verifica la consistencia de estos valores codificados en String
metadatos. Por lo tanto, es posible definir información sobre tipos genéricos que no coincida con el borrado. Como una consecuencia, las siguientes afirmaciones pueden ser ciertas:
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
Además, la firma se puede definir como no válida de manera que se genera una excepción de tiempo de ejecución. Esta excepción se produce cuando se accede a la información por primera vez, ya que se evalúa perezosamente. (Similar a los valores de anotación con un error)
Anexar metainformación de parámetros solo para ciertos métodos
El compilador de Java permite incrustar el nombre del parámetro y la información del modificador al compilar una clase con el indicador de parameter
habilitado. Sin embargo, en el formato de archivo de la clase Java, esta información se almacena por método, lo que permite incrustar dicha información de método solo para ciertos métodos.
Desordenar las cosas y dañar tu JVM
Como ejemplo, en el código de bytes de Java, puede definir invocar cualquier método en cualquier tipo. Por lo general, el verificador se quejará si un tipo no conoce dicho método. Sin embargo, si invoca un método desconocido en una matriz, encontré un error en alguna versión de JVM donde el verificador lo omitirá y su JVM finalizará una vez que se invoque la instrucción. Sin embargo, esto no es una característica, pero técnicamente es algo que no es posible con Java compilado Java. Java tiene algún tipo de doble validación. La primera validación es aplicada por el compilador de Java, la segunda por la JVM cuando se carga una clase. Al omitir el compilador, puede encontrar un punto débil en la validación del verificador. Sin embargo, esto es más una afirmación general que una característica.
Anotar el tipo de receptor del constructor cuando no hay clase externa
Desde Java 8, los métodos no estáticos y los constructores de clases internas pueden declarar un tipo de receptor y anotar estos tipos. Los constructores de clases de nivel superior no pueden anotar su tipo de receptor ya que la mayoría no declara uno.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Como Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
sin embargo devuelve un AnnotatedType
representa a Foo
, es posible incluir anotaciones de tipo para el constructor de Foo
directamente en el archivo de clase donde estas anotaciones son leídas posteriormente por la API de reflexión.
Usar instrucciones de código de bytes heredadas / no utilizadas
Como otros lo nombraron, lo incluiré también. Anteriormente, Java hacía uso de subrutinas mediante las sentencias JSR
y RET
. JBC incluso conocía su propio tipo de dirección de retorno para este propósito. Sin embargo, el uso de subrutinas complicó en exceso el análisis de código estático, por lo que estas instrucciones ya no se usan. En cambio, el compilador de Java duplicará el código que compila. Sin embargo, esto básicamente crea una lógica idéntica y es por eso que realmente no considero que logre algo diferente. Del mismo modo, podría, por ejemplo, agregar la instrucción de código de bytes de NOOP
que no utiliza el compilador de Java, pero esto tampoco le permitiría lograr algo nuevo. Como se señala en el contexto, estas "instrucciones de funciones" mencionadas ahora se eliminan del conjunto de códigos de operación legales, lo que los hace aún menos característicos.
En Java, si intenta anular un método público con un método protegido (o cualquier otra reducción en el acceso), se produce un error: "intentar asignar privilegios de acceso más débiles". Si lo hace con código de bytes JVM, el verificador está de acuerdo, y puede llamar a estos métodos a través de la clase principal como si fueran públicos.
En lenguaje Java, la primera declaración en un constructor debe ser una llamada al constructor de la superclase. Bytecode no tiene esta limitación, en cambio, la regla es que el constructor de la superclase u otro constructor de la misma clase debe invocarse para el objeto antes de acceder a los miembros. Esto debería permitir más libertad, como:
- Cree una instancia de otro objeto, almacénelo en una variable local (o pila) y páselo como un parámetro al constructor de la superclase mientras mantiene la referencia en esa variable para otro uso.
- Llamar a otros constructores diferentes en función de una condición. Esto debería ser posible: ¿cómo llamar a un constructor diferente condicionalmente en Java?
No los he probado, así que por favor corrígeme si me equivoco.
Escribí un optimizador de bytecode cuando era un I-Play, (fue diseñado para reducir el tamaño del código para las aplicaciones J2ME). Una característica que agregué fue la capacidad de usar bytecode en línea (similar al lenguaje ensamblador en línea en C ++). Logré reducir el tamaño de una función que era parte de un método de biblioteca utilizando la instrucción DUP, ya que necesito el valor dos veces. También tenía instrucciones de cero bytes (si estás llamando a un método que requiere una char y quieres pasar un int, que sabes que no necesita ser lanzado, agregué int2char (var) para reemplazar char (var) y eliminaría la instrucción i2c para reducir el tamaño del código. También hice que flotase a = 2.3; float b = 3.4; float c = a + b; y que se convertiría a punto fijo (más rápido, y también algo de J2ME) soporte punto flotante).
Hasta donde sé, no hay características principales en los códigos de bytes admitidos por Java 6 que tampoco son accesibles desde el código fuente de Java. La razón principal de esto es obviamente que el bytecode de Java fue diseñado con el lenguaje Java en mente.
Sin embargo, hay algunas características que no son producidas por los compiladores de Java modernos:
El indicador
ACC_SUPER
:Esta es una bandera que se puede establecer en una clase y especifica cómo se maneja un caso de esquina específico del bytecode
invokespecial
para esta clase. Está establecido por todos los compiladores modernos de Java (donde "moderno" es> = Java 1.1, si no recuerdo mal) y solo los antiguos compiladores de Java producían archivos de clase en los que esto no estaba establecido. Esta marca existe solo por razones de compatibilidad con versiones anteriores. Tenga en cuenta que a partir de Java 7u51, ACC_SUPER se ignora por completo debido a razones de seguridad.Los
jsr
/ret
.Estos códigos de bytes se usaron para implementar sub-rutinas (principalmente para implementar
finally
bloques). Ya no se producen desde Java 6 . La razón de su desaprobación es que complican mucho la verificación estática sin grandes ganancias (es decir, el código que se utiliza casi siempre se puede volver a implementar con saltos normales con muy poca sobrecarga).Tener dos métodos en una clase que solo difieren en el tipo de devolución.
La especificación del lenguaje Java no permite dos métodos en la misma clase cuando difieren solo en su tipo de devolución (es decir, el mismo nombre, la misma lista de argumentos, ...). Sin embargo, la especificación de JVM no tiene tal restricción, por lo que un archivo de clase puede contener dos de estos métodos, simplemente no hay forma de producir dicho archivo de clase utilizando el compilador de Java normal. Hay un buen ejemplo / explicación en esta respuesta .
Tal vez la sección 7A de este documento sea de interés, aunque se trata de trampas de bytecode en lugar de características de bytecode.
-
GOTO
se puede utilizar con etiquetas para crear sus propias estructuras de control (que no seanfor
while
etc.) - Puede anular
this
variable local dentro de un método - Combinando ambos puede crear JCompilo de JCompilo creado para crear cola optimizada (hago esto en JCompilo )
Como punto relacionado, puede obtener el nombre del parámetro para los métodos si se compila con la depuración ( Paranamer lo hace leyendo el bytecode