目录
01. 概述
- java由jvm执行的
- jvm划分java内存区域。
02. java程序执行的流程
- java文件被java编译器编译为字节码文件(.class)。
- jvm类加载器加载类字节码文件。
- 加载完毕之后,交由jvm执行引擎执行。
- 分析:在整个程序执行过程中,jvm会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),即 jvm内存。因此,在Java中常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
03. jvm内存
jvm内存划分为:程序计数器(program counter register)、java栈(jvm stack)、本地方法栈(native method stack)、方法区(method area)、堆(heap)。
3.1. 程序计数器(program counter register)
- 别名
PC寄存器 - 特点
α. 线程创建时, 创建。
β. 指向下一条指令执行的地址。
γ. 线程私有
原因:一个处理器,某一时刻只会执行一条线程的指令
δ.执行native方法时,PC值为undefined。
ε.Java虚拟机规范中唯一没有规定任何OutOfMemoryError情况的区域。
解释:只存储下一条指令执行的地址,因此占用的空间不会随程序的执行而发生改变,也就不会发生内存溢出现象。
3.2. java栈(jvm stack)
- 概述
- java方法执行的内存模型。
- java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。在栈帧中包括:
α. 局部变量表(local variables)
β. 存放基本类型(boolean、byte、char、short、int、float、long、double),其中long、double占用2个局部变量空间。
γ. 对象引用(refrence), 即对象起始地址的指针
δ. returnAddress类型
ε. 操作数栈(operand stack)
ζ. 指向当前方法所属类的运行时常量池引用(Reference to runtime constant pool)
ν. 方法返回地址(return address)
ξ. 附加信息 - 别名
虚拟机栈 - 特点
α. 线程私有,一个线程对应一个java栈。
β. 存放栈帧。每个栈帧对应一个被调用的方法。
γ. 调用方法时,创建一个对应的栈帧, 压栈到栈顶。
δ. 栈顶指向线程当前正在调用的方法。
ε. 方法调用完毕,栈顶栈帧出栈。
ζ. 抛异常的情况
如果线程请求的栈深度超出虚拟机允许的深度,抛出StackOverflowError异常。
如果无法申请到内存,抛出OutOfMemoryError异常。 - JVM设置参数
-Xss
栈内存在jvm中,默认为1mb
3.3. 本地方法栈(native method stack)
- 概述
与java栈类似。
区别:本地方法栈为执行native方法的空间,java栈为执行java方法的空间。 - 抛异常的情况
如果线程请求的栈深度超出虚拟机允许的深度,抛出StackOverflowError异常。
如果无法申请到内存,抛出OutOfMemoryError异常。
3.4. 方法区(method area)
- 概述
α. 被Java虚拟机描述为堆的一个逻辑部分。习惯叫永久代。永久代也会垃圾回收,主要针对常量池回收,类型卸载(比如反射生成大量临时使用的Class等信息)。
β. java8中已经没有方法区了,取而代之的是元空间(metaspace)。 - 别名
永久代 - 特点
α. 线程共享。
β. 存储虚拟机加载的类信息、常量、静态变量等。
γ. 针对常量池的回收,以及类型的卸载。
δ. 抛出异常的情况
当方法区满时,无法再分配空间,就会抛出内存溢出的异常(OutOfMemoneyError)。 - JVM配置参数
-XX:MaxPermSize=64m
方法区占用的最大内存
-XX:PermSize=64m
方法区分配的初始内存
3.5. 运行时常量池
- 用途
存放编译器运行期生成的字面量和符号引用,且类加载后存放到常量池中。
jdk1.6及之前字符串常量池位于方法区之中。
jdk1.7字符串常量池已经被挪到堆之中。α. 字面量
常量,如字符串、final常量值。
β. 符号引用
编译原理方面的概念,包括:类和接口的完全限定名、字段名称和描述符、方法的名称和描述符。 - 特点
α. 方法区的一部分。
β. 线程共享。
γ. 动态性
java语言,常量在编译期、运行期间产生,存放于运行时常量池中。常量包括:基本类型包装类(不管理浮点型,整型只管理-128 ~ 127)、String(也可通过String.intern()强制将String放入常量池)。 - 优点
α. 对象共享, 避免频繁创建和销毁对象而影响系统性能。
β. 节省内存空间。
γ. 节省运行时间。
比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值地址是否相等。 - 基本类型
public final class Integer extends Number implements Comparable<Integer> { public static Integer valueOf(int i) { assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } } public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // 将对象加入到常量池之中 public native String intern(); }
3.6. 堆(heap)
- 概述
存储Java实例和对象的地方,是GC的主要区域,线程共享的内存区域。 - 特点
α. 线程共享。
β. 虚拟机启动,创建。
γ. 存放对象实例和数组。
δ. 垃圾收集器管理的主要区域。
ε. 根据分带收集算法分
- 细分
-XX:NewRatio
设置新生代与老年代比值。如:-XX:NewRatio=4,即新生代:老年代=1:4。-
新生代
存放新创建的占用空间小的实例、以及新生代垃圾收集没有超过设置次数(默认15次)的实例。
补充:占用空间大的实例(如Array),直接存放到老年代。优点:a. 防止占用空间大的对象创建,导致新生代还有大量剩余空间,而提前发生gc;b. 防止在eden区和survivor区的大对象复制造成性能问题。
-XX:PretenureSizeThreshold 实例超过该值直接存储到老年代。 -
老年代
存放新生代垃圾收集超过设置次数(默认15次)的实例,以及一些占用空间大的实例(比如缓存)。
-
- 更细致一点:
-
eden空间
新创建的对象(不包括占用空间大的对象)存放在eden区。 -
from和to
当eden区占满时,存活的实例复制到from区。
当from区占满时,minor gc将eden、from存活的实例复制到to区,同时清空from和eden,以及交换from和to。
注意:如果to区不能容纳minor gc之后的实例,那么超过占用to空间最多实例存活年代的对象将被移到old区。hotspot虚拟机新生代eden和survivor大小比值为4:1,因为有两个survivor,因此eden:from:to比值为4:1:1。
-
- 抛异常的情况
在堆中没有内存分配实例,并且无法再扩展时,将抛出OutOfMemoryError异常。 - 设置参数
-Xms
最小堆空间,如-Xms512m
-Xmx
最大堆空间,如-Xmx1g
-Xmn
设置新生代内存大小,如-Xmn2g
-XX:SurvivorRatio
设置survivor:eden比值,如:-XX:SurvivorRatio=4,则2个survivor与一个eden的比值为2:4。
在JVM中,最小堆空间默认物理内存的1/64,最大堆内存在JVM中默认物理内存1/4,且建议最大堆内存不大于4G。
3.7. 直接内存
- 概述
α. 直接内存不属于jvm运行时数据区,也不属于JVM规范中定义的内存区域。
β. jdk1.4引入nio(new input/output)类,nio是基于通道(channel)与缓冲区(buffer)的IO方式,使用native函数库直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。能在一些场景中显著提高性能,避免在java堆和native堆中来回复制数据。
γ. 直接内存的分配不会受到Java堆大小的限制,受到本机总内存大小限制。
δ. 配置虚拟机参数时,不要忽略直接内存,防止出现OutOfMemoryError异常。 - 特点
α. 直接内存申请空间耗费更高的性能,频繁申请到一定量时尤为明显。
β. 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。 - 使用场景
α. 有很大的数据需要存储,它的生命周期很长。
β. 适合频繁的IO操作,例如网络并发场景。 - 直接内存和非直接内存对比
/** 直接内存与堆内存的比较 **/ public class ByteBufferCompare { public static void main(String[] args) { allocateCompare(); //分配比较 operateCompare(); //读写比较 } /** * 直接内存和堆内存的分配空间比较 * 结论: 在数据量提升时, 直接内存相比非直接内的申请, 有很严重的性能问题 */ public static void allocateCompare(){ int time = 10000000; //操作次数 long st = System.currentTimeMillis(); for (int i = 0; i < time; i++) { //ByteBuffer.allocate(int capacity) 分配一个新的字节缓冲区。 ByteBuffer buffer = ByteBuffer.allocate(2); //非直接内存分配申请 } long et = System.currentTimeMillis(); System.out.println("在进行"+time+"次分配操作时, 堆内存分配耗时:"+(et-st)+"ms" ); long st_heap = System.currentTimeMillis(); for (int i = 0; i < time; i++) { //ByteBuffer.allocateDirect(int capacity) 分配新的直接字节缓冲区。 ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请 } long et_direct = System.currentTimeMillis(); System.out.println("在进行"+time+"次分配操作时, 直接内存分配耗时:"+(et_direct-st_heap)+"ms" ); } /** * 直接内存和堆内存的读写性能比较 * 结论: 直接内存在直接的IO操作上, 在频繁的读写时会有显著的性能提升。 */ public static void operateCompare(){ int time = 1000000000; ByteBuffer buffer = ByteBuffer.allocate(2 * time); long st = System.currentTimeMillis(); for (int i = 0; i < time; i++) { // putChar(char value) 用来写入char值的相对put方法 buffer.putChar('a'); } buffer.flip(); for (int i = 0; i < time; i++) { buffer.getChar(); } long et = System.currentTimeMillis(); System.out.println("在进行" + time + "次读写操作时, 非直接内存读写耗时:" + (et-st) +"ms"); ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time); long st_direct = System.currentTimeMillis(); for (int i = 0; i < time; i++) { // putChar(char value) 用来写入char值的相对put方法 buffer_d.putChar('a'); } buffer_d.flip(); for (int i = 0; i < time; i++) { buffer_d.getChar(); } long et_direct = System.currentTimeMillis(); System.out.println("在进行" +time+"次读写操作时,直接内存读写耗时:"+ (et_direct - st_direct) +"ms"); } }
- 分析:
从数据流的角度来看
非直接内存作用链:
本地IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地IO
直接内存作用链:
本地IO -> 直接内存 -> 本地IO - 输出:
在进行10000000次分配操作时,堆内存分配耗时:12ms
在进行10000000次分配操作时,直接内存分配耗时:8233ms
在进行1000000000次读写操作时,非直接内存读写耗时:4055ms
在进行1000000000次读写操作时,直接内存读写耗时:745ms
- 分析: