Можно ли отследить объект от его финализатора, чтобы обнаружить случайное воскресение объекта другим финализатором объекта?
Одна из многих проблем с finalize
Методы в Java - это проблема "воскрешения объекта" (объясняется в этом вопросе): если объект завершен, и он сохраняет копию this
где-то глобально достижимо, ссылка на объект "ускользает", и вы в конечном итоге получаете завершенный, но живой объект (который не будет завершен снова, а в противном случае возникнет проблема).
Чтобы избежать создания воскрешенных объектов, обычный совет (как, например, видно в этом ответе) состоит в том, чтобы создать новый экземпляр объекта, а не сохранять сам объект; Обычно это достигается путем копирования всех полей объекта в новый объект. В большинстве случаев это позволяет достичь того, чтобы исходный объект был освобожден, а не воскрешен.
Тем не менее, сборщик мусора Java поддерживает сборку мусора ссылочных циклов; это означает, что объект может быть завершен, в то время как (прямо или косвенно) содержит ссылку на себя, и два объекта могут быть завершены, в то время как (прямо или косвенно) содержат ссылки друг на друга. В этом случае совет "скопировать все поля в новый объект" фактически не решает проблему; хотя мы отбрасываем this
ссылка после завершения работы финализатора, частично завершенный объект будет воскрешен через ссылку из поля. Таким образом, мы все равно получаем воскрешение объекта.
В случае, когда объект косвенно содержит ссылку на себя, можно рекурсивно просматривать все поля объекта, пока мы не найдем самоссылку (в этом случае мы можем заменить ее ссылкой на новый объект, который мы находимся строительство), тем самым предотвращая воскресение. Так что это решает проблему в этом случае.
Однако, если два объекта содержат ссылки друг на друга (и, таким образом, оба освобождаются одновременно), и мы создаем новый экземпляр каждого из них, то каждый из новых объектов будет содержать ссылку на старый завершенный объект. (а не новый объект, который был построен как замена). Это, очевидно, нежелательное состояние дел, поэтому я хотел бы попытаться использовать то же решение, что и в случае с одним объектом: рекурсивное сканирование полей (живых, вновь построенных) объектов в поисках завершенных объектов. и заменяя их соответствующими объектами замены.
Проблема в том, как я могу распознать завершенный / воскресший объект, когда я делаю это? Очевидный способ сделать это состоит в том, чтобы как-то записать идентичность завершенного объекта в финализаторе, а затем сравнить все объекты, которые мы находим во время рекурсивного сканирования, со списком завершенных объектов. Проблема в том, что не существует правильного способа записи идентификатора рассматриваемого объекта:
- Регулярная (сильная) ссылка будет поддерживать объект живым, эффективно воскрешая его, и не даст никакого метода, с помощью которого можно будет определить, что на объект фактически нет ссылки. Это решило бы проблему идентификации воскресших объектов, но столкнулось с собственной проблемой: хотя воскрешенные объекты никогда не использовались бы, за исключением их идентичности, не было бы никаких средств для их освобождения (например, вы не можете использовать
PhantomReference
обнаружить, что объект теперь действительно мертв, как вы обычно делаете в Java, потому что объект теперь сильно доступен и, таким образом, ссылка на фантом никогда не очищается). Таким образом, это фактически означает, что рассматриваемые объекты остаются выделенными навсегда, вызывая утечку памяти. - Использование слабой ссылки было моей первой идеей, но есть проблема в том, что в то время мы
WeakReference
объект, на который ссылается объект, на самом деле не является сильно, мягко или слабо достижимым. Таким образом, как только мы хранимWeakReference
везде, где это сильно достижимо (чтобы предотвратитьWeakReference
сама освобождается), тоWeakReference
Цель становится слабо достижимой, и ссылка автоматически очищается. Поэтому мы не можем хранить такую информацию. - При использовании фантомной ссылки возникает проблема, заключающаяся в том, что нет способа сравнить фантомную ссылку с объектом, чтобы увидеть, ссылается ли эта ссылка на этот объект. (Может быть, должно быть - в отличие от
get()
, который может воскресить объект, в этой операции нет никакой опасности, потому что у нас все равно есть ссылка на объект - но его нет в Java API. Точно так же,.equals()
наPhantomReference
объекты==
, а не значение равенства, поэтому вы не можете использовать его, чтобы определить, ссылаются ли две фантомные ссылки на одно и то же.) - С помощью
System.identityHashCode()
запись числа, соответствующего идентификатору объекта, почти работает - освобождение объекта не изменит записанное число, число не помешает освобождению объекта, а воскресение объекта оставляет значение тем же - но, к сожалению, будучиhashCode
, он подвержен коллизиям, поэтому может иметь ложные срабатывания, в которых объект, кажется, воскресает, когда это не так. - Последняя возможность состоит в том, чтобы изменить сам объект, чтобы пометить его как завершенный (и отследить местоположение его замены), что означает, что наблюдение этой метки на сильно достижимом объекте покажет его как воскрешенный объект, но для этого необходимо добавить дополнительное поле к любой объект, который может быть задействован в ссылочном цикле.
Таким образом, моя основная проблема заключается в том, что "учитывая объект, который в данный момент дорабатывается, создайте его копию безопасно, без случайного воскрешения каких-либо объектов, которые могут находиться в процессе его референсного цикла". Подход, который я пытался использовать, заключается в том, что "когда объект, который потенциально может быть задействован в цикле, завершен, отслеживайте его идентичность, чтобы впоследствии его можно было заменить его копией, если он окажется доступным для другого". завершенный объект "; но ни один из пяти упомянутых выше подходов не представляется удовлетворительным.
Есть ли другой способ отслеживать завершенные объекты, чтобы они могли быть распознаны при случайном перенаправлении? Существует ли совершенно иное решение исходной проблемы - безопасного создания копии объекта во время его завершения?
2 ответа
Чтобы избежать создания воскрешенных объектов, обычный совет (как, например, видно в этом ответе) состоит в том, чтобы создать новый экземпляр объекта, а не сохранять сам объект; Обычно это достигается путем копирования всех полей объекта в новый объект.
Это не "нормальный совет", и даже связанный ответ не утверждает, что это. Связанный ответ начинается с "Если вы обязательно должны воскресить объекты, …", что довольно ясно показывает, что это не совет о том, как " избежать создания воскресших объектов".
Подход, описанный в этом ответе, - это воскрешение объекта, и по иронии судьбы, это именно тот сценарий, который вы описываете как проблему, которую вы хотите решить, воскрешение объектов (тех, на которые ссылаются скопированные поля) с помощью финализатора другого объекта.
Это сохраняет все, кроме одной из проблем, связанных с финализаторами и воскресением объектов. Единственная проблема, которую он решает, заключается в том, что завершенный объект не будет снова завершен, что является самой маленькой проблемой.
Когда приложение покидает объект, оно не обязательно должно быть в допустимом состоянии. Объекты должны поддерживаться в правильном состоянии только тогда, когда они предназначены для повторного использования. Например, это нормально для приложения, чтобы вызвать close()
на объектах, представляющих ресурсы, когда сделано с ними. Но также разумно оставлять объект в середине операции, когда происходит ошибка. Ошибочное состояние результата может быть представлено другим объектом, а другой, теперь несовместимый объект, не используется.
Финализатору придется иметь дело со всеми этими возможными состояниями объектов и, что еще хуже, с непригодными состояниями объектов, вызванными финализаторами. Как вы узнали сами, графы объектов могут быть собраны целиком, и все их финализаторы будут выполнены в произвольном порядке или даже одновременно. Поэтому ему не нужны петли и не нужны попытки воскресения, чтобы попасть в беду. Когда объект A имеет ссылку на объект B и оба имеют финализаторы, попытка очистки A может потерпеть неудачу при необходимости B в процессе, так как B может быть уже завершен или даже в середине одновременного завершения.
Короче говоря, финализация даже не подходит для той очистки, для которой она изначально была предназначена. Вот почему finalize()
метод устарел с Java 9.
Ваша попытка повторно использовать значения полей объекта, находящегося в процессе доработки, просто подливает масла в огонь. Просто подумайте о сценарии A→B выше. Когда финализатор A копирует значения полей в другой объект, это подразумевает копирование ссылки на B, и он не требует попытки финализатора B сделать то же самое. Уже достаточно, если финализатор B делает то, для чего он предназначен, очищая связанные ресурсы, оставляя B в непригодном для использования состоянии.
Таким образом, моя основная проблема заключается в том, что "учитывая объект, который в данный момент дорабатывается, создайте его копию безопасно, без случайного воскрешения каких-либо объектов, которые могут находиться в процессе его референсного цикла".
Как уже говорилось, "объект, который в настоящее время дорабатывается" и "безопасно", - само по себе противоречие. Не нужно взаимных попыток повторного использования, чтобы сломать его. Даже если смотреть только на оригинальную узкую формулировку проблемы, у всех ваших подходов есть проблема, заключающаяся в том, что они даже не пытаются ее предотвратить. Все они только пытаются обнаружить проблему через какое-то произвольное время после свершившегося факта.
Тем не менее, нет никаких проблем в сравнении референта WeakReference
с какой-то другой сильной ссылкой, как weakReference.get() == someStrongReference
, Слабая ссылка очищается только тогда, когда референт был собран мусором, что означает, что сильная ссылка не может указать на него, поэтому ответ false
для сравнения null
ссылка с someStrongReference
тогда будет правильный ответ.
Как показывают другие ответы, попытка решить основную проблему таким способом не может быть достигнута, и для решения проблемы такого рода требуется нечто более широкое. Этот пост описывает решение, которое я использовал для своей проблемы, и как я туда попал.
Предполагая, что цель состоит в том, чтобы "отслеживать, как объект выглядел в тот момент, когда на него не ссылались", это можно безопасно выполнить только в том случае, если сам объект не имеет финализатора (в противном случае существует ряд трудных для решения проблем, как описано в вопросе, его комментариях и другом ответе). Единственная причина, по которой нам на самом деле нужен финализатор, заключается в том, что мы не можем иначе добраться до объекта после того, как он стал не связанным.
Это явно плохая идея - позволить объекту стать без ссылки, а затем восстановить его из финализатора. Однако "оживление" объекта без финализатора представляет собой гораздо меньшую проблему (поскольку это эквивалентно тому, что объект вообще никогда не освобождается - он не заканчивается "частично завершенным", как это сделал бы объект с финализатором). Это может быть достигнуто путем создания отдельного объекта с финализатором и преднамеренного создания ссылочного цикла между исходным объектом и отдельным, несущим финализатор объектом (который имеет только финализатор и ссылку t на исходный объект, и ничего больше); когда объект становится иным образом не связанным, будет запущен финализатор нового объекта, но исходный объект не будет освобожден и не окажется в каком-либо неловком состоянии, связанном с финализацией.
Финализатор, конечно, должен будет разорвать цикл (удалив себя из исходного объекта), чтобы избежать его воскрешения; если новая сильная ссылка на исходный объект создается во время финализации (отменяя его освобождение), следовательно, объекту финализации придется заменить себя новым объектом финализации (но это легко сделать, поскольку он не несет состояния, есть только одна ссылка на него, и мы знаем, где находится этот объект).
В заключение: не существует безопасного способа сохранить объект живым во время его собственной финализации, даже если вы копируете все его поля в другом месте: вместо этого вам нужно убедиться, что у объекта нет финализатора, и вместо этого поддерживать его живым, используя какой-то другой объект. финализации.