JAVA内存区域解析

 

虚拟机

    虚拟机从软件层面屏蔽了不同操作系统在底层硬件与指令上的区别,java之所以能够跨平台,就是因为虚拟机屏蔽了各个操作系统之间的差异。

                                                           

    因为不同的操作系统底层的硬件指令集是不同的,所以不同平台JVM的实现各不相同。同样的java代码生成的机器码也是不同的的,如上图所示,windows上生成的机器码和linux上生成的机器码是不同的。作为程序员不用去关心JVM的区别,我们看到的是同一份JAVA代码在各个不同平台都可以运行的跨平台的优越性。

JAVA内存区域

    java运行时数据区可以分为以下几个部分,程序计数器,栈,本地方法栈,堆,和堆中的方法区。如下图所示:

                         

    其中栈,本地方法栈,程序计数器是线程独有的,也就是每一个线程都会独立拥有这几块区域。堆和方法区是所有线程共享的。

    下面我们先简单介绍下各个部分,然后在通过一个实例代码来讲解各个区域的运作。

    程序计数器

    程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

    程序计数器是为了解决多线程的切换问题,由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

   

    与程序计数器一样,栈也是线程私有的,它的生命周期与线程相同。

    栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈帧的操作遵循先进后出的原则,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在栈中入栈到出栈的过程。

                                                               

    本地方法栈

    本地方法栈(Native Method Stack)与栈所发挥的作用是非常相似的,它们之间的区别不过是栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

    堆

    对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

    堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

                                     

    方法区

    方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

    Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

    有了上面的概念之后我们通过一个实例来讲解下各个内存区域是如何工作的。

public class Math {
 
    public static final Integer CONSTANT_1 = 111;
    public static User user = new User();
    public int math(){
        int a = 1;
        int b = 2;
        int c = (a+b)*10;
        return c;
    }
 
    public static void main(String[] args) {
        Math mathObj = new Math();
        mathObj.math(); 
        System.out.println(math.math());
    }
}

    我们都知道jvm要运行java代码,首先要把java代码转换成jvm可识别的字节码,也就是xxx.class的文件。但字节码文件我们是无法直接看懂的。jdk自带的javap命令,可以将class文件生成一种更可读的字节码文件。

javap -c Math.class >>Math.txt

    查看生成的Math.txt.

Compiled from "Math.java"
public class Math {
  public static final java.lang.Integer CONSTANT_1;
 
  public static java.lang.Object obj;
 
  public 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 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: bipush        111
       2: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: putstatic     #8                  // Field CONSTANT_1:Ljava/lang/Integer;
       8: new           #9                  // class java/lang/Object
      11: dup
      12: invokespecial #1                  // Method java/lang/Object."<init>":()V
      15: putstatic     #10                 // Field obj:Ljava/lang/Object;
      18: return
}

   

    oracle官方是有专门的jvm字节码指令手册来查询每组指令对应的含义的,我们通过查询手册就可以明白各个指令的具体含义,通过我们上面对栈的概念的介绍,我们可以知道目前栈内的情况如下图所示:

                                              

    下面我们以math函数为例来实际看下栈内的各个部分是如何工作的。

  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

 

0: iconst_1 //将int类型常量1压入栈。

                                        

1: istore_1 //将int类型值存入局部变量1

    局部变量1,在我们代码中也就是第一个局部变量a,先给a在局部变量表中分配内存,然后将int类型的值,也就是目前唯一的一个1存入局部变量a.

                                        

2: iconst_2 //将int类型常量2压入栈

3: istore_2 //将int类型值存入局部变量2

这两步和前两步类似,执行完毕后栈内详情如下:

                                       

4: iload_1 //从局部变量1中装载int类型值

5: iload_2 //从局部变量2中装载int类型值

这两个代码是将局部变量1和2,也就是a和b的值装载到操作数栈中.

                                        

6: iadd //执行int类型的加法

iadd指令一执行,会将操作数栈中的1和2依次从栈底弹出并相加,然后把运算结果3在压入操作数栈底。

                                       

 

7: bipush 10 //将一个8位带符号整数压入栈

这个指令就是将10压入栈.

                                      

 

9: imul //执行int类型的乘法

将3和10弹出栈,把结果30压入栈

                                     

10: istore_3 //将int类型值存入局部变量3

将30存入局部变量3,也就是c.

                                      

11: iload_3 //从局部变量3中装载int类型值

将局部变量3,也就是c的值装载到操作数栈中.

                                    

12: ireturn //从方法中返回int类型的数据

将操作数栈中的30返回.

通过上面的演示大家应该对局部变量表和操作数栈有所理解了。简单来说=后面的操作数,这些操作数进行赋值,运算的时候需要内存存放,那就是存放在操作数栈中,作为临时存放操作数的一小块内存区域。

    我们接着看源代码,main函数中Math mathObj = new Math(); 我们创建了一个mathObj局部变量,我们知道new出来的对象是放在堆里的,mathObj放的就是new出来的Math对象的内存地址。

    这样堆和栈之间的联系就建立起来了。

    程序计数器

    我们可以看到上面字节码文件中每个指令码前面都有一个行号,你就可以把它看作当前线程执行到某一行代码位置的一个标识,这个值就是程序计数器的值。

    方法区

    public static final Integer CONSTANT_1 = 111;
    public static User user = new User();

这个CONSTANT_1 就是静态变量,毋庸置疑是存放在方法区的

那么这个user就有点不一样了,user变量放在方法区,new的User是存放在堆中的

到这里我们就可以看到堆,方法区之间也是有联系的。

                          

    GC

                                               

    堆我们上面已介绍过,是由年轻代和老年代组成,年轻代又分为伊甸园区和survivor区,survivor区中又有from区和to区。我们new出来的对象一般都放在Eden区,那当Eden区满了之后呢?

    一个程序只要在运行,那么就不会不停的new对象,那么总有一刻Eden区会放满,那么一旦Eden区被放满之后,虚拟机会干什么呢?没错,就是gc,不过这里的gc属于minor gc,就是垃圾收集,来收集垃圾对象并清理的,那么什么是垃圾对象呢?

    这里就涉及到了一个GC Root根以及可达性分析算法的概念,可达性分析算法是将GC Roots对象作为起点,从这些起点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的都是垃圾对象。

GC Root根节点:线程栈的本地变量,静态变量,本地方法栈的变量等等。

                                      

    经历了第一次minor gc后,Eden可回收的对象被清理,没有被清理的对象就会被移到From区,一个对象每经历一次gc,那么它的gc分代年龄就会+1如下图。

                                          

    那么如果第二次新的对象又把Eden区放满了,那么又会执行minor gc,但是这次会连着From区一起gc,然后将Eden区和From区存活的对象都移到To区域,对象头中分代年龄都+1,如下图:

                                                 

    那么当第三次Eden区又满的时候,minor gc就是回收Eden区和To区域了,Eden区和To区域还活着的对象就会都移到From区,如上图。说白了就是Survivor区中总有一块区域是空着的,存活的对象存放是在From区和To区轮流存放,也就是互相复制拷贝,这也就是垃圾回收算法中的复制-回收算法。

    如果一个对象经历了一个限值15次gc的时候,就会移至老年代。那如果还没有到限值,From区或者To区域也放不下了,就会直接挪到老年代,这只是举例了两种常规规则,还有其他规则也是会把对象存放至老年代的。

                                               

    那么随着应用程序的不断运行,老年代最终也是会满的,那么此时也会gc,此时的gc就是Full gc了。

至此JAVA内存区域的简单介绍就结束了,希望大家能对JAVA内存区域有一个简单的了解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值