JVM初识-JVM内存结构

JVM是什么呢?

JVM(Java Virtual Machine,Java虚拟机)

    Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。

    JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。

  • JVM内存结构

1、JVM内存结构主要由三部分组成,分别是堆、方法区、栈

  1. 堆:内存占用最大,包括新生代(new space)和老年代(old space),新生代包含伊甸园(Eden)区、From Survivor空间、To Survivor空间.默认情况下按照8:1:1的比例进行分配.
  2. 堆是Java虚拟机所管理的内存中最大的一块存储区域,堆内存被所有线程共享.主要存放使用new关键字创建的对象.所有对象实例以及数组都要再对上分配.垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间,而不是对象本身). 堆中对象都需要考虑线程安全的问题,有垃圾回收机制
  3. 新生代存储“新生对象”,我们新创建的对象存储再年轻代中,当年轻代中内存不足以放下新对象时,则会触发Minor GC,清理年轻代内存空间.
  4. 老年代存储长期存活的对象和大对象,年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储,老年代空间不足以存放新晋升的对象时,则会触发Full GC,Full GC是清理整个堆空间,包括年轻代和老年代,如果Full GC后,堆中仍无法储存对象,则会抛出OutOfMemory内存溢出异常.
  5. JVM初识-JVM内存结构_第1张图片
  6. 至于幸存区的两个空间,在垃圾回收阶段解释其作用.

2、方法区:

    注意:在JDK1.8以前,静态成员储存在方法区(永久代)中,此时方法区的实现叫做永久代,在JDK1.8以后,永久代被移除,此时方法区的实现更改为元空间,但由于元空间主要用于存储字节码文件,因此静态成员的存储位置从方法区更改到了堆内存中.在JDK1.6及以前,常量池存储在方法区(永久代)中,在JDK1.7中,方法区被整合到堆内存中,常量池存储在堆内存中,在JDK1.8后,方法区从堆内存中独立出来,常量池存储在方法区(永久代)中.

    1、方法区同Java堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量(JDK1.8以前)、即时编译器编译后的代码(即使编译器在类加载器阶段解释),更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中,常量池也是方法区的一部分.

    2、常量池中存储编译器生成的各种字面量和符号引用.字面量就是java中常量的意思,比如文本字符串,final修饰的常量等.方法引用则包括类和接口的权限定名、方法名和描述符,字段名和描述符等.常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息.

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址JDK1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回,JDK1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回. StringTable也存在垃圾回收,如果想提高其性能,可以将其内部的桶buffer数提高,并且使用intern()方法可以避免重复字符串过多造成内存滥用.

比如下面这段代码:JVM初识-JVM内存结构_第2张图片

S3==s5的返回值为true,因为在执行s3=“ab”时,就会在常量池中查询”ab”字符串,有则直接将“ab”字符串的引用赋值给s3,无则在常量池中储存“ab”字符串,再将引用返回并赋值给s3,当执行s5=“a”+”b”时,在编译阶段就已经优化为“ab”了,所以在常量池中查询”ab”字符串,发现已经存在,则将其引用赋值给s5,所以s5与s3的引用地址相同,则==运算自然为true,但s4!=s3,s4!=s5,因为s4是s1+s2,变量并不能在编译器进行优化,所以会使用StringBuilder().append()进行拼接,并返回新的字符串对象,所以虽然s4、s3、s5内容相同但引用地址不同.所以s4!=s3,s4!=s5

常量池中的字符串仅是符号,第一次用到时才变为对象,利用串池的机制,来避免重复创建字符串对象 字符串变量拼接的原理是stringBuilder ,字符串常量拼接的原理是编译期优化,可以使用intern方法,主动将串池中还没有的字符串对象放入串池

小练习:JVM初识-JVM内存结构_第3张图片

输出为false,true,可以自己试试.

再试一试下面这段代码,加深理解:

JVM初识-JVM内存结构_第4张图片

False,true,true,   x1==x2的结果:   true,如果调换位置,则为false,不调换位置,jdk1.6的话则为false.

    3、常量池作用:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享.

              JVM初识-JVM内存结构_第5张图片

3、栈:

1、JVM中的栈包括java虚拟机栈和本地方法栈,两者的区别就是,java虚拟机栈为JVM执行java方法服务,本地方法则为JVM使用到的Native方法服务.两者作用是及其相似的,以下java虚拟机栈简称为栈.

2、定义:限定仅在表头进行插入和删除操作的线性表.即压栈和弹栈都是堆栈顶元素进行操作的,所以栈是先进后出的.栈是线程私有的,它的生命周期与线程相同,每个线程都会被分配一个栈的空间,因此每个线程都具有它私有的一个栈空间,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常.

3、栈帧,栈帧是栈的元素(是用于支持虚拟机进行方法调用和方法执行的数据结构),每个线程在调用某个方法,比如方法A(),当线程调用A时,jvm就会在该线程的栈中创建一个栈帧,栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息.每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程.

4、栈帧中,由一个局部变量表存储数据。

局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量〈包括参数)、和对象的引用(String、数组、对象等),但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(Iong、double),JVM会为其分配两个连续的变量槽来存储。以下简称Slot 。JVM通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的Slot数量。普通方法与static方法在第О个槽位的存储有所不同。非static方法的第О个槽位存储方法所属对象实例的引用。关于局部变量表的使用,将在字节码指令部分讲解. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

5、每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,

持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解另外的一部分将在每一次运行时期转化为直接引用,这部分称为动态连接.

6、当一个方法开始执行后,只有2种方式可以退出这个方法:

方法返回指令︰执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。

异常退出︰在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

JVM初识-JVM内存结构_第6张图片JVM初识-JVM内存结构_第7张图片JVM初识-JVM内存结构_第8张图片

4、程序计数器(为每个线程私有):

程序计数器(Program Counter Register)是一块较小的内存空间(储存在栈中),可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。如果线程执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,计数器值为Undefined。程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

  

你可能感兴趣的:(JVM,堆,栈,方法区,常量池,java,开发语言)