【JVM】五、分代模型中的年轻代、老年代和永久代
我们将重点关注JVM内存划分的一些细节。这将帮助大家更深入地理解JVM内存划分的原理,以及我们在代码中创建的对象是如何在JVM中分配和流动的
1、背景引入
从今天开始,我们将重点关注JVM内存划分的一些细节。这将帮助大家更深入地理解JVM内存划分的原理,以及我们在代码中创建的对象是如何在JVM中分配和流动的。这对于大家深入理解JVM原理将大有裨益。
首先,让我们介绍一下JVM内存的一个分代模型:年轻代、老年代和永久代。
现在,大家应该都知道,我们在代码中创建的对象都会进入到Java堆内存中,比如下面的代码:
public class User {
public static void main(String[] args) {
while(true) {
loadUserInfosFromDB();
Thread.sleep(1000);
}
}
private static void loadUserInfosFromDB() {
UserManager UserManager = new UserManager();
UserManager.load();
}
}
在这段代码中,我们进行了一些微小的调整。在main()方法里,我们周期性地执行了loadUserInfosFromDB()方法,以加载副本数据。
首先,一旦main()方法被调用,它的栈帧就会被压入main线程的Java虚拟机栈中。如下图。
在每次执行while循环时,都会调用loadUserInfosFromDB()方法。这个过程中,Java虚拟机会将loadUserInfosFromDB()方法的栈帧压入当前的虚拟机栈中。如下图:

在执行loadUserInfosFromDB()方法时,会在Java堆内存中创建一个UserManager对象实例。同时,loadUserInfosFromDB()方法的栈帧中会有一个名为UserManager的局部变量,用于引用位于Java堆内存中的UserManager对象实例。如下图:

然后就会执行UserManager对象的load()方法。
2、对象的存活周期
现在存在一个问题,即在代码中的UserManager对象实际上是一个短暂存在的对象。
我们可以观察一下,在loadUserInfosFromDB()方法中创建了UserManager对象,并执行了该对象的load()方法。一旦load()方法执行完毕,loadUserInfosFromDB()方法就会结束。
当loadUserInfosFromDB()方法结束时,它的栈帧将被弹出。如下图:

在之前的文章中提到,当没有任何引用指向UserManager对象时,该对象将被JVM的垃圾回收机制所回收。垃圾回收线程负责释放不再使用的对象所占用的内存空间,从而优化系统资源利用。如下图。

在main()方法的while循环中,当再次执行loadUserInfosFromDB()方法时,会经历以下过程:首先,将loadUserInfosFromDB()方法的栈帧压入Java虚拟机栈,然后在Java堆中创建一个UserManager实例对象。
当执行完UserManager对象的load()方法后,loadUserInfosFromDB()方法结束并出栈。随后,垃圾回收器释放掉Java堆内存中的UserManager对象。
因此,在这段代码中,UserManager对象实际上是一个生命周期非常短暂的对象。每次执行loadUserInfosFromDB()方法时,它都会被创建,然后执行其load()方法,可能在1毫秒之后,就被垃圾回收了。
从这段代码可以明显看出,我们代码中创建的大部分对象都是生命周期很短的。实际上,这种短暂生命周期的对象在我们编写的Java代码中占据了很大比例。
3、长期存活的对象
但是我们来看另外一段代码,假如说咱们用下面的这种方式来实现同样的功能:
public class User {
private static UserManager UserManager = new UserManager();
public static void main(String[] args) {
while(true) {
loadUserInfosFromDB();
Thread.sleep(1000);
}
}
private static void loadUserInfosFromDB() {
UserManager.load();
}
}
通过以上代码,我们在User类中创建了一个静态变量UserManager,并在静态块中将其引用初始化为指向一个在Java堆内存中创建的UserManager实例对象。这样,在程序运行时,我们就可以通过User类的静态变量UserManager来访问和操作UserManager实例对象。如下图。

在main()方法中,会在一个while循环里不断调用UserManager对象的load()方法,形成一种周期性运行的模式。
在这个情况下,我们需要思考一下,这个UserManager实例对象会被User的静态变量引用,并且会一直驻留在Java堆内存中,不会被垃圾回收机制清除。
由于这个实例对象需要长期被使用,并且周期性地调用load()方法,因此它成为了一个长时间存在的对象。
类似这种被类的静态变量长期引用的对象,它们需要长时间停留在Java堆内存中,这些对象具有很长的生存周期,它们不会轻易被垃圾回收机制回收,因为它们需要长期存在并持续被使用。
4、JVM分代模型:年轻代和老年代
接下来我们将深入探讨今天的核心主题,即JVM的分代模型,包括年轻代和老年代。
在编写代码时,根据我们选择的编程方式,创建和使用对象的方式会有所不同,从而影响对象的生存周期。为了适应这种差异,JVM将Java堆内存划分为两个主要区域:年轻代和老年代。
年轻代,正如其名,主要用于存储那些在创建和使用之后立即需要回收的对象。这类对象通常是短寿命的,短暂的存在后就会被垃圾回收机制清理掉。
而老年代,则用于存放那些在创建后需要长期存在的对象。这类对象通常具有较长的生命周期,可能会在程序运行过程中一直存在,直到被明确地释放或不再使用。
通过将内存分为年轻代和老年代,JVM能够更有效地管理内存,针对不同类型对象的生命周期进行优化,从而提高程序的性能和稳定性。大家看下图:

比如下面的代码,我们再次来改造一下,再结合图,大家会看的更加的明确一些。
public class User {
private static UserInfoFetcher fetcher = new UserInfoFetcher();
public static void main(String[] args) {
loadUserInfosFromDB();
while(true) {
fetchUserInfosFromRemote();
Thread.sleep(1000);
}
}
private static void loadUserInfosFromDB() {
UserManager UserManager = new UserManager();
UserManager.load();
}
private static void fetchUserInfosFromRemote() {
fetcher.fetch();
}
}
这段代码稍微复杂了点,让我们来解释一下。User中的静态变量“fetcher”引用了一个UserInfoFetcher对象,这个对象需要长时间驻留在内存中以供使用。
在JVM的垃圾回收机制中,对象会在年轻代(Young Generation)中停留一段时间。年轻代是用于存储新创建的对象的区域,其中的对象通常具有较短的生命周期。随着时间的推移,如果对象在年轻代中没有被回收,它们会被移动到老年代(Old Generation)。
因此,UserInfoFetcher对象最初会在年轻代中存在一段时间。然而,由于它被设计为长期驻留在内存中,所以最终会被转移到老年代。老年代是用于存储长时间存活的对象的区域,这些对象在多次垃圾回收之后仍然存在于内存中。
通过将UserInfoFetcher对象放置在老年代中,可以确保它在内存中长期存在,以满足User对长时间驻留对象的需求。大家看下图。

在进入main()方法后,首先会调用loadUserInfosFromDB()方法。这个方法的业务含义是在系统启动时从磁盘加载一次副本数据。当调用这个方法时,它的栈帧会被压入栈中。
在loadUserInfosFromDB()方法内部,创建了一个UserManager对象。这个对象在使用完后会被回收,因此它会被放置在年轻代内存区域中。该对象由栈帧中的局部变量进行引用。此时对应着下图:

然后一旦loadUserInfosFromDB()方法执行完毕了,方法的栈帧就会出栈,对应的年轻代里的UserManager对象也会被回收掉,如下图:

在代码中,有一个while循环将会被执行。这个循环会周期性地调用UserInfoFetcher对象的fetch()方法,以便从远程加载副本数据。
由于UserInfoFetcher对象被User类的静态变量fetcher引用,它将持续存在于老年代中并被长期使用。这意味着UserInfoFetcher对象不会被频繁创建和销毁,从而减少了内存开销和性能消耗。
5、为什么要分成年轻代和老年代?
阅读完这篇文章后,大家将能够清楚地理解什么样的对象是短期存活的,什么样的对象是长期存在的,以及它们是如何分别存在于年轻代和老年代中的。
那么,为什么需要这样区分呢?这是因为这涉及到垃圾回收机制。对于年轻代中的对象,它们的特点是创建之后很快就会被回收,因此需要采用一种垃圾回收算法。而对于老年代中的对象,它们的特点是需要长期存在,因此需要采用另一种垃圾回收算法。这就是为什么需要将对象分为两个区域进行存放的原因。
很多人可能会问,你不是说“UserInfoFetcher”这个长期存在的对象,最初也在年轻代中,后来才会进入老年代吗?那么它到底是什么时候进入老年代的呢?
别急,后面的文章将会对这个问题进行分析。
6、什么是永久代?
在JVM中,有一个区域被称为永久代,它实际上是我们之前提到过的方法区。简单来说,方法区就是用于存储类信息的地方,而这个永久代正是指的就是这个方法区。对于这个概念,现在无需过于深入地理解,因为当我们在后续的讨论中涉及到相关的内容时,会再进行详细的解释。所以,现在你只需要知道,永久代,或者方法区,是用于存储类信息的即可。