本文讲的虚拟机是指Hotspot VM。Hotspot VM是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
运行时数据分区
运行时数据分区可以分为程序计数器、虚拟机栈、本地方法栈、堆和方法区(Java8之后移出了方法区,增加了元空间)。下面将分别阐述不同空间。
程序计数器:程序计数器是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。线程恢复和异常处理等功能都是依赖程序计算器完成的。程序计数器是线程私有的。
在任意时刻,一个内核都只会执行一个线程中的指令,多线程运行是通过线程切换并分配处理器的执行时间来实现的,为了线程切换后线程能从正确的位置执行,所以程序计数器是线程私有的内存。
在执行Native方法,程序计数器的值为空。
虚拟机栈:方法在运行是会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接。方法出口等信息。这个栈帧在虚拟机栈中入栈到出栈对应了一个方法从调用到完成的过程。也是线程私有的内存。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。
-Xss可以设置每个线程的虚拟机栈大小。在相同的物理内存下,这个值越小能生成更多的线程。不过操作系统对一个进程内的线程数也是有限制的,一般最高为3000~5000。如果线程数过多会出现栈内存溢出,此时可以通过两种方法解决
-
减少最大堆的值,这样相应的虚拟机栈的空间会增大;
-
减少-Xss的值,这样能使得可以创建的线程数增多。
当方法中出现了死循环可能出现虚拟机栈内存溢出,此时也会出现cpu飙高。
本地方法栈:本地方法栈与虚拟机栈的功能是一致的,不过本地方法栈是为虚拟机执行Native方法创建的。
堆:java堆存放了所有对象实例,数组也是在堆上分配的。这块内存是线程共享的。在进行垃圾回收时,也将这块内存分为新生代和老年代。
可以通过-Xms(初始堆内存大小)和-Xms(最大堆内存大小)来调节这个区域内存的大小。小编一般将这两个值设置为一样的大小。
对于堆内存的分析,小编推荐MAT(Eclipse Memory Analyzer)。在使用MAT进行分析前先需要dump出来的堆转储快照,下面小编就推荐一些日常设置的参数来监控GC情况。
-
-Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps. (用于监控GC日志);
-
-XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=100.(用于设置GC算法)
-
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_BASE/logs(OOM错误时dump出堆快照)
-
-XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh. (OOM错误时执行的shell脚本)
方法区(java8之后的元空间):这一部分内存用于存放Class和Meta的信息,Class在被加载的时候被放入方法区。Class文件中有类的版本、字段、方法、接口和常量池(存放了编译期生成的各种字面量和符号引用)。
同时运行时常量池也是方法区的一部分,运行时常量池是动态的,运行期间新的常量也会放入池中。String类的intern()方法就利用了这个特性。
‑XX:MaxPermSize参数可以设置方法区的最大大小,-XX:PermSize 方法区的初始大小。
在java8之后这个区域用元空间进行了代替,元空间是使用的本地内存空间来存储的,元空间的内存是由专门的元空间虚拟机来管理的。
元空间虚拟机给每个类加载器分配一个内存块的列表,只进行线性分配。块的大小取决于类加载器的类型, sun/反射/代理对应的类加载器的块会小一些。
下面用图来表示这次改动:
![](https://i-blog.csdnimg.cn/blog_migrate/84142ac480da17b5b90638b38599bc06.png)
可以利用-XX:MetaspaceSize(达到这个值会进行回收这个区域)和-XX:MaxMetaspaceSize参数来调解元空间的大小。
这里需要注意的是反射、动态代理、字节码技术都能在运行时动态生成类。例如spring会使用CGLib来增强类,越多的类被增强,需要的空间越多。类似的还有大量JSP的应用、基于OSGi的应用。
利用Jconsole可以检查应用类加载的情况,如果类加载数量较大就可以考虑优化代码。
![](https://i-blog.csdnimg.cn/blog_migrate/50dbe484075ef289e477d42c8161969a.png)
直接内存:直接内存不是java虚拟机运行时数据区的一部分,不过这个区域也会出现OutOfMemoryError的异常。直接内存的分配不是受Java堆大小的限制,而是受本机总内存的限制以及处理器寻址空间的限制。当我们设置-Xmx最大堆内存的大小时,要注意各个内存区域的和不能大于物理内存的大小。
NIO类就是使用Native函数库直接分配堆外内存,然后使用存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样做的好处是避免了Java堆和Native堆中来回复制数据,一定程度上提高了性能。
直接内存可以通过参数-XX:MaxDirectMemorySize来指定大小,默认与-Xmx的大小一致。
由直接内存导致的内存溢出有一个特征就是Dump出来的文件很小。如果发生OOM错误,但是Dump出来的文件很小,此时就可以考虑检查一下有没有直接内存的溢出。
直接指针访问
下面小编用一幅图来展示在Hotspot虚拟机中对象的访问方式,此方式也被称作直接指针访问。
![](https://i-blog.csdnimg.cn/blog_migrate/88d8999ea3180005754ac002c6343790.jpeg)
今天关于虚拟机相关的知识只大概介绍了虚拟机的内存模型,对于虚拟机还有很多需要掌握的知识,例如垃圾回收算法、字节码技术、类加载等等。这些专题可以在之后的文章中进行阐述。