1 JVM内存模型图
根据JVM规范,JVM内存共分为五个部分。方法区、堆、虚拟机栈、本地方法栈、程序计数器。其中程序计数器、虚拟机栈、本地方法栈是线程私有的。方法区、堆是线程公有的。
1.1 JDK1.8内存模型概览
1.8同1.7相比,最大的差别就是元空间取代了永久代。元空间的本质和永久代类似,都是堆JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不存在虚拟机中,而是使用本地内存。
2 运行时数据区
2.1 程序计数器
2.1.1 什么是程序计数器
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
2.1.2 为什么需要程序计数器
Java在编译后的字节码未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”解释执行。可以简单理解为解释器读取装入内存的字节码,按照顺序读区字节码指令,并翻译成指定的操作,并根据这些操作进行计算、跳转、循环等操作。
从上面的描述,可以怀疑程序计数器还有必要吗?如果程序永远只有一个线程,指令按顺序执行即可,确实程序计数器没有存在的必要。但是Java程序是多线程协同合作执行的,而JVM的多线程是通过CPU的时间片轮转算法实现,也就是说线程可能还没有执行完,但是由于时间片耗尽,不得不挂起,等待下次分配时间片,才能继续执行。所以需要计数器记住被挂起时,程序执行的位置。
2.1.3 程序计数器的特点
1 、每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
2、程序计数器的值为对应线程执行字节码指令的地址。
3、 如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为空 (Undefined)。
4、程序计数器占用内存空间很小,几乎可以忽略不计。
5 、此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2.2 虚拟机栈
和程序计数器一样,虚拟机栈也是线程私有的,它的生命周期和线程相同。
虚拟机栈描述的是方法执行的线程内存模型,方法执行的时候,Java虚拟机都会在虚拟机栈里面创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
2.2.1 入栈出栈图解
符合后入先出的栈规则
2.2.2 栈的溢出(StackOverflowError)
1 如果栈帧的数量过多,或者某些栈帧过大会引发SOE(StackOverflowError)
2 如果允许虚拟机栈动态扩展,那么当内存不足时,会导致OOM(OutOfMemoryError)
2.2.3 栈帧图解
局部变量表
局部变量表存放了这个栈帧对应的方法的局部变量(基本类型是值、引用类型是句柄或者指针)
引申:方法中定义的局部变量是否线程安全?
如果对象是在方法内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
操作数栈
主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是出栈和入栈操作。
例如整数加法(2+3)的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了int类型的数据,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。
动态连接
动态连接将符号引用转化为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。
直接引用:直接引用可以是直接指向目标的指针,也可以是能间接定位到目标的句柄,还可以是相对偏移量。
每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
方法出口
无论是程序正常返回或者是异常调用完成返回,都必须回到最初方法被调用时的位置。
2.3 本地方法栈
本地方法栈也是线程私有的。
本地方法栈与虚拟机栈所发挥的作用是类似的,区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地Native方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError。
2.4 方法区
方法区只是一个概念上的东西,JDK1.8元空间是方法区的实现。方法区是线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
Class对象是存放在堆区的,不是方法区!这点很多人容易犯错。
类的元数据(元数据并不是类的Class对象!Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区。
类型信息:对于加载的类型包括class、interface、enum、annotation,JVM必须在方法区中存储以下类型信息:
① 这个类型的完整有效名称(全名=包名.类名)
② 这个类型直接父类完整有效名(对于interface或者是java.langlObject,都没有父类)
③ 这个类型的修饰符(public、abstract、final的某个子集)
④ 这个类型直接接口的一个有序列表
2.4.1 运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外、还有一项信息是常量池表的引用,用于存放编译期生成的各种字面量与符号引用。
Java中的字节码需要数据支持,通常这种数据很大以至于不能直接存到字节码里面,所以就存到常量池,而字节码文件存储的就是指向常量池的引用,在动态链接的时候会用到运行时常量池。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池表的运行时表示形式,在类或接口被加载到JVM后,对应的运行时常量池就被创建出来,常量池表的字面量与符号引用就会放到运行时常量池。
2.4.1 方法区版本优化
java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
java7中,存储在永久代的部分数据就已经转移到Java Heap(堆)或者Native memory(本地内存)。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。
java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连。所以1.8之后,字符串常量池和静态变量在堆中,运行时常量池在元空间里,元空间虽然用的本地内存,也是会进行垃圾回收的。
为什么移除永久代?
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、永久代大小不容易确定,PermSize指定太小容易造成永久代OOM
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
2.5 堆
堆是线程共享的内存区域,它是虚拟机管理内存中最大的一块。堆主要用于存放各种类的实例对象和数组。Java世界里几乎所有的对象实例都在这里分配内存。当堆中没有内存分配给对象实例时,会抛出OutOfMemoryError
新生代gc流程:
1 刚刚新建的对象在Eden中,经历一次Minor GC,Eden中存活对象就会被移动到s0,Eden被清空。
2 等Eden区再满了,就出发一次Minor GC,Eden和s0中存活的对象又会被复制到s1中(这个过程非常重要,因为这种复制算法保证了s1中来自s0和Eden两部分的存活对象占用了连续的空间,避免碎片化)
3 s0和Eden被清空,然后下一轮s0与s1互换角色,如此循环。