java - software - preguntas frecuentes en una entrevista android
Pregunta de la entrevista: Objetos elegibles para la recolección de basura (7)
Dar el siguiente código:
class A {
Boolean b;
A easyMethod(A a){
a = null;
return a;
}
public static void main(String [] args){
A a1 = new A();
A a2 = new A();
A a3 = new A();
a3 = a1.easyMethod(a2);
a1 = null;
// Some other code
}
}
La pregunta es cuántos objetos son elegibles para la recolección de basura justo antes de // Some other code
.
Entonces, la respuesta correcta es (al menos esa es la respuesta del entrevistador): 2 - la booleana b
porque es una envoltura y a1
.
¿Podría por favor explicarme por qué a2
y a3
no se recogen basura?
MÁS TARDE:
- Ok, creo que lo entiendo ahora. Fue un poco confuso al principio, pero ahora estoy seguro de que el entrevistador estaba equivocado. Mi error inicial fue que al principio no consideraba que Java solo se pasa por valor, por lo que es imposible hacer a2 nulo dentro de una función que tome "a2" como parámetro, porque a2 es en realidad una copia de a2.
- La parte con el booleano b era de hecho bastante obvia.
Gracias por una respuesta, enviaré algunos comentarios de la entrevista después de eso :).
¿Podría por favor explicarme por qué a2 y a3 no se recogen basura?
Porque a2 y a3 no son objetos. Son variables. Las variables no son coleccionables por la sencilla razón de que no son asignables.
La pregunta es cuántos objetos son elegibles para la recolección de basura justo antes de
// Some other code.
La pregunta es absurda.
Los recolectores de basura actúan en tiempo de ejecución sobre la información disponible allí. La accesibilidad está determinada por las raíces globales almacenadas en los registros, en las pilas de hilos y en las variables globales. El contenido de los registros y las pilas son la culminación de muchas etapas de compilación que destruyen completamente el código. Los conceptos de alcance léxico y números de línea del código fuente ya no existen, por lo que no tiene sentido hacer preguntas sobre lo que el GC puede ver en ciertos puntos del código fuente porque esos puntos no existen en el mundo del GC.
Por lo tanto, primero debemos asumir una correspondencia directa entre el código fuente y el código generado, o incluso no podemos entender el punto en el código al que se refiere la pregunta. No puedo pensar en ninguna VM en funcionamiento que realmente haga esto en la práctica y, de hecho, Java probablemente tenga construcciones de lenguaje de alto nivel que ni siquiera pueden compilarse en bytecode de esta manera.
A continuación, debemos asumir un modelo para la forma en que se mantienen las referencias locales en la pila. En lugar de asumir algún modelo mal definido para obtener una respuesta aleatoria, voy a mostrar que la elección del modelo nos permite llegar a respuestas que van desde "nada es elegible para GC" a "todo es elegible para GC". Esta gama completa de respuestas justificables realmente resalta lo mala que es esa pregunta.
Considere un modelo simple donde los valores intermedios (los resultados de todas las subexpresiones y las variables) se insertan en la pila hasta el final de la función. Los compiladores simples y las máquinas virtuales como HLVM y .NET en Windows Phone 7 realmente funcionan así en la práctica, aunque esto conserva las referencias por más tiempo del necesario. Con este modelo, cada new A()
inserta una referencia en la pila hasta que la función regresa, de modo que las tres sean accesibles desde la pila en el punto en cuestión y, por lo tanto, nada es elegible para la recolección de basura.
Considere un modelo en el que las referencias se eliminen de la pila en el primer punto del que nunca se volverán a leer. Esto se acerca más a la forma en que funcionan las máquinas virtuales de producción como .NET y OCaml, excepto que mantienen referencias locales en los registros cuando es posible y derraman entradas asignadas previamente en el marco de pila de la llamada de función, sobrescribiendo locales muertos para minimizar el tamaño del marco de pila. Con este modelo, todas las referencias están muertas en el punto en cuestión, de modo que no se puede acceder a ellas y, por lo tanto, los tres objetos recientemente asignados son elegibles para la recolección de basura, así como la matriz de args
y todas las cadenas a las que hace referencia.
Por lo tanto, no solo hemos demostrado que las respuestas que van de nothing
a everything
pueden justificarse, sino que incluso hemos citado máquinas virtuales en funcionamiento y recolectores de basura que implementan estos modelos para que nuestras predicciones sean verificables.
Si me dieran esa pregunta en una entrevista, pondría a prueba al entrevistador explicando algo de esto. Si reaccionaran bien expresando interés en aprender, todavía estaría interesado en el trabajo. Si reaccionaran mal al tratar de defender sus suposiciones mal definidas y predicciones no verificables, entonces no querría trabajar con ellos.
En primer lugar, el entrevistador se equivoca con respecto al Booleano: este código no crea ningún objeto, por lo que no hay nada que recoger como basura.
Es incorrecto hablar de variables como b
y a2
como basura recolectada. Los objetos son recolectados en basura, no variables. Si una variable dentro del alcance hace referencia a un objeto, entonces no puede ser recolectada como basura. De manera simplista, solo cuando una variable ya no hace referencia a un objeto, se puede recolectar basura.
Entonces tenemos tres instancias de A siendo creadas en este código. Comienzan con referencias a1
etc. pero como las referencias de las variables cambian, me referiré a las instancias de objetos como A1, A2 y A3. Dado que no ha mostrado una definición del método go
, voy a asumir que está destinado a ser una llamada a easyMethod
.
Dado que la variable a1 se reasigna a nula, nada apunta a la instancia A1, por lo que puede ser recolectada como basura.
Dado que la variable a2 nunca se reasigna (la asignación en easyMethod
no afecta a la variable original), la instancia A2 no puede ser recolectada como basura.
Como easyMethod
siempre devuelve null
y a3 se le asigna el resultado de ese método, nada apunta a la instancia A3, por lo que también puede ser recolectado como basura.
Para el referente original de a2, en realidad depende completamente de lo que suceda en "algún otro código". Si "algún otro código" no usa a2 o a3, entonces el objeto a2 original es elegible para la recolección de basura.
Eso es porque el tiempo de ejecución no tiene que preocuparse por el alcance léxico . Solo necesita saber que un objeto nunca puede ser referenciado nuevamente. Por lo tanto, si "algún otro código" no utiliza a2 o a3, el objeto al que apuntan nunca podrá volver a hacer referencia y, por lo tanto, ya está disponible para la recolección de basura.
Siempre que a1.go(a2)
esté destinado a ser a1.easyMethod(a2)
, la respuesta es ciertamente 2, pero no las que enumeró. Como bien señaló Bozho, b
no está inicializado, por lo que no se refiere a ningún objeto. Los dos objetos elegibles para la recolección de basura en el punto del comentario son los originalmente referenciados por a1
y a3
.
a1
obviamente se anula, y a3
se reasigna al valor de retorno de a1.easyMethod(a2)
, que es nulo. Sin embargo, a2
no se ve afectado por la llamada al método, ya que Java se pasa por valor , por lo que solo se pasa una copia de la referencia a2
al método. Aunque la copia está configurada en nulo, eso no afecta el valor del a2
original.
Suponiendo que go
se supone que es easyMethod
. El easyMethod
funciona así.
class A {
Boolean b;
A easyMethod(A a){
a = null; // the reference to a2 was passed in, but is set to null
// a2 is not set to null - this copy of a reference is!
return a; // null is returned
}
public static void main(String [] args){
A a1 = new A(); // 1 obj
A a2 = new A(); // 2 obj
A a3 = new A(); // 3 obj
a3 = a1.go(a2); // a3 set to null and flagged for GC - see above for why
a1 = null; // so far, a1 and a3 have been set to null and flagged
// Some other code
}
}
Dos objetos son elegibles para la recolección de basura (a1 y a3). b
no es porque es solo una referencia a nulo. Ningún Boolean
se hizo nunca.
Para sortear las sutiles sutilezas de lo que // Some other code
podría ser // Some other code
, en cambio propongo que la pregunta se vuelva a redactar de la siguiente manera:
Predecir y explicar el siguiente resultado:
class A {
int i;
A(int i) { this.i = i; }
public String toString() { return ""+i; }
A go(A a){
a = null; // the reference to a2 was passed in, but is set to null
// a2 is not set to null - this copy of a reference is!
return a; // null is returned
}
public static void main(String [] args){
A a1 = new A(1); // 1 obj
A a2 = new A(2); // 2 obj
A a3 = new A(3); // 3 obj
a3 = a1.go(a2); // a3 set to null and flagged for GC - see above for why
a1 = null; // so far, a1 and a3 have been set to null and flagged
test(a1);
test(a2);
test(a3);
}
static void test(A a) {
try { System.out.println(a); }
catch(Exception e) { System.out.println((String)null); }
}
}
Y salida:
c:/files/j>javac A.java
c:/files/j>java A
null
2
null
Y el seguimiento es que en ese momento, a1 y a3 eran elegibles para GC, y a2 no lo era.
La lección de esta pregunta es que "Pasar una referencia de objeto a un método y establecer esa referencia en nulo no hace que la referencia original se anule". Esa es la información que el entrevistador estaba intentando probar.
Tu pregunta es bastante confusa.
El recolector de basura trabaja en objetos, no en variables. Así que cuando dices que a2 es elegible para GC no significa nada. Debe decir que el objeto al que hace referencia a2 en la línea N es elegible para GC en la línea N + M.
En tu ejemplo solo tienes 3 objetos siendo instanciados. Es la primera instancia de crear A y la última instancia de crear A que son elegibles para GC.