Это очень простой вопрос. Я сформулирую это, используя C ++ и Java, но это действительно не зависит от языка.
Рассмотрим известную проблему в C ++:
struct Obj
{
boost::shared_ptr<Obj> m_field;
};
{
boost::shared_ptr<Obj> obj1(new Obj);
boost::shared_ptr<Obj> obj2(new Obj);
obj1->m_field = obj2;
obj2->m_field = obj1;
}
Это утечка памяти, и все это знают :). Решение также хорошо известно: нужно использовать слабые указатели, чтобы сломать «блокировку рефконта». Также известно, что эта проблема не может быть решена автоматически в принципе. Ответственность за ее решение лежит исключительно на программисте.
Но есть и положительный момент: программист полностью контролирует значения пересчета. Я могу приостановить свою программу в отладчике и проверить refcount для obj1, obj2 и понять, что есть проблема. Я также могу установить точку останова в деструкторе объекта и наблюдать момент разрушения (или узнать, что объект не был уничтожен).
Мой вопрос о Java, C #, ActionScript и других языках «Сборка мусора». Я мог бы что-то упустить, но, по моему мнению, они
Я часто слышу, что эти языки просто не позволяют программисту утекать память, и поэтому они великолепны. Насколько я понимаю, они просто скрывают проблемы с управлением памятью и затрудняют их решение.
Наконец, сами вопросы:
Джава:
public class Obj
{
public Obj m_field;
}
{
Obj obj1 = new Obj();
Obj obj2 = new Obj();
obj1.m_field = obj2;
obj2.m_field = obj1;
}
Системы управляемой памяти построены на предположении, что вы не хотите отслеживать проблему утечки памяти. Вместо того, чтобы облегчить их решение, постарайтесь убедиться, что они никогда не произойдут.
У Java действительно есть термин «утечка памяти», что означает любой рост памяти, который может повлиять на ваше приложение, но никогда не бывает точки, что управляемая память не может очистить всю память.
JVM не использует подсчет ссылок по ряду причин
Хотя JLS не запрещает использование счетчиков ссылок, он не используется ни в одной JVM AFAIK.
Вместо этого Java отслеживает ряд корневых контекстов (например, каждый стек потоков) и может отслеживать, какие объекты необходимо сохранить, а какие можно отбросить на основании того, достижимы ли эти объекты. Он также предоставляет возможность для слабых ссылок (которые сохраняются до тех пор, пока объекты не очищены) и мягких ссылок (которые обычно не очищаются, но могут быть на усмотрение сборщиков мусора)
AFAIK, Java GC работает, начиная с набора четко определенных начальных ссылок и вычисляя транзитивное замыкание объектов, которое может быть достигнуто из этих ссылок. Все, что недоступно, «просочилось» и может быть скопировано.
У Java есть уникальная стратегия управления памятью. Все (кроме нескольких конкретных вещей) размещается в куче и не освобождается до тех пор, пока GC не начнет работать.
Например:
public class Obj {
public Object example;
public Obj m_field;
}
public static void main(String[] args) {
int lastPrime = 2;
while (true) {
Obj obj1 = new Obj();
Obj obj2 = new Obj();
obj1.example = new Object();
obj1.m_field = obj2;
obj2.m_field = obj1;
int prime = lastPrime++;
while (!isPrime(prime)) {
prime++;
}
lastPrime = prime;
System.out.println("Found a prime: " + prime);
}
}
C обрабатывает эту ситуацию, требуя, чтобы вы вручную освободили память как ‘obj’, а C ++ считает ссылки на ‘obj’ и автоматически уничтожает их, когда они выходят из области видимости.
Java делает не Освободите эту память, по крайней мере, сначала.
Среда выполнения Java ждет некоторое время, пока не почувствует, что используется много памяти. После этого начинается сборщик мусора.
Допустим, сборщик мусора Java решает очистить после 10 000-й итерации внешнего цикла. К этому времени было создано 10 000 объектов (которые уже были бы освобождены в C / C ++).
Хотя существует 10 000 итераций внешнего цикла, код может ссылаться только на вновь созданные obj1 и obj2.
Это «корни» GC, которые Java использует для поиска всех объектов, на которые можно ссылаться. Затем сборщик мусора рекурсивно перебирает дерево объектов, отмечая «пример» как активный в зависимости от корней сборщика мусора.
Все эти другие объекты затем уничтожаются сборщиком мусора.
Это приводит к снижению производительности, но этот процесс был сильно оптимизирован и не имеет значения для большинства приложений.
В отличие от C ++, вам не нужно беспокоиться о ссылочных циклах совсем, так как будут жить только объекты, достижимые от корней GC.
С Java-приложениями вы делать надо беспокоиться о памяти (подумайте, списки держатся за объекты из всех итераций), но это не так важно, как в других языках.
Что касается отладки: идея Java по отладке больших значений памяти использует специальный «анализатор памяти», чтобы выяснить, какие объекты все еще находятся в куче, не беспокоиться о том, что ссылается на что.
Критическая разница в том, что в Java и т. Д. вы вообще не вовлечены в проблему утилизации. Это может показаться довольно пугающим положением, но это удивительно расширяет возможности. Все решения, которые вы должны были принять в отношении того, кто несет ответственность за удаление созданного объекта, пропали.
Это действительно имеет смысл. Система знает гораздо больше о том, что достижимо, а что нет, чем вы. Он также может принимать более гибкие и разумные решения о том, когда разрушать конструкции и т. Д.
По сути — в этой среде вы можете манипулировать объектами гораздо более сложным способом, не беспокоясь о том, чтобы его уронить. Единственное, о чем вам сейчас нужно беспокоиться, это если вы случайно приклеите его к потолку.
Как бывший программист C, переехав на Java, я чувствую вашу боль.
Повторно — ваш последний вопрос — это не утечка памяти. Когда GC начинает все отбрасывается кроме того, что достижимо. В этом случае, если вы выпустили obj1
а также obj2
ни один не достижим, поэтому они оба будут отброшены.
Сборка мусора не просто подсчет ссылок.
Пример циклической ссылки, который вы демонстрируете, не будет встречаться в управляемом языке сборки мусора, потому что сборщик мусора захочет отследить ссылки на выделение вплоть до чего-либо в стеке. Если где-то нет ссылки на стек, это мусор. Системы подсчета ссылок, такие как shared_ptr
не настолько умны, и возможно (как вы демонстрируете) иметь где-то в куче два объекта, которые мешают друг другу быть удаленными.
Языки, собираемые мусором, не позволяют проверять refcounter, потому что их нет ни у кого. Сборка мусора — это совсем не то, что пересчет управления памятью. Реальная разница в детерминизме.
{
std::fstream file( "example.txt" );
// do something with file
}
// ... later on
{
std::fstream file( "example.txt" );
// do something else with file
}
в C ++ у вас есть гарантия того, что example.txt был закрыт после закрытия первого блока или при возникновении исключения. Сравнение с Java
{
try
{
FileInputStream file = new FileInputStream( "example.txt" );
// do something with file
}
finally
{
if( file != null )
file.close();
}
}
// ..later on
{
try
{
FileInputStream file = new FileInputStream( "example.txt" );
// do something with file
}
finally
{
if( file != null )
file.close();
}
}
Как видите, вы обменяли управление памятью на управление всеми остальными ресурсами. Это реальная разница, пересчитанные объекты все еще сохраняют детерминистское разрушение. В языках сборки мусора вы должны вручную освобождать ресурсы и проверять наличие исключений. Кто-то может утверждать, что явное управление памятью может быть утомительным и подверженным ошибкам, но в современном C ++ оно смягчается умными указателями и стандартными контейнерами. У вас все еще есть некоторые обязанности (например, циклические ссылки), но подумайте, сколько блоки catch / finally вы можете избежать, используя детерминированное уничтожение, и сколько набираете Java / C # / etc. программист должен сделать вместо этого (поскольку они должны вручную закрывать / освобождать ресурсы, кроме памяти). И я знаю, что в C # используется синтаксис (и что-то похожее в новейшей Java), но он охватывает только время жизни блока, а не более общую проблему совместного владения.