一、基本概念
JVM,Java Virtual Machine(即Java虚拟机),是一种用于计算设备的规范,它是一个虚构出来的计算机。
二、JVM内存模型
2.1 运行时内存区域
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域
(1)jdk1.8之前:
jdk1.8之后:方法区变成了元空间
2.2 内存区域划分和基本概念:
线程间公有的:堆、方法区(元空间)、直接内存
线程间私有的:程序计数器、本地方法栈、虚拟机栈
堆内存:
堆是JVM所管理的内存区域中最大的一块,为所有线程所共享。主要用处是用来存储new 出来的对象实例,几乎所有的对象实例以及数组都会在这里被分配。
Java堆,是垃圾回收的主要区域,所以也被成为GC堆。
方法区:
主要用来存储虚拟机加载出来的类信息、常量、静态变量,运行时常量池,以及即时编译出来的代码等信息。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 方法区也被为永久代。
虚拟机栈:
- 虚拟机栈的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的
- 虚拟机栈主要由栈桢组成,方法间的调用和返回实际上是一个压栈和出栈的过程。每个栈桢由局部变量表、操作数栈、动态链接和方法出组成。
- 虚拟机栈主要用来存储各种基本类型(byte、short、int、double、boolean、boolean、char、float、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
虚拟机栈常报的两个错:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误
本地方法栈:
主要存放了为虚拟机底层使用的各种Native方法服务
程序计数器:
程序计数器,是一块较小的内存空间,可以看成是当前线程执行的字节码的行号指示器。
主要作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
运行时常量池:
运行时常量池,是方法区的一部分,class文件中不光有类的版本、字段名称、方法名称接口等描述信息,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存:
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
三、堆内存划分及垃圾回收时的内存分配
3.1堆内存划分
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:
- 新生代(Young Generation)
- 老年代(Old Generation)
- 永久代(Permanent Generation)
新生代又分为Dden区和 Survivor区。Survivor区分为 From Space(s0)和To Space(s1)
一般情况下:
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
新生代的内存占用比例为 eden:s0:s1 = 8:1:1
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
3.2 JVM垃圾回收过程
由于现在收集器基本都采用分代垃圾收集算法,所以在进行GC的时候,新生代和老年代都会经历一下步骤:
新生代:
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要
回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。
老年代:
老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
分代回收的过程:
首先,在创建对象的时候,实例对象基本都会被分配在Eden区,Survivor区是空的。
(1)在进行第一次GC时,当新生代的 Eden Space 就会发生一次 GC,幸存下来的对象被复制到s0区,Eden 区都会被清理掉。
(2)在进行下一次GC的时候,触发时机是 Eden Space 和s0内存不足。与(1)不同的是,此次幸存的下来的对象,以及把上次 GC移到 S0 中的对象会被复制到s1区,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。这时,Eden区和s0都会被清理掉。
(3)当s0无法足够存储某个对象,则将这个对象存储到老年代。
(4)当年龄增加到一定程度(默认15,可以通过参数 -XX:MaxTenuringThreshold 配置),这个对象就从年轻代转移到了老年代。
这样,老年代中的对象就持续增加。
(5)触发 major gc 对老年代空间进行清理和压缩。
注:不是永远要求新生代年龄达到阈值才能被移到年老代
如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。
3.3 报错OOM(OutOfMemoryError)
- OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
- java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space 错误。(和本机物理内存无关,和你配置的对内存大小有关!)
四、方法区和永久代
4.1方法区和永久代的关系
方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
4.2 jdk1.8为什么要把永久代替换成元空间
(1)整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace
JDK8之前,在JVM启动之前通过在命令行设置参数XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M
JDK8之后,使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
(2)元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
(3)在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
五、JVM内存参数设置
- -Xms设置堆的最小空间大小。
- -Xmx设置堆的最大空间大小。
- -Xmn:设置年轻代大小
- -XX:NewSize设置新生代最小空间大小。
- -XX:MaxNewSize设置新生代最大空间大小。
- -XX:PermSize设置永久代最小空间大小。
- -XX:MaxPermSize设置永久代最大空间大小。
- -Xss设置每个线程的堆栈大小
- -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
- -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
典型JVM参数配置参考:
- java-Xmx3550m-Xms3550m-Xmn2g-Xss128k
- -XX:ParallelGCThreads=20
- -XX:+UseConcMarkSweepGC-XX:+UseParNewGC
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大 小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000 左右。