可视化JVM中的内存管理

5 篇文章 0 订阅

🚀 Visualizing memory management in JVM(Java, Kotlin, Scala, Groovy, Clojure)

Part of "memory-management" series

     

Please follow me on Twitter for updates and let me know if something can be improved in the post.


In this multi-part series, I aim to demystify the concepts behind memory management and take a deeper look at memory management in some of the modern programming languages. I hope the series would give you some insights into what is happening under the hood of these languages in terms of memory management. In this chapter, we will look at the memory management of the Java Virtual Machine(JVM) used by languages like Java, Kotlin, Scala, Clojure, JRuby and so on.

If you haven’t read the first part of this series, please read it first as I explained the difference between the Stack and Heap memory there which would be useful to understand this chapter.

JVM memory structure

First, let us see what the memory structure of JVM is. This is based on JDK 11 onwards. Below is the memory available to a JVM process and is allocated by the Operating System(OS).

JVM Memory structure

This is the native memory allocated by the OS and the amount depends on OS, processor, and JRE. Let us see what the different areas are for:

Heap Memory

This is where JVM stores objects or dynamic data. This is the biggest block of memory area and this is where Garbage Collection(GC) takes place. The size of heap memory can be controlled using the Xms(Initial) and Xmx(Max) flags. The entire heap memory is not committed to the Virtual Machine(VM) as some of it is reserved as virtual space and the heap can grow to use this. Heap is further divided into “Young” and “Old” generation space.

  • Young generation: Young generation or “New Space” is where new objects live and is further divided into “Eden Space” and “Survivor Space”. This space is managed by “Minor GC” also sometimes called “Young GC”
    • Eden Space: This is where new objects are created. When we create a new object, memory is allocated here.
    • Survivor Space: This is where objects that survived the minor GC are stored. This is divided into two halves, S0 and S1.
  • Old generation: Old generation or “Tenured Space” is where objects that reached the maximum tenure threshold during minor GC live. This space is managed up by “Major GC”.

Thread Stacks

This is the stack memory area and there is one stack memory per thread in the process. This is where thread-specific static data including method/function frames and pointers to objects are stored. The stack memory limit can be set using the Xss flag.

Meta Space

This is part of the native memory and doesn’t have an upper limit by default. This is what used to be Permanent Generation(PermGen) Space in earlier versions of JVM. This space is used by the class loaders to store class definitions. If this space keeps growing, the OS might move data stored here from RAM to virtual memory which might slow down the application. To avoid that its possible to set a limit on meta-space used with the -XX:MetaspaceSize and -XX:MaxMetaspaceSize flag in which case application might just throw out of memory errors.

Code Cache

This is where the Just In Time(JIT) compiler stores compiled code blocks that are often accessed. Generally, JVM has to interpret byte code to native machine code whereas JIT-compiled code need not be interpreted as it is already in native format and is cached here.

Shared Libraries

This is where native code for any shared libraries used are stored. This is loaded only once per process by the OS.


JVM memory usage (Stack vs Heap)

Now that we are clear about how memory is organized let’s see how the most important parts of it are used when a program is executed.

Let’s use the below Java program, the code is not optimized for correctness hence ignore issues like unnecessary intermediatory variables, improper modifiers and such, the focus is to visualize stack and heap memory usage.

class Employee {
    String name;
    Integer salary;
    Integer sales;
    Integer bonus;

    public Employee(String name, Integer salary, Integer sales) {
        this.name = name;
        this.salary = salary;
        this.sales = sales;
    }
}

public class Test {
    static int BONUS_PERCENTAGE = 10;

    static int getBonusPercentage(int salary) {
        int percentage = salary * BONUS_PERCENTAGE / 100;
        return percentage;
    }

    static int findEmployeeBonus(int salary, int noOfSales) {
        int bonusPercentage = getBonusPercentage(salary);
        int bonus = bonusPercentage * noOfSales;
        return bonus;
    }

    public static void main(String[] args) {
        Employee john = new Employee("John", 5000, 5);
        john.bonus = findEmployeeBonus(john.salary, john.sales);
        System.out.println(john.bonus);
    }
}
Click on the slides and move forward/backward using arrow keys to see how the above program is executed and how the stack and heap memory is used:

JVM Memory usage(Stack vs Heap)

As you can see:

  • Every function call is added to the thread’s stack memory as a frame-block
  • All local variables including arguments and the return value is saved within the function frame-block on the Stack
  • All primitive types like int are stored directly on the Stack
  • All object types like EmployeeIntegerString are created on the Heap and is referenced from the Stack using Stack pointers. This applies to static fields as well
  • Functions called from the current function is pushed on top of the Stack
  • When a function returns its frame is removed from the Stack
  • Once the main process is complete the objects on the Heap do not have any more pointers from Stack and becomes orphan
  • Unless you make a copy explicitly, all object references within other objects are done using pointers

The Stack as you can see is automatically managed and is done so by the operating system rather than JVM itself. Hence we do not have to worry much about the Stack. The Heap, on the other hand, is not automatically managed by the OS and since its the biggest memory space and holds dynamic data, it could grow exponentially causing our program to run out of memory over time. It also becomes fragmented over time slowing down applications. This is where the JVM helps. It manages the Heap automatically using the garbage collection process.


JVM Memory management: Garbage collection

Now that we know how JVM allocates memory, let us see how it automatically manages the Heap memory which is very important for the performance of an application. When a program tries to allocate more memory on the Heap than that is freely available(depending on the Xmx config) we encounter out of memory errors.

JVM manages the heap memory by garbage collection. In simple terms, it frees the memory used by orphan objects, i.e, objects that are no longer referenced from the Stack directly or indirectly(via a reference in another object) to make space for new object creation.

GC Roots

The garbage collector in JVM is responsible for:

  • Memory allocation from OS and back to OS.
  • Handing out allocated memory to the application as it requests it.
  • Determining which parts of the allocated memory is still in use by the application.
  • Reclaiming the unused memory for reuse by the application.

JVM garbage collectors are generational(Objects in Heap are grouped by their age and cleared at different stages). There are many different algorithms available for garbage collection but Mark & Sweep is the most commonly used one.

Mark & Sweep Garbage collection

JVM uses a separate daemon thread that runs in the background for garbage collection and the process runs when certain conditions are met. Mark & Sweep GC generally involves two phases and sometimes there is an optional third phase depending on the algorithm used.

Mark & sweep GC

 

  • Marking: First step where garbage collector identifies which objects are in use and which ones are not in use. The objects in use or reachable from GC roots(Stack pointers) recursively are marked as alive.
  • Sweeping: The garbage collector traverses the heap and removes any object that is not marked alive. This space is now marked as free.
  • Compacting: After deleting unused objects, all the survived objects will be moved to be together. This will decrease fragmentation and increase the performance of allocation of memory to newer objects

This type of GC is also referred to us stop-the-world GC as they introduce pause-times in the application while performing GC.

JVM offers few different algorithms to choose from when it comes to GC and there might be few more options available depending on the JDK vendor you use(Like the Shenandoah GC, available on OpenJDK). The different implementations focus on different goals like:

  • Throughput: Time spent collecting garbage instead of application time affects throughput. The throughput ideally should be high(I.e when GC times are low).
  • Pause-time: The duration for which GC stops the application from executing. The pause-time ideally should be very low.
  • Footprint: Size of the heap used. This ideally should be kept low.

Collectors available as of JDK 11

As of JDK 11, which is the current LTE version, the below garbage collectors are available and the default used is chosen by JVM based on hardware and OS used. We can always specify the GC to be used with the -XX switch as well.

  • Serial Collector: It uses a single thread for GC and is efficient for applications with small data sets and is most suitable for single-processor machines. This can be enabled using -XX:+UseSerialGC switch.
  • Parallel Collector: This one is focused on high throughput and uses multiple threads to speed up the GC process. This is intended for applications with medium to large data sets running on multi-threaded/multi-processor hardware. This can be enabled using -XX:+UseParallelGC switch.
  • Garbage-First(G1) Collector: The G1 collector is a mostly concurrent collector(Means only expensive work is done concurrently). This is for multi-processor machines with a large amount of memory and is enabled as default on most modern machines and OS. It has a focus on low pause-times and high throughput. This can be enabled using -XX:+UseG1GC switch.
  • Z Garbage Collector: This is a new experimental GC introduced in JDK11. It is a scalable low-latency collector. It’s concurrent and does not stop the execution of application threads, hence no stop-the-world. It is intended for applications that require low latency and/or use a very large heap(multi-terabytes). This can be enabled using -XX:+UseZGC switch.

GC process

Regardless of the collector used, JVM has two types of GC process depending on when and where its performed, the minor GC and major GC.

Minor GC

This type of GC keeps the young generation space compact and clean. This is triggered when below conditions are met:

  • JVM is not able to get the required memory from the Eden space to allocate a new object

Initially, all the areas of heap space are empty. Eden memory is the first one to be filled, followed by survivor space and finally by tenured space.

Let us look at the minor GC process:

Click on the slides and move forward/backward using arrow keys to see the process:

JVM Minor GC

Let us assume that there are already objects on the Eden space when we start(Blocks 01 to 06 marked as used memory)

  1. The application creates a new object(07)
  2. JVM tries to get required memory from Eden space, but there is no free space in Eden to accommodate our object and hence JVM triggers minor GC
  3. The GC recursively traverses the object graph starting from stack pointers to mark objects that are used as alive(Used memory) and remaining objects as garbage(Orphans)
  4. JVM chooses one random block from S0 and S1 as the “To Space”, let’s assume it was S0. The GC now moves all the alive objects into the “To Space”, S0, which was empty when we started and increments their age by one.
  5. The GC now empties the Eden space and the new object is allocated memory in the Eden space
  6. Let us assume that some time has passed and there are more objects on the Eden space now(Blocks 07 to 13 marked as used memory)
  7. The application creates a new object(14)
  8. JVM tries to get required memory from Eden space, but there is no free space in Eden to accommodate our object and hence JVM triggers second minor GC
  9. The mark phase is repeated and alive/orphan objects are marked including the ones in survivor space “To Space”
  10. JVM chooses the free S1 as the “To Space” now and S0 becomes “From Space”. The GC now moves all the alive objects from Eden space and the “From Space”, S0, into the “To Space”, S1, which was empty when we started and increments their age by one. Since some objects don’t fit here, they are moved to the “Tenured Space” as the survivor space cannot grow and this process is called premature promotion. This can happen even if one of the survivor space is free
  11. The GC now empties the Eden space and the “From Space”, S0, and the new object is allocated memory in the Eden space
  12. This keeps on repeating for each minor GC and the survivors are shifted between S0 and S1 and their age is incremented. Once the age reaches the “max-age threshold”, 15 by default, the object is moved to the “Tenured space”

So we saw how minor GC reclaims space from the young generation. It is a stop-the-world process but it’s so fast that it is negligible most of the time.

Major GC

This type of GC keeps the old generation(Tenured) space compact and clean. This is triggered when below conditions are met:

  • Developer calls System.gc(), or Runtime.getRunTime().gc() from the program.
  • JVM decides there is not enough tenured space as it gets filled up from minor GC cycles.
  • During minor GC, if the JVM is not able to reclaim enough memory from the Eden or survivor spaces.
  • If we set a MaxMetaspaceSize option for the JVM and there is not enough space to load new classes.

Let us look at the major GC process, it’s not as complex as minor GC:

  1. Let us assume that many minor GC cycles have passed and the tenured space is almost full and JVM decides to trigger a “Major GC”
  2. The GC recursively traverses the object graph starting from stack pointers to mark objects that are used as alive(Used memory) and remaining objects as garbage(Orphans) in the tenured space. If the major GC was triggered during a minor GC the process includes the young(Eden & Survivor) and tenured space
  3. The GC now removed all orphan objects and reclaims the memory
  4. During a major GC event, if there are no more objects in the Heap, the JVM reclaims memory from the meta-space as well by removing loaded classes from it this is also referred to as full GC

Conclusion

This post should give you an overview of the JVM memory structure and memory management. This is not exhaustive, there are a lot more advanced concepts and tuning options available for specific use cases and you can learn about them from https://docs.oracle.com. But for most JVM(Java, Kotlin, Scala, Clojure, JRuby, Jython) developers this level of information would be sufficient and I hope it helps you write better code, considering these in mind, for more performant applications and keeping these in mind would help you to avoid the next memory leak issue you might encounter otherwise.

I hope you had fun learning about the JVM internals, stay tuned for the next post in the series.


References


If you like this article, please leave a like or a comment.

You can follow me on Twitter and LinkedIn.

Also published at Dev.to

 

后记:由于很多网站(csdn,oschina,头条)写博文的富文本编辑器不支持iframe嵌入,故而改成超链接了。且不同的jdk版本(引入的新特性)有出入(比如我早年用jdk1.4时,很多时髦的特性都没有,都得自己想办法),所以请根据实际情况阅读该文。

 

我也追加几条以前的资料供参考(更多家业请看 https://github.com/dongguangming/java,后续(最近由于存储空间到了上限,需要整理)会慢慢上传):

  1. The Java Memory Model  http://www.cs.umd.edu/~pugh/java/memoryModel/https://github.com/dongguangming/java/blob/master/jsr133.pdf

  2. The Java  Virtual Machine Specification (Java SE 8 Edition)https://github.com/dongguangming/java/blob/master/jvms8.pdf

  3. JVM (java virtual machine) in detail in java
      https://www.javamadesoeasy.com/2015/06/jvm-java-virtual-machine.html

  4. Java Virtual Machine's Internal Architecture  https://www.artima.com/insidejvm/ed2/jvm6.html

  5. 7. Memory : Stack vs Heap  https://gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html

  6. What Is Garbage Collection In Java And How Does It Work  https://www.softwaretestinghelp.com/garbage-collection-in-java/

  7. Understanding The Java Virtual Machine (JVM) Architecture Part 1 https://blog.pythian.com/understanding-java-virtual-machinejvm-architecture-part1/

  8. Java Garbage Collection Introduction https://javapapers.com/java/java-garbage-collection-introduction/

  9. 一文看懂JVM内存布局及GC原理 https://www.infoq.cn/article/3WyReTKqrHIvtw4frmr3

  10. What is default garbage collector for Java 7, 8 and 9  https://www.javamadesoeasy.com/2016/12/what-is-default-garbage-collector-for.html

  11. Java Garbage Collection Algorithms [till Java 9] https://howtodoinjava.com/java/garbage-collection/all-garbage-collection-algorithms/

  12. Advances in Programming Languages: Memory management  http://homepages.inf.ed.ac.uk/stg/teaching/apl/handouts/memory.pdf

  13.  
### 回答1: Java可视化内存管理是指通过可视化界面展示出Java程序运行的内存情况,并提供相关图表和数据分析,从而帮助开发人员更好地理解和管理内存资源。 Java程序在运行过程,需要通过内存来存储各种对象和数据。如果内存管理不当,会引发内存泄漏、内存溢出等问题,严重影响程序的性能和稳定性。为了解决这些问题,Java提供了一套自动内存管理机制,即垃圾回收器。垃圾回收器负责自动释放不再使用的内存,使得开发人员不需要关心手动内存管理,从而提高开发效率和程序的可维护性。 可视化内存管理工具可以帮助开发人员实时监控Java程序的内存使用情况。通过界面展示当前内存分配情况、垃圾回收的执行情况及效果,开发人员可以及时了解到程序的内存状况,及时进行优化和调整。 这些工具一般提供了一些核心功能,如内存使用曲线图、内存分配情况、垃圾回收器的执行情况等。通过这些图表和数据,开发人员可以直观地了解到内存使用的趋势和规律,以及垃圾回收的效果。开发人员可以根据这些信息,采取相应的优化措施,例如调整内存分配策略、提高垃圾回收效率等,从而改善程序的性能和稳定性。 总之,Java可视化内存管理工具能够帮助开发人员更好地了解和管理程序的内存资源,提高程序的性能和稳定性。通过实时监控和分析内存使用情况,开发人员可以针对性地进行优化和调整,从而提高程序的运行效果。 ### 回答2: Java可视化内存管理主要是指Java虚拟机(JVM)对内存资源的分配、使用和回收进行可视化展示,以便开发者更好地了解和优化程序的内存使用情况。 首先,Java内存管理是由JVM负责的,JVM将内存区域划分为多个部分,包括堆内存、栈内存、方法区等。Java可视化内存管理能够展示这些内存区域的使用情况,如堆内存的大小、已使用空间和剩余空间等,开发者可以根据这些信息进行内存调优。 其次,Java可视化内存管理能够显示对象的创建和销毁过程。开发者可以观察对象的生命周期,了解对象的创建时间、存活时间和销毁时间,从而判断对象是否存在内存泄漏或过早销毁等问题。 此外,Java可视化内存管理还能够展示对象之间的引用关系。开发者可以查看对象的引用链,了解对象之间的依赖关系和循环引用等情况,以便及时解除无用的引用,避免内存泄漏问题。 除了以上功能,Java可视化内存管理还可以显示内存的使用情况和内存泄漏等警告信息。开发者可以通过监控内存的使用情况,及时发现内存占用过高的问题,并进行相应的调优。同时,当存在内存泄漏时,可视化内存管理工具会给出相应的警告信息,提醒开发者进行修复。 综上所述,Java可视化内存管理可以帮助开发者更好地了解和优化程序的内存使用情况,提高程序的性能和稳定性。通过可视化的展示,开发者可以及时发现和解决内存相关的问题,提升程序的质量。 ### 回答3: Java提供了一个可视化内存管理机制,通过Java虚拟机(JVM)来实现。在Java程序,内存分为堆(Heap)和栈(Stack)两部分。 堆是用来存储对象的地方,所有的对象都在堆分配空间。堆是在Java虚拟机启动时自动创建的,其大小由启动参数决定。堆的管理是自动的,即当对象不再被引用时,垃圾回收器会自动释放该对象占用的内存空间。为了保证堆的高效利用,Java提供了分代垃圾回收机制,将堆分为新生代和老年代,不同的对象会被分配到不同的代,并采用不同的垃圾回收算法。 栈是用来存储变量和方法调用的地方。每个线程在运行时都会有一个独立的栈,用来存储局部变量和方法的调用信息。栈的内存分配和释放是自动的,一旦方法调用结束,栈帧的数据就会被立即释放。 Java通过可视化工具,例如Java VisualVM,来监控和管理内存使用情况。这些工具提供了图形化的界面,可以实时查看堆和栈的使用情况,包括对象的数量、大小、引用关系等。同时,它们还提供了垃圾回收的相关信息,例如回收时间、频率等。通过这些工具,开发人员可以及时发现内存泄漏和性能问题,并采取相应的措施进行调优。 总的来说,Java可视化内存管理机制提供了方便、高效的方式来管理内存,帮助开发人员更好地优化程序性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值