I looked into this a bit, and it seems to check out. I don't really care for using the behavior of code depending on System/gc to claim breakages, since "do nothing" is a valid implementation of that method. So instead, I AOT-compiled this code once in clojure 1.4 and once in clojure 1.5, and inspected the generated bytecode, to see if locals were really being cleared differently.
My summary (details to follow) is that for whatever reason clojure 1.5 structures the body of the try differently, creating an extra lambda that doesn't exist in 1.4, and holds onto the s local longer. It's as if you had written this:
(defn leaker [n]
(with-open [_ (reify java.io.Closeable (close [this]))]
(let [s (map #(FinalizeReporter. %) (take 10 (iterate inc 0)))]
(when (> n -1)
(try
((fn body []
(->> s
(map #(do (Thread/sleep 100) (System/gc) (println @%)))
dorun))))))))
And the 'body function saves a reference to s. I'm not qualified to say whether this is a problem with locals-clearing (shouldn't body release its local before calling map?) or something else (why is this lambda generated at all?), but it does look like a serious, but rare, regression for 1.5.
As to the details, here are the relevant snippets I found in the disassembler, annotated for explanation:
// clojure 1.4
115: new #137; // core$leaker$fn__25, the lambda #(do (Thread/sleep 100) (System/gc) (println @%))
118: dup
119: invokespecial #138; // call its no-arg constructor
122: aload_3 // this is the local s
123: aconst_null
124: astore_3 // clear it
125: invokeinterface #129, 3; // call constructed leaker$fn with s
No problem there, of course.
// clojure 1.5
97: new #140; // core$leaker$fn__27, an "implicit?" lambda containing the whole 'try body, (fn [] (dorun (map #(do (Thread/sleep 100) (System/gc) (println @%)) s)))
100: dup
101: aload_3 // we pass s to that lambda's constructor, since it needs our s
102: aconst_null
103: astore_3 // still clearing our local, though
104: invokespecial #143; // aforesaid one-arg constructor, called with s
107: checkcast #126; //class clojure/lang/IFn
110: invokeinterface #145, 1; // call it with no arguments
If the body of leaker$fn__27 cleared s, as ^:once functions do when called, this would all be fine, but the relevant part is:
18: new #45; // core$leaker$fn__27$fn__28, the lambda #(do (Thread/sleep 100) (System/gc) (println @%)) as in 1.4
21: dup
22: invokespecial #46; // a no-arg constructor
25: aload_0
26: getfield #36; // field 's' from the "body" lambda-object
29: invokeinterface #49, 3; // (map f s)
34: invokeinterface #52, 2; // (dorun coll)
Note that there is no `aload_null; setfield #36` pair to match the clearing that happens for locals.
I wonder if this problem leaked in as part of the fix for handling of recur inside of try/catch/finally clauses, but the only recent commit in that area of the compiler is
9b80a552, which seems harmless and designed to fix exactly this problem. So I definitely don't have a proposed fix: I'm just reporting this here
to confirm that this looks like a real problem, and so that hopefully someone whose time is more valuable than mine won't have to do as much mucking about with assembly internals to find the cause.
~Alan