linux java 进程虚拟内存分析_局部变量

前言

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

博客园,搜索:像风一样

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

正文

Java虚拟机内存模型

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

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

1、程序计数器

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

如果当前程序正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址,如果当前线程正在执行一个Native方法,则程序计数器为空。

2、Java虚拟机栈

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

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

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

虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态连接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作。相应的,方法的返回则表示栈帧的出栈操作。

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

linux java 进程虚拟内存分析_JVM_02

2.1、局部变量

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

linux java 进程虚拟内存分析_java虚拟机的内存模型_03

在运行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,是一种有效的做法。

linux java 进程虚拟内存分析_局部变量_04

运行结果:

[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回收。

linux java 进程虚拟内存分析_局部变量_05

运行结果:

[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堆分为新生代和老年代两个部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。

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

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

linux java 进程虚拟内存分析_linux java 进程虚拟内存分析_06

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

运行这段代码:

linux java 进程虚拟内存分析_JVM_07

运行结果:

[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] HeapPSYoungGen 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

运行这段代码:

linux java 进程虚拟内存分析_局部变量_08

运行结果:

[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 就有可能回收该类型。

linux java 进程虚拟内存分析_java虚拟机的内存模型_09


尾声

篇幅原因,本篇文章暂时只摘自学弟的《深入理解JAVA虚拟机(内存模型+GC算法+JVM调优)》中Java虚拟机内存模型,余下部分会不定期的更新