Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
[flagged] Why I hate Java (povusers.org)
21 points by adgasf on July 6, 2017 | hide | past | favorite | 50 comments


> Why does this matter? It matters for the exact same reason why memory leaks are bad in general: They consume more memory than necessary.

Thing is, this doesn't usually matter. I have never gotten an out of memory error from a leak in Java. Now compare that to all the development time I've saved by not having to deal with pointer arithmetic. I consider it a huge win. It's all about the type of apps you're making.


When I play Minecraft on my notebook, I first shut down all nonessential system services (I have a handy line in the shell history for that). This allows me to get around 45 minutes out of Minecraft (instead of 30 minutes) before it gets struck by the OOM killer.


Hmm, weird. I've never had Minecraft killed for OOM, since starting to play in the alpha days. In fact, I can watch the memory pool slowly grow, then a drop in framerate and increase in free memory when the GC is triggered every 30 seconds or so.

I don't do anything special to play; my sessions used to run into multiple hours, during my peak years of play.

I suspect that it's not fair to blame Java for whatever problem you were having, though.


I've used tons of programs that have problems and crash for various reasons. Is this an argument against the language? I don't think there'd be any left to use with this line of thinking.

Besides, there's too many variables in your anecdote. Is it a laptop from 1995? Is the OOM from a bug that could/should be fixed?


The notebook is from 2012 and has 4GB RAM. Minecraft stands out because most other programs of similar complexity (e.g. Portal 2) work fine.

I've heard a story that Minecraft's RAM consumption got a lot worse after Notch stepped down. The new developers refactored the code for OOP best practices (such as passing a 3D coordinate as an object rather than "int x, int y, int z"), which tremendously increased the number of allocations and thus GC pressure and memory usage. So it's fair IMO to blame this to a language. Having good practices lead to such consequences is terrible design.


Have you tried limiting the heap size with -Xmx <size>? If it has a bad (too large for your system) default it might grow to much before running GC.


You haven't used anything nontrivial then. Say, IntelliJ eating 2 GB of RAM, quite a lot for a glorified editor...


Just because IntelliJ is using 2GB of RAM doesn't mean they have a memory leak. As long as that 2GB profile is stable, it isn't going to bring down the system.


I didn't build that. How are you so sure it would have less leaks if it were written in a language like C++? I'd bet it would be worse.


I don't know about the other things but Even Bjarne Stroustrup calls multiple inheritance the wrong way to go. I don't think I'm better than him. So I disagree on that. The other stuff - Well all languages are imperfect. That's why we have so many of them.


> One problem with garbage collection is that it needs to see the entire memory used by the program even if the program is not using big parts of it for anything.

The first generational GC was proposed for Lisp in 1983. See Liebermann&Hewitt. The heap is divided into generations based on the lifetime of objects. Typically only the youngest generation, which is kept small, is scanned. This is based on the observation that a lot of objects are only short-lived. Thus a GC does typically does only need to look at a fraction of the memory.

Also GCs might want to use regions of similar objects. That way only those regions need to be looked at, which may free up space for an ohject that wants to be allocated currently.


Probably worth jumping up a level in the URL to: http://warp.povusers.org/grrr/ —and noting that this comes from the guy's "What grinds my gears" page, which also links to a "Why I hate C" article.


Java is a decent language for its time, with a bad default tool set and a development culture that leans very heavily towards J2EE-style verbosity (javascript is trending that way now too, w/ angular, react, etc).

The JVM, however, is a national treasure.


"Why I hate garbage collected languages"


Garbage collection sometimes offers a better efficiency than RAII - simply because it is "lazy" (vs. RAII which is always "eager" - often unnecessarily so).


To be fair, there is some material there not related to GC; like about inconsistencies between built-in types and classes and whatnot.

The rant about OOP and responsibilities is completely way off mark.

It is not a firm rule of OOP that a module has to clean up all the resources it allocates and never pass any of them off to be cleaned up elsewhere.

This is violated all over the place with layering. For instance, instead of allocating a buffer here and then having someone free it, we can create a Message class which has a buffer. We now send a Message from one module to another. Oh, the buffer is now a single responsibility: it is allocated by the Message module and freed by the Message module. But all we have done is wrap the resource in a trivial class; the design hasn't really changed. We can have a semantic leak if we start creating Messages and stuffing them into some queue which nobody dequeues, just like with the original buffers.

Layering of ownership creates the appearance that one module is managing a resource. That module's instances, though, can be created by one higher layer module and released by another.


It would be interesting to see a language that implements RAII but provides a GC as a fall-back.


Rust went through that stage at one point. It had language-level support for GC, first by having @-pointers (e.g. @i32 would have been pointer to GC-managed int) and later when sigils were removed it had short-lived Gc<T> type. iirc GC support was finally completely removed during the crunch to 1.0.

I'd point out that afaik rustc never got a proper good GC implementation (which probably weighted heavily in the decision to remove it before 1.0), so @T/Gc<T> were mostly plain old refcounted pointers with some smoke and mirrors.

See http://words.steveklabnik.com/pointers-in-rust-a-guide for example (section "Managed pointers")


TXR Lisp supports a form of RAII:

  This is the TXR Lisp interactive listener of TXR 181.
  Quit with :quit or Ctrl-D on empty line. Ctrl-X ? for cheatsheet.
  1> (defstruct raii-class nil
       (:init (me) (put-line "raii-class hello"))
       (:fini (me) (put-line "raii-class goodbye")))
  #<struct-type raii-class>
  2> (with-objects ((o (new raii-class)))
       (put-line "inside block"))
  raii-class hello
  inside block
  raii-class goodbye
  t
Constructor abort via non-local transfer:

  3> (defstruct ctor-throws nil
       (:init (me) (error "refuse to init"))
       (:fini (me) (put-line "ctor-throws goodbye")))
  #<struct-type ctor-throws>
  4> (new ctor-throws)
  ctor-throws goodbye
  ** refuse to init
  ** during evaluation at expr-3:2 of form (error "refuse to init")
:fini called by GC:

  5> (progn (new raii-class) nil)
  raii-class hello
  nil
  6> (sys:gc)
  raii-class goodbye
  t
with-objects is not just with structs but for anything with a GC finalizer, like the (1 2 3) list in this example:

  6> (with-objects ((o (new raii-class))
                    (p [finalize (list 1 2 3) prinl]))
       (put-line "inside block"))
  raii-class hello
  inside block
  (1 2 3)
  raii-class goodbye
  t
with-objects can't be used for defining function arguments, but it can refer to variables in scope; resource not defined in the block can be finalized:

  11> (let ((x (new raii-class)))
        (with-objects ((x x))
          (put-line "inside-block")))
  raii-class hello
  inside-block
  raii-class goodbye
  t
Parameter passing has reference semantics so the smart-pointer style RAII use of C++ across function interfaces is not really applicable.

We don't want to copy a struct argument when a function is called and be incrementing refcounts on things the struct's slots point to; that's just stupid.


Python uses reference counting with GC as a fall-back. RAII is basically reference counting with destructors, but it's not very useful if the object lifetimes are not easy to reason about. The optional GC (for circular references) greatly complicates things.

[1] http://arctrix.com/nas/python/gc/


You might like to have fun with the Boehm GC: http://www.hboehm.info/gc/.


or just manual memory management with gc as a catchall..


Came here to post exactly this. Also, when the hell was this written?


I ma not sure about the rest of the points made but I agree about the lack of deterministic destructors to free up external resources. C# has the same problem. It would be really good if the destructor ran when the reference count of an object goes to 0. This would make a lot of code that deals with OS resources much cleaner.


Isn't this what the AutoCloseable interface (combined with try-with-resources statements) addresses for everything except memory? It wasn't added until Java 1.7, though, so maybe the article predates that feature.

https://docs.oracle.com/javase/tutorial/essential/exceptions...

...basically it's the same as .NET dispose pattern for achieving deterministic cleanup. Most IDEs and code analysis tools will throw a fit if you don't use an AutoCloseable/IDisposable class properly in a try-with-resources/using statement.


The dispose pattern puts the work on the user of the resource. In addition I have seen cases of classes that didn't need to be disposable but later changes were made that required them to be disposable. Now you have code using these classes that don't dispose them.

I much prefer destructors like in C++ (or VB or even PHP) that run when the reference count goes to 0.


C# has a using block that makes resource deallocation explicit syntax, which I far prefer to implicit destructor behaviour.

That said, we need IDEs that sound the klaxons if a Closeable is not placed in a using statement.


I find the using syntax pretty ugly especially once you have several objects. Some keyword like "autodispose" may help. Managed C++ has this. Objects that are allocated with the stack syntax get disposed at the end of the scope.


Again, that creates implicit behaviours that can be extremely surprising.

You can deal with multiple Closeables by enhancing syntax.

But tying deallocation to disposal creates action at a distance that I'm really not a fan of, personally.

Resource allocation and deallocation can take time, can block, and can fail. Those semantics make them a terrible fit for constructors and destructors.


As a counterpoint to this:

- Making it explict has the two distinct problems that a) the code for it must be written each time (I regularly encounter code that ignores Disposables) and b) is closed to change (adding a dispose semantics to a class essentially creates bugs throughout your codebase). Those two, I deem far worse than a destructor that is blocking.

- On the C++ side: If you encapsulate the resource properly, you're not really binding the two together (at least on one level of abstraction), since you're basically just managing a handle. Note that it is also really easy to split both up (by introducing some NULL-handle) and only dispose the resource in the destructor if it hasn't been disposed yet (of course, that is more bug-prone, but so is IDisposable).

- With move semantics (or std::swap before C++11) this also allows you to dispatch "cleanups" to other contexts, if necessary, which gives you exactly the same possibilities where necessary (of course, this also being explicit in those cases).

- Failed resource allocations in constructors should not be a problem (either use exceptions or NULL-handles).

- Failed resource deallocations are a problem in any implementation I know of and likely not "conquered" easily in the near future.

So, to sum it up, I think C++ gives you the better tooling because you can design the appropriate API yourself. C#'s implementation is very set in it's way and has a couple of problems that would be nice to avoid (e.g. the "you can't free resources in a finalizer" thing).

I'd prefer an API that does the right thing by default (albeit slow) and lets me optimize where needed instead of one that has little benefit (for most scenarios) but makes the code more error-prone. That means essentially: If you implement IDisposable in C++ and dispose in the destructor where necessary, you get the best of both approaches.

The remaining arguments that then can be made against C++ are either bad code, abstraction problems and/or cumbersome libraries.


There are plenty of cases where Disposables are used in non-simple scopes enabled by using statements, and where it is better to call dispose explicitly rather than using an explicit using statement.


No doubt, though I would argue those are the exception rather than the rule... which is why it should be an ignorable warning, preferably requiring a code annotation to turn it off. But it absolutely be something a modern IDE should flag as a potential error (IntelliJ, for example, has whole classes of these types of inspections for Java, Kotlin, etc).


If the false positive rate was even just 1 or 2%, it might be too noisy to warn about. Perhaps an annotation to opt out would be more apt.


Is there a date on this? This feels like a (1997) article at the latest.


Maybe not 1997, but surely not "recent" (let alone "news") at least the Wayback Machine has it archived since Feb 8,2006:

https://web.archive.org/web/*/http://warp.povusers.org/grrr/...


The index page has dates for other articles, but not this one. http://warp.povusers.org/grrr/index.html

Archive.org's first snapshot of this page is on 2006-02-08.

The HTTP response headers contain the entry: "Last-Modified: Sun, 14 Oct 2012 08:58:12 GMT"



It predates generics at least.


And according to Wikipedia generics were added to Java in 2004, so that gives some baseline. JSR14 was for introducing generics, and it was initiated in 1999. That leaves 5 year window during which the article was most likely written.


> And according to Wikipedia generics were added to Java in 2004

I was in college around that time, using Java as a language for coursework. The professors jumped on generics right away, so between two quarters, the style that we wrote our Java in shifted drastically. I was nice to rip the band-aid off that fast.


Generics don't solve the TypeDef problem. It's the old, I am probably never going to need anything over 2 billion, but I am not 100% sure.


Generic method syntax is both much more limited and more complex... Foo<? super Bar extends Madness> with various insane limitations. Makes C++ SFINAE seem elegant...


I was talking about the following:

"Now, in Java, if you use a data container provided by the language, you are forced to upcast all the time."

Which is solved by generics.


Last I checked, if you store an int in a generic it get's converted to an object which takes more memory to store.

So, the syntax is similar, but the actual behavior is not because int's can't be null, but Integers can for example. This costs 8 ish bytes of memory.


That is true. It looks like there is some work being done to potentially relax this constraint, http://openjdk.java.net/jeps/218


Risking discussing religiousity:

Java as a language has some rough-edges: multiple inheritance and verbosity. Personally, I'd use Kotlin if there were a requirement to run on the JVM.

In terms of the JVM:

Erlang VM is under-appreciated: each "process" (not an OS process and lighter than an OS thread) has its own heap so there's no global GC pauses, share-nothing and let-it crash doesn't take out Erlang VM.

For anyone stuck on JVM, look at Azul's free Zing for shorter/less GC pauses and a generally faster JVM.


What about the point of gc solving the issue of your app crashing or security issues everywhere because a malformed input ends up crashibg/executing code? In the debian security mail list almost every week there is a path for that situation so looks like it's pretty common.


GC does not solve any kind of a crash. (In fact might cause some extra NPE or out of memory crashes.) Safe pointers do. That is a separate feature of Java from GC.


A precise garbage collector requires safe pointers. A precise GC allows the copy/move/compact strategy, whereas a conservative GC only allows mark-and-sweep.


Should add 2006 to the title...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: