JVM tuning suggestions based on practical experience~

In fact, JVM tuning has always been a relatively difficult technical problem. Usually, our main energy is responsible for code writing, and we have not paid too much attention to and participated in the JVM tuning of the online environment. And when the performance of the machine deteriorates one day, most of them first perform elastic expansion, and then ignore it. So it is easy to ignore the tuning skills of the JVM over time.

So let us review today, what points should we pay attention to during JVM tuning?

The ratio of the young generation to the old generation needs to be adjusted according to the actual scene

We know that in the Java memory model, the largest memory area block is called the heap, and when the generational collection type garbage collector is used in Hotspot JDK8, the heap is divided into the young generation and the old generation.

Young generation: Newly created objects live here, internally divided into eden area and from survivor, to survivor area.

Old generation: It is mainly used to store objects that are still alive after multiple collections by the young generation (the number of collections exceeds the threshold to promote), or some objects that are promoted due to other special reasons.

The old generation has a characteristic, that is, the objects are relatively stable, so it may be more difficult to tune this part of GC. When we pay attention to the old generation, we pay more attention to whether the space is enough.

In the young generation of Jvm, there are two modules, namely the eden area and the survivor area. The overall memory layout of the young generation and the old generation is as follows:

In the Hotspot version of Jdk8, the default ratio of the young generation to the old generation is 1:2, and this ratio can be controlled by the following parameters.

 –XX:NewRatio=2 (这里意思是年轻代占据了1/3的堆内存空间,老年代占比是年轻代的两倍)
复制代码

Seeing this, you may think, why not set the memory of the young generation to be larger, can this reduce the number of minior gc? However, this will also lead to insufficient memory in the old generation, so this has to be combined with actual tests to get the best ratio. If you're in doubt, I recommend using the default ratios.

If you find a better ratio setting than the default value in the actual test, you can refer to the following parameters:

-Xms128M : set the minimum heap memory

-Xmx128M : set the maximum heap memory

-XX:NewSize=64M : Set the minimum value of the New area

-XX:MaxNewSize=64 : Set the maximum value of the New area

-XX:NewRatio=2 : Set the ratio of the Old area to the New area

-Xmn64M : Set the size of the New area, which is equivalent to the two parameters -XX:NewSize=64M and -XX:MaxNewSize=64M. Once this value is set, –XX:NewRatio will be invalid.

It is not recommended to use the NewSize and MaxNewSize parameters here, it is recommended to use the Xmn area instead of them. In this way, the size of the young generation can be allocated to the maximum at the beginning, reducing the consumption of this memory expansion process. But it also depends on whether the actual machine memory is tight.
In addition, there is a very important summary of practical tuning experience in GC: Since the GC cost of the old generation is usually much higher than that of the young generation, it is recommended to properly set the size of the young generation through the Xmn command area. Minimize the promotion of objects to the old generation.

Reasonably set the ratio of Eden area and Survivor area

These two areas exist in the young generation, and their size ratios can be set through the following parameters:

-XX:SurvivorRatio=2 (表示eden区大小是survivor区大小的2倍)
复制代码

Usually, in JDK8, the default ratio is 1:8 (single survivor:eden). This ratio is actually the value set by JDK developers after many actual battles. Therefore, it is not recommended to adjust casually without actual stress testing. this ratio. Why do I say this? Here I have summarized two reasons:
Do not set too high a space in the Eden area. Although there is a lot of storage space for young generation objects, there will be very little space for survivors, which may cause objects that are promoted from the Eden area to the Survivor area. , there is not enough space to store it, and then it goes directly to the old generation.

Do not set the eden area size too low

First of all, the insufficient space in the Eden area will lead to frequent occurrence of minior gc, and at the same time, the survivor area will also lead to excess space and waste of memory.

How to allocate heap memory in combination with business scenarios

We mentioned earlier that it is very important to allocate the ratio of the eden area and the survivor area reasonably. In order to let everyone understand the importance of this more deeply, I will analyze the GC tuning with you through a case.

Suppose we have a high-concurrency message middle-end service that provides crud operations for basic user information. It is estimated that the qps after the launch will be around 6000+. It is estimated that the service nodes deployed after the launch will be 2core/4gb, 16 units. So how to evaluate the jvm parameters at this time.

Here we can analyze, assuming that 6000+ requests are allocated to 16 nodes, then each node probably carries about 400 qps. Here, because there will be more database queries in the bottom layer of the message platform, the storage part is divided into databases and tables, and most of the cases will be processed by caching.

Suppose our message object Message is:

public class MessagePO {
    private Long id;
    private String sid;
    private Long userId;
    private String content;
    private Integer type;
    private Integer readStatus;
    private Integer replyStatus;
    private String ext;
    private Date sendTime;
    private Date updateTime;
    private Long receiveId;
//getter setter省略}
复制代码

Here we can simulate the storage content of this object, and then estimate the size:

public static void main(String[] args) throws IllegalAccessException {
    MessagePO messagePO = new MessagePO();
    messagePO.setUserId(10012L);
    messagePO.setReadStatus(1);
    messagePO.setReceiveId(12389L);
    messagePO.setReplyStatus(1);
    messagePO.setContent("这是一条测试语句");
    messagePO.setExt("{"key":"value"}");
    messagePO.setSid("981hhdkahnhiodqw012");
    messagePO.setSendTime(new Date());
    messagePO.setUpdateTime(new Date());
    messagePO.setId(191912342L);
    messagePO.setType(1);
    System.out.println(messagePO);

    ClassIntrospector ci = new ClassIntrospector();
    System.out.println(ci.introspect(new Short((short) 1)).getDeepSize()); //16字节
    System.out.println(ci.introspect(messagePO).getDeepSize()); //912字节
    System.out.println(ci.introspect(new Integer((short) 1)).getDeepSize()); //16字节
    System.out.println(ci.introspect(new Long((short) 1)).getDeepSize()); //24字节
}


复制代码

Use tools to estimate the size of a single messagePO object at about 912bytes, and here we estimate it to be about 1kb. Then, in the face of 400qps access to a single node, the MessagePO object alone may start at 400kb per second, and there may be other miscellaneous other objects generated. Here we can estimate the amount by 10 times for the time being. (The 10 times here should be calculated in combination with business scenarios). Finally, we actually need to consider whether the data structure of List will be used in the code. If so, it may have to be increased by 10 times, that is, the object generation rate of 40mb/s.

Most of these newly generated objects are in a state of being discarded when they are used up, so basically they cannot survive a round of Minior GC. However, during Minior GC, objects may still be referenced (for example, some methods are halfway executed), and some objects may survive each time Minior GC recycles, and then enter the survivor area.

As we said before, the total memory of the service node is 4gb, so the jvm heap memory can allocate about 60% of the space (reserved part is meta space and thread memory, etc.), which is about 2.5gb. So at this point you can try to assign parameters:

  • -Xms2560m -Xmx2560m -Xmn1024m

This parameter can be allocated to the young generation with a size of about 1 GB. According to the default ratio, the Eden area is 780 MB, and the two Survivor areas are combined to about 260 MB, that is, a single Survivor area is 130 MB. This means that, according to the situation we expected above, the object generation rate of 40mb/s can occupy the eden area in about 20 seconds. According to statistics, about 95% of the objects will be recycled, leaving about 35mb Objects are put into the survivor area.

At present, everything seems to be quite normal from a theoretical perspective, but don't forget that it still needs to be verified through stress testing. Suppose one day our business scenario changes, and programmers use a lot of Lists to store objects in the code, then the situation of GC may not be as simple as you think.

For example, one day, when you find a requirement, the online old generation GC becomes frequent, but there is no problem in the code, then at this time, there may be a possibility that your survivor area is too small, The volume of the object that causes the object to survive after the minior gc is greater than half of the survivor area, which leads to the direct promotion of the object.

At this time, you can conduct tuning analysis based on business scenarios, such as reducing the size ratio of the old generation and increasing the size of the survivor area.

Of course, what I said above requires you to analyze it in combination with business scenarios. Here I just gave an idea. I summarized the overall idea, which is roughly: allocate the Eden area and the Survivor area reasonably, and try not to let objects enter the old age .

Pay attention to the memory compression frequency of the old generation when using the CMS garbage collector

In the old generation, CMS will first use the mark clear algorithm to reclaim memory by default. Every time the old generation performs full gc, a counter will be accumulated.

When the full gc of the old generation exceeds a certain number of times, a memory compression will be performed. This memory compression can reduce the existence of memory fragmentation, which is controlled by the following parameters

-XX:CMSFullGCsBeforeCompaction=10
复制代码

This value is 0 by default, which means that every time the full gc of the old generation is executed, it will trigger a compression of memory fragments. During the process of memory compression, the GC time will be extended. So I think this parameter can be tuned, but it needs to be adjusted in combination with actual combat.

Reasonably set the recycling frequency of the CMS garbage collector in the old age

-XX:CMSInitiatingOccupancyFraction indicates the old generation usage threshold that triggers CMS GC, generally set to 70~80 (percentage), setting too small will increase the frequency of CMS GC discovery, setting too large may cause concurrent mode failure or promotion failure. The default is -1, which means that the CMS GC will be automatically triggered by the JVM. 

-XX:+UseCMSInitiatingOccupancyOnly means that CMS GC is only triggered based on CMSInitiatingOccupancyFraction. If this parameter is not set, JVM will only trigger the first CMS GC according to CMSInitiatingOccupancyFraction, and it will be triggered automatically later. It is recommended to set these two parameters at the same time.

The default value of CMSInitiatingOccupancyFraction is 92%, so when using the cms garbage collector, the recovery of the old generation is very small by default, and if the memory reaches 92% occupancy, the recovery process of the CMS garbage collector will be triggered at this time.

If it is found that the memory space is insufficient at this time, it will enter the link of using the Serial Old collector for recycling. The performance at this stage will be very poor, so this is also a hidden risk of the CMS garbage collector. In extreme scenarios There may be a long stuw.

What points should be paid attention to in JVM parameters in containerized deployment

In a Java program of the HotSpot type, the memory size of the JVM is usually (heap size + stack space * number of threads + metaspace + other memory), so it is not enough to configure the Xmx (maximum heap memory) parameter.

In fact, the default startup heap size of a Java program is 1/4 of the memory size of the operating system; it can be viewed through the parameter -XshowSettings:vm -version. If we deploy the program to the container node, but do not want to configure the xmx type parameter, we can use UseCGroupMemoryLimitForHeap to set it at this time. After using this parameter, the Java application can read the memory in the container node when it starts size. In this way, there is no need to worry about the JVM memory size exceeding the cgoup memory usage of the container, and being killed by mistake. But the utilization rate of using this parameter will be very low.

Of course, if you don't trust the automatic transmission mechanism, you can use the manual transmission method to set the Xmx memory parameters for safety. (automatic)

Guess you like

Origin blog.csdn.net/qq_41221596/article/details/129347498