Java虚拟机在执行Java程序的过程有把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自不同的作用,如下图
详细
《深入理解Java虚拟机》
1.程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是-个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。,
2.Java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame")用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。.
局部变量表:存放局部变量
操作数栈:存放操作数,进行操作运算,将结果压回操作数栈
动态链接:根据符号(方法)找到对应的内存地址
方法返回地址:执行一个方法时,退出方法(栈帧出栈)
局部变量表存放了编译期可知的各种基本数据类型(boolean、 byte、 char、 short、 int、float、long、 double)、 对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一一个指向对象起始地址的引用指针,也可能指向-一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型( 指向了一条字节码指令的地址)。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
3.本地方法栈
本地方法栈(NativeMethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
4.Java堆
对于大多数应用来说,Java 堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配”,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换。优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(GarbageCollected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(ThreadLocalAllocation Buffer, TLAB)。 不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java 堆中的上述各个区域的分配和回收等细节将会是下一.章的主题。
根据Java虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
5.方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap (非堆),目的应该是与Java堆区分开来。
运行时常量池( Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java虚拟机栈
下面用代码来说一下Java虚拟机栈
package com.lq;/** /** * * @Description: TOTO * @author BushRo * @date 2018-12-21 * */ public class Math { public static Object obj = new Object(); public static final Integer CONSTANT = 6661; public int math(){ int a=1; int b=2; int c=(a+b)*10; return c; } public static void main(String[] args) { Math math = new Math(); System.out.println(math.math()); } }
按照代码的加载顺序,会先加载静态变量与静态常量,并放在方法区中。然后加载main()方法,Java虚拟机栈会为每个方法创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。main方法里面中的局部变量表中会存放math这个对象引用,真正的对象会存放在堆中。main方法中又使用到了math()这个方法,所有又会创建一个栈帧用在存放math()有关的东西。栈的存储方式是先进后出。
我们可以把代码进行反编译来看下JVM是怎么解读的,使用javap -c ..calss就可以反编译了。
Compiled from "Math.java" public class com.lq.Math { public static java.lang.Object obj; public static final java.lang.Integer CONSTANT; public com.lq.Math(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int math(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn public static void main(java.lang.String[]); Code: 0: new #2 // class com/lq/Math 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: invokevirtual #5 // Method math:()I 15: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 18: return static {}; Code: 0: new #7 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: putstatic #8 // Field obj:Ljava/lang/Object; 10: sipush 6661 13: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 16: putstatic #10 // Field CONSTANT:Ljava/lang/Integer; 19: return }
运行时数据区中,java栈、本地方法栈、程序计数器都是线程私有的,堆和方法区是所有线程共享的。在上面的代码中执行main()方法时jvm就会专门创建一个线程来处理, 并为线程中的每个方法创建一个java虚拟机栈。
我们可以通过查看jvm指令集 https://www.cnblogs.com/dreamroute/p/5089513.html,知道大概的意思。
观看math()方法中的字节码,查表得:iconst_1是将int型1推送至栈顶,在这里也就是将1放入操作数栈, istore_1 将栈顶int型数值存入第二个本地变量,也就是把a=1放入局部变量表中。依次类推,最后方法有一个返回的出口,也就是方法出口会指向main()方法,也就是把结果返回给main()方法。
栈:
函数中定义的基本类型变量,对象的引用变量都在函数的栈内存中分配。
栈内存特点,数数据一执行完毕,变量会立即释放,节约内存空间。
栈内存中的数据,没有默认初始化值,需要手动设置。
堆:
堆内存用来存放new创建的对象和数组。
堆内存中所有的实体都有内存地址值。
堆内存中的实体是用来封装数据的,这些数据都有默认初始化值。
堆内存中的实体不再被指向时,JVM启动垃圾回收机制,自动清除,这也是JAVA优于C++的表现之一
堆
堆:分为新生代,老年代,元空间(1.8以前叫永久代),两者之间的区别是元空间使用的是物理内存,永久代使用的是堆里面的内存。(下图中的From与To有称为s0,s1)
Young,年轻代(易被 GC)。Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区,其中 Survivor 区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在 Young 区间变满的时候,minor GC 就会将存活的对象移到空闲的Survivor 区间中,根据 JVM 的策略,在经过几次垃圾收集后,任然存活于 Survivor 的对象将被移动到 Tenured 区间。
Tenured,终身代。Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区,一般如果系统中用了 application 级别的缓存,缓存中的对象往往会被转移到这一区间。
Perm,永久代。主要保存 class,method,filed 对象,这部门的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的 class 没有被卸载掉,这样就造成了大量的 class 对象保存在了 perm 中,这种情况下,一般重新启动应用服务器可以解决问题。
堆的内存分配:首先new出来的对象都是放在新生代的Eden区,直到把Eden装满为止,然后会进行一次小GC把无用的对象清除掉,如果清理过后Eden还是没有足够的空间的话就会把一些Eden存活的对象放到From区,直到From区满为止,就会对From进行一次GC操作,并把From存活的对象转移到To区,当Eden满了之后又做GC,然后把Eden还存活的对象往To区里面 放,直到To区满为止,To区做GC,然后反过来往From区放,就这样一直来回的做GC,jdk1.8默认如果一个对象被GC了15次还存活的话,就会把这个对象往老年代里面放。如果老年代也放满了就进行一个FullGC,FullGC主要是对老年代进行清理,进行FullGC的时候程序会停止运行。Java调优就是让jvm少进行GC操作。