Skip to content

java.lang.OutOfMemoryError error occurs when StringScanner keeps storing << and scan results in an array. (JRuby only, not CRuby) #83

Closed

Description

@naitoh

java.lang.OutOfMemoryError error occurs when StringScanner keeps storing << and scan results in an array.
(JRuby only, not CRuby)

No Problem Code

  • string_scanner_scan_string.rb
require 'strscan'

s = StringScanner.new('')

N = 100000

a = []
N.times do
  s.string = s.rest + "1234567890"
  a << s.scan(/.*/)
end
$ ruby string_scanner_scan_string.rb 
$

Problem Code

  • string_scanner_scan.rb
require 'strscan'

s = StringScanner.new('')

N = 100000

a = []
N.times do
  s << "1234567890"
  a << s.scan(/.*/)
end
$ ruby string_scanner_scan.rb 
Error: Your application used more memory than the automatic cap of 3758MB.
Specify -J-Xmx####M to increase it (#### = cap size in MB).
Specify -w for full java.lang.OutOfMemoryError: Java heap space stack trace

Estimated memory requirements

100,000 x 10 [Byte] = 1,000,000 [Byte] = 1 [MB]

The above is required for Array and StringScanner.

1 [MB] x 2 = 2 [MB]

3758MB is not needed, so a memory leak is likely to occur.

Environment

$ ruby -v
jruby 9.4.5.0 (3.1.4) 2023-11-02 1abae2700f Java HotSpot(TM) 64-Bit Server VM 25.121-b13 on 1.8.0_121-b13 +jit [x86_64-darwin]
$ gem list strscan

*** LOCAL GEMS ***

strscan (3.0.8 java, default: 3.0.7 java)

Activity

  1. headius commented on Jan 20, 2024

    @headius
    Contributor

    I ran this with a JVM flag to dump the heap and have some information:

    $ jruby -J-XX:+HeapDumpOnOutOfMemoryError memory_bug.rb 
    java.lang.OutOfMemoryError: Java heap space
    Dumping heap to java_pid28134.hprof ...
    Heap dump file created [4081053622 bytes in 6.000 secs]
    
    Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
    

    Analyzing the heap, I see all the String objects in the Array, but each one is holding a huge buffer from the original string.

    I believe the problem happens like this:

    • StringScanner gets appended "1234567890" each loop
    • StringScanner#scan returns a shared string based on that buffer
    • Next loop modifies the string, so the buffer is copied to a new buffer
    • Old buffer is retained by the String returned from scan, so memory usage grows very quickly.

    This is a case where copy-on-write is failing us; as the string grows, so do the retained buffers, and we end up with exponentially more byte[] data in memory than you would expect.

    I'm not sure why this doesn't affect CRuby.

  2. headius commented on Jan 20, 2024

    @headius
    Contributor

    I'm not sure why this doesn't affect CRuby.

    Aha. The C code does not make a shared string for #scan. I will make a PR.

  3. added a commit that references this issue on Jan 20, 2024
    101e474
  4. added a commit that references this issue on Jan 20, 2024
    83e6b46
  5. headius commented on Jan 20, 2024

    @headius
    Contributor

    This is fixed.

    @hsbt Could we get a release with this please?

  6. naitoh commented on Jan 20, 2024

    @naitoh
    ContributorAuthor

    @headius

    Aha. The C code does not make a shared string for #scan. I will make a PR.

    There seems to be a problem with StringScanner#check, is this one ok?

    • string_scanner_check.rb
    require 'strscan'
    
    s = StringScanner.new('')
    
    N = 100000
    
    a = []
    N.times do
      s << "1234567890"
      a << s.check(/.*/)
    end
    $ ruby string_scanner_check.rb 
    Error: Your application used more memory than the automatic cap of 3743MB.
    Specify -J-Xmx####M to increase it (#### = cap size in MB).
    Specify -w for full java.lang.OutOfMemoryError: Java heap space stack trace

    I checked this before the merge of #84.

  7. headius commented on Jan 20, 2024

    @headius
    Contributor

    @naitoh Uh-oh! That one still seems to be broken. I thought I caught everywhere this should be a copied string but I must have missed check. I'll do another PR.

  8. kou commented on Jan 20, 2024

    @kou
    Member

    I can release a new version when we're ready.

  9. headius commented on Jan 20, 2024

    @headius
    Contributor

    @naitoh False alarm! This has the same effect under CRuby, because check is returning a longer and longer string each time. Both implementations will consume many GB of memory, but JRuby hits the JVM limit and raises an error.

  10. naitoh commented on Jan 20, 2024

    @naitoh
    ContributorAuthor

    @headius

    @naitoh False alarm! This has the same effect under CRuby, because check is returning a longer and longer string each time. Both implementations will consume many GB of memory, but JRuby hits the JVM limit and raises an error.

    Sorry.
    I have confirmed that CRuby and JRuby work similarly in the case of StringScanner#check.

    This seems to be a reasonable behavior in this case.

  11. naitoh commented on Jan 21, 2024

    @naitoh
    ContributorAuthor

    @kou
    Is it possible to make this release?

  12. kou commented on Jan 21, 2024

    @kou
    Member

    Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions