Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 3 years ago.
Improve this question
I have some code which is like:
String country = null;
country = getEuropeanCountry(); //Germany
//after few lines of code
country = getAsianCountry(); //Japan
//after few more lines
country = getNorthAmericanCountry(); //Canada
/*and the code follows by assigning a different country value to the same variable "country"*/
I have this kind of usage in most of my code.
For some reason, my application throws "Error java.lang.OutOfMemoryError: GC overhead limit exceeded".
So I tried with VM argument: -XX:-UseGCOverheadLimit
Then my app ran successfully but I noticed that it is consuming more memory (I had to set -Xmx to 5g or 6g; otherwise I get: out of memory error).
I checked my app and there are no memory leaks. But most of my code has the similar code that I posted above.
Can anyone tell me if it is beneficial for memory management if I refactor the above code to:
String europeanCountry = getEuropeanCountry(); //Germany
//after few lines of code
String asianCountry = getAsianCountry(); //Japan
//after few more lines
String northAmericanCountry = getNorthAmericanCountry(); //Canada
/*and the code follows by assigning a different country value to a different String variable*/
I can't use collections. I mean, in general, which way is better to use heap space and garbage collector efficiently?
For the question " I mean, in general, which way is better to use heap space and garbage collector efficiently?"
Lets look at the String implementation e.g. jdk8 https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/lang/String.java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
So it is a final character array - it cannot be reasigned or changed. So it is generated on the heap in your method and is never changed - only a reference(a name) is generated.
To make no mistake lets also look at the constructor of a String (doing smth. like eg. "newString = new String(otherString)":
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
Also in that case no additional space on the heap is allocated - it stays the same single final char array on the heap.
So you can assign a new String to a reference.(Give it an additional name). But it is allways the same unique String generated in your method and no new space on the heap is allocated.
So comparing the two methods in a first narrowing:
String europeanCountry = getEuropeanCountry();
String asianCountry = getAsianCountry();
and
String country = null;
country = getEuropeanCountry();
country = getAsianCountry();
Both will formaly create the same amount of Strings on the heap as the String is allways generated in the same methods. The variable are only references to that.
The only difference is that reusing in the second case allows formaly the String to be earlier garbage collected(In the moment the reference to it is removed by reusing the variable).
So with the second approach(reusing) you may generate a smaller memory footprint for a neclectable time.
I said in a first narrowing as this is only true if there is no other reference to the String and without optimization - so if no other reference exists and no optimization would take place the above would be the case.
However in your above code the variables don't exit scope and are never used. The compiler will detect this and no variable will be assigned at all. Depending on what the methods do they may be inlined and also not called. So what the methods you call look like makes a difference. Depending how complex they are the space on the heap is allocated or not.
Also the other way round: if you use the variable and runtime detects that you will likely call the method again for the same value, the value will be kept on the heap and not freed even if there is formal no reference and it could formally be garbage collected - so the assigment again makes no difference but the call of the method.
Also the obvioue: if the methods don't only generate the Strings but pull them from somewhere(a container) or store them somewhere that other reference is the one for which space on the heap is kept (and is) allocated and your assignment makes no difference at all regarding heap: it is the same final char array on the heap.
With that in mind the problem you are facing is most probably not that assignment of Strings but the design of your code. It must be a by far more complex scenario in which references are kept longer.
So far for your question.
For your problem i would look out:
for containers
where variables are generated
for frequent use . That is calling the methods very frequently for a lot of different values as in such case they are kept in memory for the next assumed call to happen.
for code where it is not easy to follow the flow of the data. The compiler optimizes by analyzing the flow. If you can't follow, it is more likely that the compiler can't neither than in other parts.
Assuming lines of code shown are from single method(let me know if that's not the case), there are at least 3 issues I can point outwith the code:
it seems method size is too large. Prefer writing methods that are as concise as possible and do just "one thing" and do it well.
too much state change. In the 1st example where you set variable 'country' to 3 different method return values.
consider using polymorphism, rather than repeating code in an if-else fashion for fetching the country
finally, it's not clear how country values are used in methods.
Related
I've got a recursive method which has local String variables:
private void recursiveUpdate(int id){
String selectQuery="Select ...";
String updateQuery="Update or rollback ..."
...
for(int childID: children)
recursiveUpdate(childID);
}
Is there any reason to externalize local String variables like this:
private static final String selectQuery="Select ...";
private static final String updateQuery="Update or rollback ..."
private void recursiveUpdate(int id){
...
for(int childID: children)
recursiveUpdate(childID);
}
From a technical point of view the difference between the two should be negligible since in either case you'd always use the same string instances. If you are parsing those query in every call you might consider externalizing that as well (e.g. using prepared statements).
From a development point of view, I'd probably externalize the queries to separate them from the call logic.
In the former case, you are relying on the compiler to recognize that those strings are unchanging across all calls so it doesn't need to give a fresh copy of each variable to each invocation of recursiveUpdate, whereas in the latter case, there is no question about it.
Yes. You'd want to externalize the variables. If left as a local variable, amd depending on the size of the call stack, you could quickly accumulate many string objects and lower efficiency. Even worse if making edits to the string inside the recursive method. Also, you will not be making changes to the strings as it appears to me, so if used as a 'reference' it would be better to externalize it.
Typically you should be concerned with the size of your stack memory when making recursive calls. This tells the cpu where to jump when the method completes. It contains your method parameters and returning location.
Object instantiated within the body of the method are saved in the heap. In this case, I think the compiler will figure out that these are constants and save them to static memory. The heap is much larger and is more likely to survive the recursion when objects are instantiated so I wouldn't worry about it.. By moving object out, you'll save a little space in your heap.
IMO, it's best to move the variable out, if the values are always the same (no-dynamic). This way, if they ever change, you can find them easily.
We all know that String is immutable in Java, but check the following code:
String s1 = "Hello World";
String s2 = "Hello World";
String s3 = s1.substring(6);
System.out.println(s1); // Hello World
System.out.println(s2); // Hello World
System.out.println(s3); // World
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[])field.get(s1);
value[6] = 'J';
value[7] = 'a';
value[8] = 'v';
value[9] = 'a';
value[10] = '!';
System.out.println(s1); // Hello Java!
System.out.println(s2); // Hello Java!
System.out.println(s3); // World
Why does this program operate like this? And why is the value of s1 and s2 changed, but not s3?
String is immutable* but this only means you cannot change it using its public API.
What you are doing here is circumventing the normal API, using reflection. The same way, you can change the values of enums, change the lookup table used in Integer autoboxing etc.
Now, the reason s1 and s2 change value, is that they both refer to the same interned string. The compiler does this (as mentioned by other answers).
The reason s3 does not was actually a bit surprising to me, as I thought it would share the value array (it did in earlier version of Java, before Java 7u6). However, looking at the source code of String, we can see that the value character array for a substring is actually copied (using Arrays.copyOfRange(..)). This is why it goes unchanged.
You can install a SecurityManager, to avoid malicious code to do such things. But keep in mind that some libraries depend on using these kind of reflection tricks (typically ORM tools, AOP libraries etc).
*) I initially wrote that Strings aren't really immutable, just "effective immutable". This might be misleading in the current implementation of String, where the value array is indeed marked private final. It's still worth noting, though, that there is no way to declare an array in Java as immutable, so care must be taken not to expose it outside its class, even with the proper access modifiers.
As this topic seems overwhelmingly popular, here's some suggested further reading: Heinz Kabutz's Reflection Madness talk from JavaZone 2009, which covers a lot of the issues in the OP, along with other reflection... well... madness.
It covers why this is sometimes useful. And why, most of the time, you should avoid it. :-)
In Java, if two string primitive variables are initialized to the same literal, it assigns the same reference to both variables:
String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true
That is the reason the comparison returns true. The third string is created using substring() which makes a new string instead of pointing to the same.
When you access a string using reflection, you get the actual pointer:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
So change to this will change the string holding a pointer to it, but as s3 is created with a new string due to substring() it would not change.
You are using reflection to circumvent the immutability of String - it's a form of "attack".
There are lots of examples you can create like this (eg you can even instantiate a Void object too), but it doesn't mean that String is not "immutable".
There are use cases where this type of code may be used to your advantage and be "good coding", such as clearing passwords from memory at the earliest possible moment (before GC).
Depending on the security manager, you may not be able to execute your code.
You are using reflection to access the "implementation details" of string object. Immutability is the feature of the public interface of an object.
Visibility modifiers and final (i.e. immutability) are not a measurement against malicious code in Java; they are merely tools to protect against mistakes and to make the code more maintainable (one of the big selling points of the system). That is why you can access internal implementation details like the backing char array for Strings via reflection.
The second effect you see is that all Strings change while it looks like you only change s1. It is a certain property of Java String literals that they are automatically interned, i.e. cached. Two String literals with the same value will actually be the same object. When you create a String with new it will not be interned automatically and you will not see this effect.
#substring until recently (Java 7u6) worked in a similar way, which would have explained the behaviour in the original version of your question. It didn't create a new backing char array but reused the one from the original String; it just created a new String object that used an offset and a length to present only a part of that array. This generally worked as Strings are immutable - unless you circumvent that. This property of #substring also meant that the whole original String couldn't be garbage collected when a shorter substring created from it still existed.
As of current Java and your current version of the question there is no strange behaviour of #substring.
String immutability is from the interface perspective. You are using reflection to bypass the interface and directly modify the internals of the String instances.
s1 and s2 are both changed because they are both assigned to the same "intern" String instance. You can find out a bit more about that part from this article about string equality and interning. You might be surprised to find out that in your sample code, s1 == s2 returns true!
Which version of Java are you using? From Java 1.7.0_06, Oracle has changed the internal representation of String, especially the substring.
Quoting from Oracle Tunes Java's Internal String Representation:
In the new paradigm, the String offset and count fields have been removed, so substrings no longer share the underlying char [] value.
With this change, it may happen without reflection (???).
There are really two questions here:
Are strings really immutable?
Why is s3 not changed?
To point 1: Except for ROM there is no immutable memory in your computer. Nowadays even ROM is sometimes writable. There is always some code somewhere (whether it's the kernel or native code sidestepping your managed environment) that can write to your memory address. So, in "reality", no they are not absolutely immutable.
To point 2: This is because substring is probably allocating a new string instance, which is likely copying the array. It is possible to implement substring in such a way that it won't do a copy, but that doesn't mean it does. There are tradeoffs involved.
For example, should holding a reference to reallyLargeString.substring(reallyLargeString.length - 2) cause a large amount of memory to be held alive, or only a few bytes?
That depends on how substring is implemented. A deep copy will keep less memory alive, but it will run slightly slower. A shallow copy will keep more memory alive, but it will be faster. Using a deep copy can also reduce heap fragmentation, as the string object and its buffer can be allocated in one block, as opposed to 2 separate heap allocations.
In any case, it looks like your JVM chose to use deep copies for substring calls.
To add to the #haraldK's answer - this is a security hack which could lead to a serious impact in the app.
First thing is a modification to a constant string stored in a String Pool. When string is declared as a String s = "Hello World";, it's being places into a special object pool for further potential reusing. The issue is that compiler will place a reference to the modified version at compile time and once the user modifies the string stored in this pool at runtime, all references in code will point to the modified version. This would result into a following bug:
System.out.println("Hello World");
Will print:
Hello Java!
There was another issue I experienced when I was implementing a heavy computation over such risky strings. There was a bug which happened in like 1 out of 1000000 times during the computation which made the result undeterministic. I was able to find the problem by switching off the JIT - I was always getting the same result with JIT turned off. My guess is that the reason was this String security hack which broke some of the JIT optimization contracts.
According to the concept of pooling, all the String variables containing the same value will point to the same memory address. Therefore s1 and s2, both containing the same value of “Hello World”, will point towards the same memory location (say M1).
On the other hand, s3 contains “World”, hence it will point to a different memory allocation (say M2).
So now what's happening is that the value of S1 is being changed (by using the char [ ] value). So the value at the memory location M1 pointed both by s1 and s2 has been changed.
Hence as a result, memory location M1 has been modified which causes change in the value of s1 and s2.
But the value of location M2 remains unaltered, hence s3 contains the same original value.
The reason s3 does not actually change is because in Java when you do a substring the value character array for a substring is internally copied (using Arrays.copyOfRange()).
s1 and s2 are the same because in Java they both refer to the same interned string. It's by design in Java.
String is immutable, but through reflection you're allowed to change the String class. You've just redefined the String class as mutable in real-time. You could redefine methods to be public or private or static if you wanted.
Strings are created in permanent area of the JVM heap memory. So yes, it's really immutable and cannot be changed after being created.
Because in the JVM, there are three types of heap memory:
1. Young generation
2. Old generation
3. Permanent generation.
When any object are created, it goes into the young generation heap area and PermGen area reserved for String pooling.
Here is more detail you can go and grab more information from:
How Garbage Collection works in Java .
[Disclaimer this is a deliberately opinionated style of answer as I feel a more "don't do this at home kids" answer is warranted]
The sin is the line field.setAccessible(true); which says to violate the public api by allowing access to a private field. Thats a giant security hole which can be locked down by configuring a security manager.
The phenomenon in the question are implementation details which you would never see when not using that dangerous line of code to violate the access modifiers via reflection. Clearly two (normally) immutable strings can share the same char array. Whether a substring shares the same array depends on whether it can and whether the developer thought to share it. Normally these are invisible implementation details which you should not have to know unless you shoot the access modifier through the head with that line of code.
It is simply not a good idea to rely upon such details which cannot be experienced without violating the access modifiers using reflection. The owner of that class only supports the normal public API and is free to make implementation changes in the future.
Having said all that the line of code is really very useful when you have a gun held you your head forcing you to do such dangerous things. Using that back door is usually a code smell that you need to upgrade to better library code where you don't have to sin. Another common use of that dangerous line of code is to write a "voodoo framework" (orm, injection container, ...). Many folks get religious about such frameworks (both for and against them) so I will avoid inviting a flame war by saying nothing other than the vast majority of programmers don't have to go there.
String is immutable in nature Because there is no method to modify String object.
That is the reason They introduced StringBuilder and StringBuffer classes
This is a quick guide to everything
// Character array
char[] chr = {'O', 'K', '!'};
// this is String class
String str1 = new String(chr);
// this is concat
str1 = str1.concat("another string's ");
// this is format
System.out.println(String.format(str1 + " %s ", "string"));
// this is equals
System.out.println(str1.equals("another string"));
//this is split
for(String s: str1.split(" ")){
System.out.println(s);
}
// this is length
System.out.println(str1.length());
//gives an score of the total change in the length
System.out.println(str1.compareTo("OK!another string string's"));
// trim
System.out.println(str1.trim());
// intern
System.out.println(str1.intern());
// character at
System.out.println(str1.charAt(5));
// substring
System.out.println(str1.substring(5, 12));
// to uppercase
System.out.println(str1.toUpperCase());
// to lowerCase
System.out.println(str1.toLowerCase());
// replace
System.out.println(str1.replace("another", "hello"));
// output
// OK!another string's string
// false
// OK!another
// string's
// 20
// 7
// OK!another string's
// OK!another string's
// o
// other s
// OK!ANOTHER STRING'S
// ok!another string's
// OK!hello string's
For example, you want to reverse a string, will there two ways:
first:
String a = "StackOverFlow";
a = new StringBuffer(a).reverse().toString();
and second is:
String a = "StackOverFlow";
StringBuffer b = new StringBuffer(a);
a = b.reverse().toString();
at above code, I have two question:
1) in first code, does java create a "dummy object" StringBuffer in memory before do reverse and change to String.
2) at above code, does first will more optimize than second because It makes GC works more effectively ? (this is a main question I want to ask)
Both snippets will create the same number of objects. The only difference is the number of local variables. This probably won't even change how many values are on the stack etc - it's just that in the case of the second version, there's a name for one of the stack slots (b).
It's very important that you differentiate between objects and variables. It's also important to write the most readable code you can first, rather than trying to micro-optimize. Once you've got clear, working code you should measure to see whether it's fast enough to meet your requirements. If it isn't, you should profile it to work out where you can make changes most effectively, and optimize that section, then remeasure, etc.
The first way will create a very real, not at all a "dummy object" for the StringBuffer.
Unless there are other references to b below the last line of your code, the optimizer has enough information to let the environment garbage-collect b as soon as it's done with toString
The fact that there is no variable for b does not make the object created by new less real. The compiler will probably optimize both snippets into identical bytecode, too.
StringBuffer b is not a dummy object, is a reference; basically just a pointer, that resides in the stack and is very small memory-wise. So not only it makes no difference in performance (GC has nothing to do with this example), but the Java compiler will probably remove it altogether (unless it's used in other places in the code).
In answer to your first question, yes, Java will create a StringBuffer object. It works pretty much the way you think it does.
To your second question, I'm pretty sure that the Java compiler will take care of that for you. The compiler is not without its faults but I think in a simple example like this it will optimize the byte code.
Just a tip though, in Java Strings are immutable. This means they cannot be changed. So when you assign a new value to a String Java will carve out a piece of memory, put the new String value in it, and redirect the variable to the new memory space. After that the garbage collector should come by and clear out the old string.
There are cases when one needs a memory efficient to store lots of objects. To do that in Java you are forced to use several primitive arrays (see below why) or a big byte array which produces a bit CPU overhead for converting.
Example: you have a class Point { float x; float y;}. Now you want to store N points in an array which would take at least N * 8 bytes for the floats and N * 4 bytes for the reference on a 32bit JVM. So at least 1/3 is garbage (not counting in the normal object overhead here). But if you would store this in two float arrays all would be fine.
My question: Why does Java not optimize the memory usage for arrays of references? I mean why not directly embed the object in the array like it is done in C++?
E.g. marking the class Point final should be sufficient for the JVM to see the maximum length of the data for the Point class. Or where would this be against the specification? Also this would save a lot of memory when handling large n-dimensional matrices etc
Update:
I would like to know wether the JVM could theoretically optimize it (e.g. behind the scene) and under which conditions - not wether I can force the JVM somehow. I think the second point of the conclusion is the reason it cannot be done easily if at all.
Conclusions what the JVM would need to know:
The class needs to be final to let the JVM guess the length of one array entry
The array needs to be read only. Of course you can change the values like Point p = arr[i]; p.setX(i) but you cannot write to the array via inlineArr[i] = new Point(). Or the JVM would have to introduce copy semantics which would be against the "Java way". See aroth's answer
How to initialize the array (calling default constructor or leaving the members intialized to their default values)
Java doesn't provide a way to do this because it's not a language-level choice to make. C, C++, and the like expose ways to do this because they are system-level programming languages where you are expected to know system-level features and make decisions based on the specific architecture that you are using.
In Java, you are targeting the JVM. The JVM doesn't specify whether or not this is permissible (I'm making an assumption that this is true; I haven't combed the JLS thoroughly to prove that I'm right here). The idea is that when you write Java code, you trust the JIT to make intelligent decisions. That is where the reference types could be folded into an array or the like. So the "Java way" here would be that you cannot specify if it happens or not, but if the JIT can make that optimization and improve performance it could and should.
I am not sure whether this optimization in particular is implemented, but I do know that similar ones are: for example, objects allocated with new are conceptually on the "heap", but if the JVM notices (through a technique called escape analysis) that the object is method-local it can allocate the fields of the object on the stack or even directly in CPU registers, removing the "heap allocation" overhead entirely with no language change.
Update for updated question
If the question is "can this be done at all", I think the answer is yes. There are a few corner cases (such as null pointers) but you should be able to work around them. For null references, the JVM could convince itself that there will never be null elements, or keep a bit vector as mentioned previously. Both of these techniques would likely be predicated on escape analysis showing that the array reference never leaves the method, as I can see the bookkeeping becoming tricky if you try to e.g. store it in an object field.
The scenario you describe might save on memory (though in practice I'm not sure it would even do that), but it probably would add a fair bit of computational overhead when actually placing an object into an array. Consider that when you do new Point() the object you create is dynamically allocated on the heap. So if you allocate 100 Point instances by calling new Point() there is no guarantee that their locations will be contiguous in memory (and in fact they will most likely not be allocated to a contiguous block of memory).
So how would a Point instance actually make it into the "compressed" array? It seems to me that Java would have to explicitly copy every field in Point into the contiguous block of memory that was allocated for the array. That could become costly for object types that have many fields. Not only that, but the original Point instance is still taking up space on the heap, as well as inside of the array. So unless it gets immediately garbage-collected (I suppose any references could be rewritten to point at the copy that was placed in the array, thereby theoretically allowing immediate garbage-collection of the original instance) you're actually using more storage than you would be if you had just stored the reference in the array.
Moreover, what if you have multiple "compressed" arrays and a mutable object type? Inserting an object into an array necessarily copies that object's fields into the array. So if you do something like:
Point p = new Point(0, 0);
Point[] compressedA = {p}; //assuming 'p' is "optimally" stored as {0,0}
Point[] compressedB = {p}; //assuming 'p' is "optimally" stored as {0,0}
compressedA[0].setX(5)
compressedB[0].setX(1)
System.out.println(p.x);
System.out.println(compressedA[0].x);
System.out.println(compressedB[0].x);
...you would get:
0
5
1
...even though logically there should only be a single instance of Point. Storing references avoids this kind of problem, and also means that in any case where a nontrivial object is being shared between multiple arrays your total storage usage is probably lower than it would be if each array stored a copy of all of that object's fields.
Isn't this tantamount to providing trivial classes such as the following?
class Fixed {
float hiddenArr[];
Point pointArray(int position) {
return new Point(hiddenArr[position*2], hiddenArr[position*2+1]);
}
}
Also, it's possible to implement this without making the programmer explicitly state that they'd like it; the JVM is already aware of "value types" (POD types in C++); ones with only other plain-old-data types inside them. I believe HotSpot uses this information during stack elision, no reason it couldn't do it for arrays too?
I am having a hard time trying to understand what is going on with Strings and memory management in Android (Java).
To simplify, have a look at this simple piece of code:
public void createArray(){
String s = "";
String foo = "foo";
String lorem = "Lorem ipsum ad his scripta blandit partiendo, eum
fastidii accumsan euripidis in, eum liber hendrerit an.";
ArrayList<Item> array = new ArrayList<Item>();
for( int i=0; i<20000; i++ ){
s = foo.replace("foo", lorem);
array.add( new Item(s) );
}
System.gc();
}
private class Item{
String name;
public Item(String name){
this.name = name;
}
}
If executed, createArray will allocate more than 5Mb in memory. Even with the System.gc() instruction, memory won't be deallocated after the loop.
Now, if we replace s = foo.replace("foo", lorem); with s = lorem;, allocated memory will only increase by 0.5Mb.
I need to understand what is going on to improve my application's performance.
Can anybody explain how should I replace Strings in a case like this? And why is System.gc() not deallocating memory?
Thanks.
UPDATE:
Thanks for the answers, now I understand that System.gc() is only a hint.
To clarify the other question (the important one):
How can I dynamically generate 20000 Strings ("foo1", "foo2"... "foo20000") add them to the ArrayList and do not run out of memory? If this 20000 strings were static they wouldn't allocate more than 0.5Mb in memory. Besides, if s = foo.replace("foo", lorem) creates a brand new String, why my function allocates 5Mb which is 10 times more memory? Shouldn't be around 1Mb?
(I am already thinking in a workaround but I want to be sure there is no way to generate the strings dynamically without using this huge amount of memory)
replace is generating a brand new String object every time it is called, since strings in Java are immutable and so modifications cannot be made "ìn-place". Also, you're adding that String to an ArrayList which will hold a reference to the new object, preventing it from being collected.
Because String's are immutable, calling foo.replace("foo", lorem) will create a new String each time. In the example where you simply set s = lorem, no new String is created.
Also, System.gc() is simply a recommendation to the VM, and it will in no way guarantee a garbage collection. There is nothing you can do to force a garbage collection. (other than using up all available memory that is)
System.gc() is a hint to the garbage collector to run again, when it has the time.
That said, the garbage collector only collects unreferenced objects, and all the strings you generated are still being held by the object array. It is entirely possible that you might add the lines after System.gc()
for (String item : array) {
System.out.println(item);
}
Which would have proven the garbage collector's wisdom in not destroying the strings.
In the event that you have some true need to reuse a String, you can trade off CPU cycles for possibly lower memory footprint with String intern methods (which will scan allocated strings for duplicates and return a reference to the single kept string if found.
You might argue that in your case the garbage collector hint could invoke compile time optimization techniques to realize that array is not being used downstream and thus act to destroy the strings before they would normally be dereferenced at the end of the program; however, it would add side effects that would appear quite strange in some runtime environments (like debugging sessions).
In the end, it is not possible to accurately perform reflection, code stepping, compile binding to source code, and various types of reflection with such exotic compile time optimization. That said, Java optimizes itself pretty well considering how many features you get out-of-the-box.
Keep in mind that System.gc() does not force the garbage collector to run. It is merely a hint that you would like it to run at some point in the future.