jruby / jruby

JRuby, an implementation of Ruby on the JVM
https://www.jruby.org
Other
3.78k stars 921 forks source link

Free up memory memory used by JRuby during teardown #8343

Open laumacirule opened 2 weeks ago

laumacirule commented 2 weeks ago

Environment Information

Actual Behavior To allow garbage collection as soon as possible, we call the following methods to release memory once a JRuby plugin is shut down, but the base JVM process keeps running.

  def release_jruby_runtime_memory
    logger.info "Release JRuby runtime memory..."
    runtime = JRuby.runtime
    runtime.release_class_loader

    # Clear loaded features to allow sooner garbage collection of it.
    # JRuby runtime will not be garbage collected right away due to thread local soft references to JRuby ThreadContext objects.
    clear_loaded_features(runtime)

    # Clear boundMethods
    runtime.getBoundMethods.clear

    clear_concurrent_hash_map_fields(runtime)
    release_memory_of_large_objects(runtime)
  rescue => e
    logger.error "release_jruby_runtime_memory failed with #{e.class.name}: #{e.message}"
  end

  def clear_loaded_features(runtime)
    load_service = runtime.getLoadService
    load_service.getLoadedFeatures.clear

    f = load_service.java_class.declared_field("librarySearcher")
    f.accessible = true
    library_searcher = f.value(load_service)

    loaded_features_index_method = library_searcher.java_class.declared_method('getLoadedFeaturesIndex')
    loaded_features_index_method.accessible = true
    loaded_features_index = loaded_features_index_method.invoke(library_searcher)
    loaded_features_index.clear

    f = library_searcher.java_class.declared_field("loadedFeaturesIndex")
    f.accessible = true
    f.set_value(library_searcher, loaded_features_index.class.new)
  rescue => e
    logger.error "clear_loaded_features failed with #{e.class.name}: #{e.message}"
  end

  def clear_concurrent_hash_map_fields(runtime)
    %w(allModules constantNameInvalidators).each do |field_name|
      f = runtime.java_class.declared_field(field_name)
      f.accessible = true
      (value = f.value(runtime)).clear
      f.set_value(runtime, value.class.new)
    end
  rescue => e
    logger.error "clear_concurrent_hash_map_fields #{field_name} failed with #{e.class.name}: #{e.message}"
  end

  def release_memory_of_large_objects(runtime)
    {
      "symbolTable" => runtime.getSymbolTable.class.new(JRuby.runtime),
      "javaSupport" => runtime.loadJavaSupport
    }.each do |field_name, new_value|
      f = JRuby.runtime.java_class.declared_field(field_name)
      f.accessible = true
      f.set_value(runtime, new_value)
    end
  rescue => e
    logger.error "release_memory_of_large_objects #{field_name} failed with #{e.class.name}: #{e.message}"
  end

Expected Behavior

The above teardown actions could be added to the org.jruby.Ruby.teardown logic to fully free up the memory that JRuby used.

headius commented 2 weeks ago

Thanks for this! There's definitely some cleanup here we should probably add to JRuby's normal runtime teardown.

But first, I need to know: what is the lifecycle of one of your JRuby-based plugins related to the normal JRuby lifecycle? Specifically, do you do this cleanup followed by a call to org.jruby.Ruby.tearDown or the ScriptingContainer equivalent?

I think we can get most of this into JRuby's teardown, especially if it helps GC the rest of the runtime instance sooner!

headius commented 2 weeks ago

Marked for 9.4.9.0 but it may be too much to add in a maintenance release. Additional cleanup could easily run into new errors if there are environments already doing the same cleanup or depending (perhaps incorrectly) on some of these resources not being cleaned up.