1、背景引入

我们从下面这段代码开始今天的内容:

 

有没有很眼熟呀,跟前面我们的代码差不多。只是这里稍微调整了一下,在main() 方法中,会循环调用 loadReplicasFormDisk() 方法。

我们来用内存分配的角度来用图示来描述一下上面的代码怎么运行的。

首先,一旦 main() 方法执行,那么就会把 main() 方法的栈桢压入 main 线程的Java 虚拟机栈中,如图:

 

接着,进入循环体中,会调用 loadReplicasFormDisk() 方法,就会把 loadReplicasFormDisk() 方法的栈桢也压入到 main 线程的Java 虚拟机里面去,然后图示就像下面这样:

 

然后,在 loadReplicasFormDisk()方法中,每次都会去堆里面创建 一个 ReplicaManger 对象的实例,然后通过 loadReplicasFormDisk() 栈桢中的局部变量表中的 “replicaManager” 变量去引用堆中创建的那个 ReplicaManger 对象的实例,看下图:

 

2、大部分对象存活时间都是很短的

上图中,处于 Java 堆里面的那个 ReplicaManager 对象的实例其实就是一个短暂存活的对象。

为什么这么说呢?我们继续分析上面的代码。

大家可以再看一下代码。你会发现,我们是在 main 方法中,用循环的方式去调用 loadReplicasFormDisk() 方法,loadReplicasFormDisk() 里面去创建的。

那么这里每一次循环结束,loadReplicasFormDisk() 方法的栈桢就从 main 线程的Java 虚拟机栈中出栈了。也就意味着不再有变量再去引用处于 Java 堆中的那个 ReplicaManager 对象的实例了。

 

根据我们上周的垃圾回收的知识,那么这里没有被引用的这个 处于Java 堆中的 ReplicaManager 对象实例,就会很快被垃圾回收掉的。

 

下一次循环的时候,继续走上面的流程,把 loadReplicasFormDisk() 方法的栈桢加入到 main 线程的 Java虚拟机栈中,然后在Java 堆中创建一个ReplicaManager 对象,并且让 loadReplicasFormDisk() 栈桢中的局部变量 replicaManager来引用这个对象。

当 replicaManager.load() 执行完成后,loadReplicasFormDisk() 方法结束,loadReplicasFormDisk 虚拟机栈出栈。后面的每次循环都是这个操作。

所以每次在 Java 堆中创建的 ReplicaManager 对象的实例存活周期其实都是很短的。调用 loadReplicasFormDisk() 方法的时候被创建,执行 replicaManager.load() 方法后被回收。

大家也可以想想自己平常写的代码是不是大多数时候都是这样去写的。

3、少数部分对象是可以长期存活的

我们调整一下上面的代码,把创建 ReplicaManager 对象的操作放到类上面来作为Kafka类的一个 ,loadReplicasFormDisk 中只做 load() 操作。看下面代码:

 

上面的代码给 Kafka 这个类定义了一个叫 “replicaManager” 的静态变量,这个变量处于 JVM 的方法区中。然后去引用了处于Java 堆里面的 ReplicaManager 对象实例,看起来图示就是下面这样子:

 

此时在main 方法中,就是周期性的调用 ReplicaManager 对象的 load() 方法。

现在这个处于 Java 堆中的 ReplicaManager 对象,是被 Kafka 的静态变量 replicaManager 所引用的,他会长期驻留在哪中,是不会被垃圾回收掉的。

因为它长期驻留在内存中,还周期性的被调用,所以它也就成为了一个长时间存活的对象了。

4、JVM 的分代模型:年轻代和老年代

大家通过上面的两段代码可以发现,我们不同代码的编写方式,决定了这些对象的生存周期。

所以JVM 将Java 堆内存划分为了 两个区域,一个是年轻代,一个是老年代。

年轻代就是存放那种使用完就立马回收的对象。

而老年代则用来存放那些长期驻留在内存中的对象。

 

用上面的两段代码融合来看一下整个流程:

 

首先 Kafka 中的静态变量 fetcher 引用了 ReplicaFetcher 对象,这是长期需要驻留在内存中的,会放到老年代中。这里其实还是会短暂的先放到年轻代里面的,知识最终会放到老年代区,至于为什么,后面会细讲,这里你只需要认为它会被放到老年代中去就行了。

 

接着开始执行 main 方法,进去后会先执行loadReplicasFormDisk() 方法,此时会创建一个 loadReplicasFormDisk() 方法的栈桢压入 main 线程的 Java 虚拟机栈中。

loadReplicasFormDisk() 方法中会使用一个局部变量表中的 “replicaManager” 变量去指向在Java 堆中年轻代里创建的一个 ReplicaManger 对象。因为 loadReplicasFormDisk() 方法调用完成以后,就没有变量在指向 这个 ReplicaManger 对象了。所以丢在年轻代里面的,如下图:

 

当 loadReplicasFormDisk() 执行完毕以后,loadReplicasFormDisk()方法的栈帧就会出栈,对应年轻代中的 ReplicaManger 对象也会被回收掉,回收后入下图:

 

接着会执行 while 循环代码,会周期性的去调用 ReplicaFetcher的 fetch() 方法。

但是 ReplicaFetcher 这个对象是被 kafka 的静态变量 fathcer 给引用了,所以他会存在于 老年代中,持续被使用。

5、为什么要分成年轻代和老年代

从上面的分析,我们可以看到哪些代码会在年轻代?存活时间短的。哪些代码对象会在老年代?存活时间长的。

但是为什么要这么分呢?

其实这里还跟垃圾回收的机制有关系,对于存活时间短的和长期存活的对象,回收的算法是不一致的。

关于垃圾回收算***在第三周中详细讲到的。这里我们就关注 JVM 的划分就好。

6、永久代

其实我们这里说的 JVM 中的永久代,就是上面图中的方法区。暂时可以任务它就是来存放类的信息的。

7、最后

今天我们结合了实际的代码例子来讲述了 JVM的内存模型划分,也通过图示说明了哪些对象是放到哪块区域的,大家可以好好理解一下。

后面一篇会带来实战第二周的学习总结:《你的Java对象在JVM中是怎么分配和流转的》,将说明对象第一次是分配在哪里?什么时候会触发垃圾回收,新生代的回收,老年代的回收?等等...