Handling of memory overflow in intercepting substring() field in jdk1.6

Keywords: Java

Recently, a little friend told me that the program he wrote had no problem in the test environment, but memory overflow occurred frequently when it was sent to the production environment. This problem has plagued him for more than a week. So at the weekend, I began to help him check all kinds of problems.

If the article is helpful to you, let's praise, collect, comment and share. Let's go~~

Little partner's questions

Problem determination

The whole process of troubleshooting is quite time-consuming. Here, I will directly talk about the problem located. Later, I will write a separate article on the detailed troubleshooting process!

In the process of troubleshooting, I found that the JDK used by this little partner is still version 1.6. At first, I didn't think so much. I continued to check the code he wrote and didn't find any problems. However, once the program in the production environment is started, it is not long before the JVM throws a memory overflow exception.

That's strange. What's going on?

Add reasonable JVM parameters when starting the program, and the problem still exists...

No way, keep looking at his code! Inadvertently, I found that the substring() method of the String class was heavily used to intercept strings in the code he wrote. So I followed the code in the JDK to see the parameters passed in.

This inadvertently click in a check, unexpectedly found the problem!!

String type in JDK1.6

After analysis, I found a big hole in the String class in JDK1.6! Why is it a pit? Because its substring() method will make people miserable! Not much to say, let's first look at the substring() method of the String class in JDK 1.6.

public String substring(int bedinIndex, int endIndex){
    if(beginIndex < 0){
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if(endIndex > count){
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if(beginIndex > endIndex){
          throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value);
}

Next, let's take a look at a construction method of the String class in JDK 1.6, as shown below.

String(int offset, int count, char[] value){
    this.value = value;
    this.offset = offset;
    this.count = count;
}

See, here, I believe the careful little partner has found the problem. The culprit of the problem is the following line of code.

this.value = value;

In JDK1.6, when using the constructor of String class to create a substring, it is not just a simple copy of the required objects, but the whole value will be referenced every time. If the original String is large, even if the String is no longer applied, the memory allocated by the String will not be released.   This is also my conclusion after a long time of analyzing the code. It's really too pit!!

Now that the problem has been found, we must solve it.

Upgrade JDK

Since there are such huge holes in the String class in JDK 1.6, the most direct and effective way is to upgrade the JDK. So I explained the situation to my little partner and asked him to upgrade JDK to JDK1.8.

Similarly, let's look at the substring() method of the String class in JDK 1.8.

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
        : new String(value, beginIndex, subLen);
}

In the substring() method of the String class in JDK1.8, the constructor of the String class is also called to generate a substring. Let's take a look at this constructor, as shown below.

public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

In JDK1.8, when we need a substring, substring generates a new string, which is constructed through the Arrays.copyOfRange function of the constructor. This is no problem.

Optimize JVM startup parameters

Here, in order to better improve the performance of the system, I also optimized the JVM startup parameters for this little partner.

Authorized by small partners,   I briefly list their business scale and server configuration: the whole system adopts a distributed architecture, the business services in the architecture are deployed in clusters, the average daily visits are hundreds of millions, the average daily transaction orders are 50W~100W, and the server nodes of the order system are configured as 4-core 8G. JDK has been upgraded to version 1.8.

According to the above conditions, I give the parameter configuration after JVM tuning.

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M

As for why the above JVM parameter configuration is given, I will write a separate article to specifically analyze how to tune the JVM parameters according to the actual business scenario.

After analyzing and solving the problem, the small partner's program runs smoothly in the production environment. At least there is no memory overflow at present!!

conclusion

If a relatively large object is created in the program, and we generate some other information based on the large object, at this time, we must release the reference relationship with the large object, otherwise, the hidden danger of memory overflow will be buried.

The goal of JVM optimization is to allocate and recycle objects in the new generation as much as possible, try not to let too many objects enter the old generation frequently, avoid frequent garbage collection for the old generation, and give the system sufficient memory size to avoid frequent garbage collection for the new generation.

 

Posted by ytse on Fri, 08 Oct 2021 19:51:41 -0700