给大家分享下JVM的概念,根据JVM规范,JVM 内存共分为程序计数器(Program Counter Register)、Java 虚拟机栈(Java Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、堆(Heap space)、方法区(Method Area)五个部分。
本篇重点介绍堆、方法区(非堆)内存,解决常见的JVM错误,具体的JVM逻辑内存模型可参考“Tomcat性能调优:JVM逻辑内存模型”。
Java堆和非堆的概念
Java 虚拟机具有一个堆(Heap),堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。堆的大小可以固定,也可以扩大和缩小。堆的内存不需要是连续空间。
在JVM中堆之外的内存称为非堆内存(Non-heap memory)。
Java 虚拟机具有一个由所有线程共享的方法区。方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法和构造方法的代码。它是在 Java 虚拟机启动时创建的。
方法区在逻辑上属于堆,但 Java 虚拟机实现可以选择不对其进行回收或压缩。与堆类似,方法区的大小可以固定,也可以扩大和缩小。方法区的内存不需要是连续空间。
除了方法区外,Java 虚拟机实现可能需要用于内部处理或优化的内存,这种内存也是非堆内存。例如,JIT 编译器需要内存来存储从 Java 虚拟机代码转换而来的本机代码,从而获得高性能。
一、JVM中堆和方法区的概念说明
Java 堆
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(GarbageCollected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer,TLAB)。
堆大小 = 新生代 + 老年代。默认的,新生代(YoungGen)与老年代(Old)的比例的值为1:2(该值可以通过参数 –XX:NewRatio 来指定),即:新生代( YoungGen)= 1/3的堆空间大小。老年代(OldGen)= 2/3 的堆空间大小。
新生代/年轻代(YoungGen)(1/3)
新生代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。新生代(Young)被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 From 和To。
新生代 = Eden、From Survivor、To Survivor。默认的,Eden:From:To=8:1:1(可以通过参数–XX:SurvivorRatio 来设定),即: Eden = 8/10 的新生代空间大小,From=To=1/10的新生代空间大小。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
老年代(OldGen)(2/3)
老年代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
方法区(Method Area)
方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆(Non-Heap)”,方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
永久代(PermGen)
永久代是hotspot虚拟机,也就是我们使用的java虚拟机的特有的概念,他不属于堆内存,是方法区的一种实现,各大厂商对方法区有各自的实现。永久代存放jvm运行时,需要的类,包含java库的类和方法,在触发full gc的情况下,永久代也会被进行垃圾回收。永久代的内存溢出也就是PermGen Space。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 Native Memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。
永久代与方法区的关系
涉及到内存模型时,往往会提到永久代,那么它和方法区又是什么关系呢?《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。在1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收。
元空间
元空间是metaspace,在jdk1.8的时候,jvm移除了永久代的概念,元空间也是对java虚拟机的方法区的一种实现。元空间与永久代最大的区别在于,元空间不在虚拟机中,使用本地内存。通过配置如下参数可以更改元空间的大小。
-XX:MetaspaceSize:初始空间的大小。达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
永久代的回收会随着full gc进行移动,消耗性能。每种类型的垃圾回收都需要特殊处理元数据。将元数据剥离出来,简化了垃圾收集,提高了效率。
二、JVM内存设置说明
主要解决常见的JVM内存溢出问题,配置建议详见“Tomcat性能调优:虚拟内存JVM设置”。
常见JVM错误
堆溢出:
java.lang.OutOfMemoryError:Java heap spcace
栈溢出:
java.lang.StackOverflowError
方法区溢出:
java.lang.OutOfMemoryError:PermGen space
例子:
-Xms1024m -Xmx1024m -XX:PermSize=512m -XX:MaxPermSize=512m
(JVM内存=堆内存+非堆内存)<系统可用内存(一般来说32位Windows系统下为1.5G-2G,32位Linux系统下为2G-3G,64位系统无限制)
-Xms 初始堆大小。如:-Xms256m,默认为物理内存1/64。
-Xmx 最大堆大小。如:-Xmx512m,默认为物理内存1/4。
-XX:PermSize 非堆内存,永久代(方法区)的初始大小,默认为物理内存1/64。
-XX:MaxPermSize 非堆内存,永久代(方法区)的最大值,默认为物理内存1/4。
堆内存配置参数:-Xms 、-Xmx、-XX:newSize、-XX:MaxnewSize、-Xmn
- 1、-Xms:表示java虚拟机堆区内存初始内存分配的大小,通常为操作系统可用内存的1/64大小即可,但仍需按照实际情况进行分配。有可能真的按照这样的一个规则分配时,设计出的软件还没有能够运行得起来就挂了。
- 2、-Xmx:表示java虚拟机堆区内存可被分配的最大上限,通常为操作系统可用内存的1/4大小。但是开发过程中,通常会将 -Xms 与 -Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。
一般来讲对于堆区的内存分配只需要对上述两个参数进行合理配置即可,但是如果想要进行更加精细的分配还可以对堆区内存进一步的细化,那就要用到下面的三个参数了-XX:newSize、-X:MaxnewSize、-Xmn。当然这源于对堆区的进一步细化分:新生代、中生代、老生代。java中每新new一个对象所占用的内存空间就是新生代的空间,当java垃圾回收机制对堆区进行资源回收后,那些新生代中没有被回收的资源将被转移到中生代,中生代的被转移到老生代。而接下来要讲述的三个参数是用来控制新生代内存大小的。
1、-XX:newSize:表示新生代初始内存的大小,应该小于 -Xms的值;
2、-XX:MaxnewSize:表示新生代可被分配的内存的最大上限;当然这个值应该小于 -Xmx的值;
3、-Xmn:新生代。默认为 Xmx 的1/3。新生代 = Eden + 2 个 Survivor
空间。至于这个参数则是对-XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,那么-XX:newSize=
-XX:MaxnewSize = -Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的。 -XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则为1:2,则新生代占整个堆空间的1/3,老年代占2/3。–XX:NewRatio=3,则为1:3。
-XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/104、-Xss:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M。
-
5、其他配置
-XX:+PrintGCDetails 打印 GC 信息。
-XX:+PrintGCDateStamps
-Xloggc:gc.log
-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析。
非堆内存参数配置:-XX:PermSize、-XX:MaxPermSize
1、-XX:PermSize:表示非堆区初始内存分配大小,其缩写为permanent size(持久化内存)。
2、-XX:MaxPermSize:表示对非堆区分配的内存的最大上限。