JVM内存的一个分代模型:年轻代、老年代、永久代。
注:在1.8以后,永久代被移除,转而用元空间代替。这里主要是介绍一下概念。
1. 背景引入
大家现在应该都知道一点,那就是我们在代码里创建的对象,都会进入到Java堆内存中,比如下面的代码:
1 package com.test.day11; 2 3 public class TestJvm { 4 public static void main(String[] args) throws InterruptedException { 5 while (true) { 6 getString(); 7 Thread.sleep(1000); 8 } 9 } 10 private static void getString(){ 11 String string = new String("hello world"); 12 System.out.println(string); 13 } 14 }
这段代码,在main()方法里,会周期新的执行getString()方法,加载副本数据。
首先一旦执行main()方法,那么就会把main()方法的栈帧压入main线程的Java虚拟机栈 如下图。
然后每次在while循环里,调用getString()方法,就会把getString()方法的栈帧压入自己的 Java虚拟机栈。
接着在执行getString()方法的时候,会在Java堆内存里会创建一个String对象实例 而且getString()方法的栈帧里会有“string”局部变量去引用Java堆内存里的 String对象实例。
2. 大部分对象都是存活周期极短的
现在有一个问题,在上面代码中,那个String对象,实际上属于短暂存活的这么一个对象
大家可以观察一下,在getString()方法中创建这个对象,然后执行String对象的load()方法, 然后执行完毕之后,getString()方法就会结束。
一旦方法结束,那么getString()方法的栈帧就会出栈,如下图。
然后接着上篇文章已经说过,此时一旦没人引用这个String对象了,就会被JVM的垃圾回收线程给回收掉,释放内存空间,如下图。
然后在main()方法的while循环里,下一次循环再次执行getString()方法的时候,又会走一遍上面那个过程,把 getString()方法的栈帧压入Java虚拟机栈,然后构造一个String实例对象放在Java堆里。
一旦执行完String对象的load()方法之后,getString()方法又会结束,再次出栈,然后垃圾回 收释放掉Java堆内存里的String对象。
所以其实这个String对象,在上面的代码中,是一个存活周期极为短暂的对象。
可能每次执行getString()方法的时候,被创建出来,然后执行他的load()方法,接着可能1毫秒之后,就 被垃圾回收掉了。
所以从这段代码就可以明显看出来,大部分在我们代码里创建的对象,其实都是存活周期很短的。这种对象,其实在 我们写的Java代码中,占到绝大部分的比例
3. 少数对象是长期存活的
但是我们来看另外一段代码,假如说咱们用下面的这种方式来实现同样的功能:
1 package com.test.day11; 2 3 public class TestJvm { 4 private static String string = new String("hello world"); 5 6 public static void main(String[] args) throws InterruptedException { 7 while (true) { 8 getString(); 9 Thread.sleep(1000); 10 } 11 } 12 private static void getString(){ 13 System.out.println(string); 14 } 15 }
上面那段代码的意思,就是给TestJvm这个类定义一个静态变量,也就是“String”,这个TestJvm类是在JVM的方法区里的。
然后让“String”引用了一个在Java堆内存里创建的String实例对象,如下图。
接着在main()方法中,就会在一个while循环里,不停的调用println()方法,做成一个周期性运行的模式。
这个时候,我们就要来思考一下,这个String实例对象,他是会一直被TestJvm的静态变量引用的,然后会 一 直驻留在Java堆内存里,是不会被垃圾回收掉的。
因为这个实例对象他需要长期被使用,周期性的被调用load()方法,所以他就成为了一个长时间存在的对象。
那么类似这种被类的静态变量长期引用的对象,他需要长期停留在Java堆内存里,这这种对象就是生存周期很长的对 象,他是轻易不会被垃圾回收的,他需要长期存在,不停的去使用他。
4. JVM分代模型:年轻代和老年代
现在大家已经看到,其实根据你写代码方式的不同,采用不同的方式来创建和使用对象,其实对象的生存周期是不同 的。
所以JVM将Java堆内存划分为了两个区域,一个是年轻代,一个是老年代。
其中年轻代,顾名思义,就是把第一种代码示例中的那种,创建和使用完之后立马就要回收的对象放在里面
然后老年代呢,就是把第二种代码示例中的那种,创建之后需要一直长期存在的对象放在里面,大家看下图:
代码改造如下:
1 package com.test.day11; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 public class TestJvm { 7 private static Map<String, String> mp = new HashMap<>(); 8 9 public static void main(String[] args) throws InterruptedException { 10 getString(); 11 while (true) { 12 printMap(); 13 Thread.sleep(1000); 14 } 15 } 16 17 private static void getString() { 18 String string = new String("hello world"); 19 System.out.println(string); 20 } 21 22 private static void printMap() { 23 System.out.println(mp.size()); 24 } 25 }
上面那段代码稍微复杂了点,解释一下 TestJvm的静态变量“mp”引用了HashMap对象,这是长期需要驻留在内存里使用的。
这个对象会在年轻代里停留一会儿,但是最终会进入老年代,大家看下图。
进入main() 方法之后,会先调用getString0方法,业务含义是系统启动就从磁盘加载一次副本数据,这个方法的栈帧会入栈
然后在这个方法里面创建了一个String对象,这个对象他是用完就会回收,所以是会放在年轻代里的,由栈帧里的局部变量来引用
此时对应着下图:
然后一旦getString()方法执行完毕了,方法的栈帧就会出栈,对应的年轻代里的String对象也会被回收掉,如下图:
但是接着会执行一段while循环代码,他会周期性的调用Replicaprinter的print()方法,去从远程加载副本数据。
所以HashMap这个对象因为被TestJvm类的静态变量mp给引用了,所以他会长期存在于老年代里的,持续被使用。
5、为什么要分成年轻代和老年代?
- 因为这跟垃圾回收有关,
- 对于年轻代里的对象,他们的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算 法 。
- 对于老年代里的对象,他们的特点是需要长期存在,所以需要另外一种垃圾回收算法。
- 所以需要分成两个区域来放不同的对象
6、什么是永久代?
很简单,JVM里的永久代其实就是之前说的方法区。
上面那个图里的方法区,其实就是所谓的永久代,你可以认为永久代就是放一些类信息的。
方法区内会不会进行垃圾回收?
在以下几种情况下,方法区里的类会被回收。
- 首先该类的所有实例对象都已经从Java堆内存里被回收
- 其次加载这个类的ClassLoader已经被回收
- 最后,对该类的Class对象没有任何引用 满足上面三个条件就可以回收该类了。