[推前浪 -1] Java虚拟机内存模型

Posted IT面试填坑小分队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[推前浪 -1] Java虚拟机内存模型相关的知识,希望对你有一定的参考价值。

前言

这篇文章是我们的一个学弟的博客,内容是关于JVM调优的,因为篇幅较长,所以会采用连载的方式。
如果有迫不及待的先看全文的小伙伴,可以:

博客园,搜索:像风一样

看着学弟们高昂的斗志,唉,我们即将被拍死在沙滩上~

正文

Java虚拟机内存模型

JVM虚拟机将内存数据分为:

  • 程序计数器

  • 虚拟机栈

  • 本地方法栈

  • Java堆

  • 方法区等部分。

程序计数器用于存放下一条运行的指令;虚拟机栈和本地方法栈用于存放函数调用堆栈信息;Java堆用于存放Java程序运行时所需的对象等数据;方法区用于存放程序的类元数据信息。

1、程序计数器

每一个线程都必须有一个独立的程序计数器,用于记录下一条要运行的指令。各个线程之间的计数器互不影响,独立工作;是一块线程私有的内存空间。

2、Java虚拟机栈

Java虚拟机栈也是线程的私有空间,它和Java线程在同一时间创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

在Java虚拟机规范中,定义了两种异常与栈空间有关:StackOverflowError 和 OutOfMemoryError。

在 HotSpot 虚拟机中,可以使用 -Xss 参数(如:-Xss1M)来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

如果方法调用时,方法的参数和局部变量相对较多,那么栈帧中的局部变量表就会比较大,栈帧会膨胀以满足方法调用所需传递的信息。因此,单个方法调用所需的栈空间大小也会比较多。

[推前浪 -1] Java虚拟机内存模型

2.1、局部变量

如果一个局部变量被保存在局部变量表中,那么GC根就能引用到这个局部变量所指向的内存空间,从而在GC时,无法回收这部分空间。这里有一个非常简单的示例来说明局部变量对GC的影响。

public void test(){{
        // 1024*60 KB = 60 MB
        byte[] b = new byte[1024 * 1024 * 60]; 
    }
    System.gc();
    System.out.println("gc over");
}

在运行Java程序时设置参数-XX:+PrintGC打印GC日志,运行结果:

[GC (System.gc())  64775K->62176K(125952K), 0.0011984 secs]
[Full GC (System.gc())  62176K->62097K(125952K), 0.0063403 secs]
gc over

很明显,显示的Full GC并没有能释放它所占用的堆空间。这是因为,变量b仍在该栈帧的局部变量表中。因此GC可以引用到该内存块,阻碍了回收过程。

假设在该变量失效后,在这个函数体内,又未能有定义足够多的局部变量来复用该变量所占的字,那么,在整个函数体中,这块内存区域是不会被回收的。在这种环境下,手工对要释放的变量赋值为null,是一种有效的做法。

public void test(){{
        byte[] b = new byte[1024 * 1024 * 5]; // 5MB
        b = null;
    }
    System.gc();
    System.out.println("gc over");
}

运行结果:

[GC (Allocation Failure)  1513K->616K(7680K), 0.0011590 secs]
[GC (System.gc())  6191K->5880K(7680K), 0.0011550 secs]
[Full GC (System.gc())  5880K->651K(7680K), 0.0095708 secs]
gc over

在实际开发中,遇到上述情况的可能性并不大。因为在大多数情况下,如果后续仍然需要进行大量的操作,那么极有可能会申明新的局部变量,从而复用变量b的字,使b占的内存空间可以被GC回收。

public void test(){{
        byte[] b = new byte[1024 * 1024 * 5];
    }
    int a = 0;
    System.gc();
    System.out.println("gc over");
}

运行结果:

[GC (Allocation Failure)  1530K->656K(7680K), 0.0011337 secs]
[GC (System.gc())  6189K->5824K(7680K), 0.0010571 secs]
[Full GC (System.gc())  5824K->651K(7680K), 0.0077965 secs]
gc over

很明显,变量b由于变量a的作用被回收了。

3、本地方法栈

本地方法栈和Java虚拟机栈的功能很相似,也属于线程的私有空间。Java虚拟机栈用于管理Java函数的调用,而本地方法栈用于管理本地方法的调用。本地方法并不是用Java实现的,而是使用C实现的。在SUN的Hot Spot虚拟机中,不区分本地方法栈和虚拟机栈。因此,和虚拟机栈一样,他也会抛出 StackOverflowError 和 OutOfMemoryError。

4、Java堆

Java堆可以说是Java运行时内存中最为重要的部分,几乎所有的对象和数组都是在堆中分配空间的。Java堆分为新生代和老年代两个部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。

新生代又可进一步细分为:

  • eden

  • survivor space0(s0 或者 from space)

  • survivor space1(s1或者to space)

eden:对象的出生地,大部分对象刚刚建立时,通常会存放在这里。s0 和 s1 为 survivor(幸存者)空间,存放其中的对象至少经历过一次垃圾回收,并得以幸存。如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代(tenured)。

[推前浪 -1] Java虚拟机内存模型

使用JVM参数-XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M

运行这段代码:

public void test2(){
     byte[] byte1 = new byte[1024*1024/2];
     byte[] byte2 = new byte[1024*1024*8];
     byte2 = null;
     byte2 = new byte[1024*1024*8];
     System.gc(); 
}

运行结果:

[GC (System.gc()) [PSYoungGen: 11004K->1256K(18432K)] 19196K->9456K(38912K), 0.0013429 secs][Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1256K->0K(18432K)] [ParOldGen: 8200K->9361K(20480K)] 9456K->9361K(38912K), [Metaspace: 3478K->3478K(1056768K)], 0.0072324 secs][Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen      total 18432K, used 164K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
 eden space 16384K, 1% used [0x00000000fec00000,0x00000000fec290d0,0x00000000ffc00000)
 from space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
 to   space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
ParOldGen       total 20480K, used 9361K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
 object space 20480K, 45% used [0x00000000fd800000,0x00000000fe124740,0x00000000fec00000)
Metaspace       used 3485K, capacity 4498K, committed 4864K, reserved 1056768K
 class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

可以看到,在经过Full GC之后,新生代空间全部被清空,未被回收的对象全部被移入老年代。

5、方法区

方法区也是 JVM 内存区中非常重要的一块内存区域,与堆空间类似,它也是被 JVM 中所有的线程共享的。方法区主要保存的信息是类的元数据。

方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。

  • 1、类型信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表;

  • 2、常量池包括这个类方法、域等信息所引用的常量信息;

  • 3、域信息包括域名称、域类型和域修饰符;

  • 4、方法信息包括方法名称名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法帧栈的局部变量区大小以及异常表。

总之,方法区保存的信息,大部分来自于 class 文件,是 Java 应用程序运行必不可少的重要数据。

方法区是 JVM 的一种规范。 在Host Spot虚拟机的实现中,方法区也被称为永久区,是一块独立于 Java 堆的内存空间。虽然叫永久区,但是永久区中的对象同样可以被 GC 回收的(注:在 Java8 中,永久区已经被 Metaspace 元空间取而代之。相应的,JVM参数 PermSize 和 MaxPermSize 被 MetaSpaceSize 和 MaxMetaSpaceSize 取代,使用无效)。对永久区 GC 的回收,通常主要从两个方面分析:一是 GC 对永久区常量池的回收;二是永久区对类元数据的回收。

使用JVM参数-XX:+PrintGCDetails -XX:MetaspaceSize=4M -XX:MaxMetaspaceSize=5M

运行这段代码:

@Test
public void test4() {
    for (int i=0; i<Integer.MAX_VALUE;i++){
        // 加入常量池并返回
        String s = String.valueOf(i).intern();  
    }
}

运行结果:

[GC (Metadata GC Threshold) [PSYoungGen: 1331K->696K(38400K)] 1331K->696K(125952K), 0.0011365 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Metadata GC Threshold) [PSYoungGen: 696K->0K(38400K)] [ParOldGen: 0K->550K(62976K)] 696K->550K(101376K), [Metaspace: 2875K->2875K(1056768K)], 0.0062965 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Metadata GC Threshold) [PSYoungGen: 4672K->640K(38400K)] 5222K->1198K(101376K), 0.0016105 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Metadata GC Threshold) [PSYoungGen: 640K->0K(38400K)] [ParOldGen: 558K->1131K(118272K)] 1198K->1131K(156672K), [Metaspace: 4647K->4647K(1056768K)], 0.0139785 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

可以看到,持久代已经饱和,并抛出 “java.lang.OutOfMemoryError: Metaspace” 异常显示持久代溢出。

Full GC 在这种情况下不能回收类的元数据。

事实上,如果虚拟机确认该类的所有实例已经被回收,并且加载该类的 ClassLoader 已经被回收,GC 就有可能回收该类型。

图片来自互联网

尾声

篇幅原因,本篇文章暂时只摘自学弟的《深入理解JAVA虚拟机(内存模型+GC算法+JVM调优)》中Java虚拟机内存模型,余下部分会不定期的更新,如果想提前了解的,可以点击原文阅读~

也欢迎小伙伴的,把自己的学习成果分享出来~欢迎后台/评论区 留下自己的文章,笔记等~



以上是关于[推前浪 -1] Java虚拟机内存模型的主要内容,如果未能解决你的问题,请参考以下文章

图解Java虚拟机内存模型

图解Java虚拟机内存模型

小白都能看得懂的java虚拟机内存模型

Jvm内存模型

内存模型

深入理解Java虚拟机- 学习笔记 - Java内存模型与线程