4.JVM的运行时数据区
4.4 Java虚拟机堆
一般Java程序中堆内存是空间最大的一块内存区域,创建的对象(成员变量)都位于堆上。
虚拟机栈帧的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享(静态变量存放在方法区之中,这个之后再说)。
4.5 虚拟机堆内存溢出
写一个死循环,然后不断new出新对象放入一个列表中,最后会发现抛出异常OutOfMemory。这就说明堆的内存大小是有上限的,不够用就会出现堆内存溢出。
堆有三个需要关注的值:used、total、max。
used指的是当前已经使用的堆内存,total是Java虚拟机已经分配的可用堆内存,max是Java虚拟机可以分配的最大堆内存。随着程序运行,当used使用的内存已经达到total的大小时,JVM会扩大total的大小。但是不是total到了max才会出现内存溢出,事实上,total还没有达到max就会出现内存溢出。
通过arthas可以看到堆内存的实时情况。可以使用dashboard命令看到。当然,也可以使用memory这个命令只看内存的情况。
如何对堆的total和max进行设置呢?可以通过两个参数对堆的total和max进行设置。
Java服务端程序开发时,建议将-Xmx
和-Xms
设置成相同的值,这样在程序启动之后,total就是max,不会出现内存达到total然后向JVM请求扩张total的情况,减少了时间开销。-Xmx
与具体运行环境有关,之后会讲到。
4.6 Java虚拟机方法区
方法区是存放基础信息的位置,线程共享,主要包括三部分内容:类的元信息(保存了所有类的基本信息),运行时常量池(保存了字节码文件中的常量池内容),字符串常量池(保存了字符串常量)。
- 类的元信息,一般称为InstanceKlass对象。这个对象在类的加载阶段创建完成。
- 运行时常量池。字节码文件中,一般都是通过编号查表的方式找到常量,所以在字节码文件中的常量池称为静态常量池。当静态常量池被类加载器加载到内存之中后,可以通过内存地址快速定位到常量池之中的内容。这种常量池称为运行时常量池。
- 字符串常量池,存储在代码中定义的常量字符串内容。
方法区在jdk8之前,是在堆中的永久代区域,这样设计不是很合理,因为耦合了。所以在jdk8及以后,方法区被设计放在直接内存的元空间区域。
运行时常量池和字符串常量池之间有什么关系?要明确方法区是一个虚拟概念,并不是一块内存区域,所以方法区中的不同部分可能有些分布在堆中(字符串常量池,静态变量),有些分布在直接内存中(类元信息、运行时常量池)。
要注意的是:JDK7及以后的版本中,静态变量存放在堆中的Class对象中,已经脱离了永久代。
总结一下:jdk8之后,堆中存放的内容有字符串常量池、静态变量、对象。
来做两道字符串常量池的题目:
下面这道题非常神奇。原因是,jdk6之中,intern()方法直接将对象中的字符串复制一份,放入字符串常量池之中,而jdk8,为了节省内存,intern()方法只是将对象中的字符串的引用放入字符串常量池中。所以,存放着think123的对象s1使用了intern()方法后,其实是将s1的引用放入了字符串常量池,这样s1.intern()==s1自然就是true了。那为何存放着java的s2与s2.intern()不相同呢?因为java在程序启动,核心类加载的时候就已经被放入字符串常量池之中了。s2.intern()发现字符串常量池之中已经有java,自然返回字符串常量池中java的引用,而不会将s2的引用放入字符串常量池之中。所以jdk8的结果是true和false。
4.7 虚拟机方法区内存溢出
如果类的信息太多,方法区内存也会出现溢出。写一个死循环,然后用框架去生成不同类的字节码,加载到内存中。经过测试,会发现在jdk1.7中,类的数量11万左右,会抛出异常OutOfMemoryError:PermGen Space
,抛出的异常跟堆内存溢出的异常一样,这也可以侧面说明jdk1.7确实将方法区存放在堆之中。在jdk1.8之中,类的数量达到数百万个,都没有出现内存溢出。这也说明了jdk1.8及之后,与jdk1.7及之前,确实对方法区采用了不同的设计。
4.8 直接内存
直接内存并不在《Java虚拟机规范》中被定义,所以并不属于Java虚拟机运行时内存。
直接内存的出现主要是为了解决以下问题:
- Java堆中的对象如果不再使用,需要使用垃圾回收器(之后会提到)进行回收,但是垃圾回收器回收堆中的对象,会影响性能。
- IO操作比如读文件需要先把文件读入直接内存,然后再把数据复制到Java堆中,现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少数据复制的开销。写文件也是一样,没有必要从Java堆中将数据复制到直接内存,而是从直接内存中将数据写入文件中即可。
总结一下就是:为了减少NIO使用过程中对用户的影响以及提升读写文件的效率。当然,从上面也可以看到,它在jdk8之后可以保存方法区中的数据。
4.9 运行时数据区域总结
程序计数器、虚拟机栈、本地方法栈是线程不共享的。每一个线程都有自己的程序计数器、虚拟机栈以及本地方法栈。
堆、方法区是线程共享的。不同线程共享堆和方法区。因为静态变量是在堆中的,所以静态变量是线程共享的。