JVM是抽象的计算机模型,当然也遵守冯诺依曼计算机体系模型,所以JVM肯定也不是一个只有内存的结构就不管了,它必然是会将JVM划分成不同的区域(组成了运行时数据区)。
具体可以看:JVM:JVM是抽象的计算机模型,JVM运行时数据区划分的原因;CPU的发展及衍生问题,CPU发展过程,CPU多核心数的发展由来;内存缓存一致性问题,CPU缓存一致性问题,Java多线程缓存一致性问题
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
类装载生命周期的第一步装载的过程中间,内存当中就会有方法区以及堆两块内容, 那么内存当中是否还会有其它的区域呢?
jdk1.8官网的运行时数据区目录结构划分是这样的,可以看到运行时数据区分为五块区域:官网
官网对于运行时数据区介绍地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会销毁。其他数据区域是按线程划分的。每个线程的数据区域在创建线程时创建,在线程退出时销毁。
之前在讲类的生命周期(类加载的过程)的装载(Load)阶段时候,装载分为三个步骤:
在装载阶段的第(2),(3)步可以发现有运行时数据,堆,方法区等名词
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
说白了就是类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)
堆和方法区是线程共有的,程序计数器、本地方法栈、Java虚拟机栈是线程私有的。
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
官网jdk1.8中的方法区的介绍:
1)方法区是各个线程共享的内存区域,在虚拟机启动时创建
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is created on virtual machine start-up.
2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非
堆),目的是与Java堆区分开来
Although the method area is logically part of the heap,…
3)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
上述总结概括就是:
方法区是一个线程共享模型,是堆的一部分,有个别名,叫做“非堆”,目的是为了跟我们真正的Java堆区分开来。非堆之所以叫非堆,是因为它是堆逻辑的一部分,仅仅只是逻辑,方法区实际的落地是分为两块。有一部分是在JVM的堆内存当中进行一个实现,包括字符串常量池,静态变量,包括类信息是放在直接内存中的。方法区在内存不足的情况下,会抛出OOM。
注意:Java官网对于方法区的规范和方法区的具体实现并不一样,因为这仅仅只是一个规范,是一个概念(存放方法的信息),而真正的实现会随着jdk的版本而进行优化,实现是会改名的,每个版本叫法可能并不一样。
jdk1.6,1.7,1.8版本方法区具体实现变化、为什么jdk1.8移除了永久代
此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.3
这是jdk1.8官网中的堆的介绍:
1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。堆是用于存放所有内存的运行时数据区域分配类实例和数组。
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
2)Java对象实例以及数组都在堆上分配。
The heap is created on virtual machine start-up.
堆是一个线程共享模型,因为它的进程跟JVM进行绑定的,既然是进程相关的,那么按照模型来推论,它就必然是线程共享的。
此时回看装载阶段的第3步,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
常量池分为静态常量池,运行时常量池,还有字符串常量池。
静态常量池: 其实储存的就是字面量以及符号引用
运行时常量池(Run-Time Constant Pool): 运行时常量池就是我们的每个类以及每个接口在我们的JVM进行run的过程中所在内存中开辟出来的一块用来储存我们静态常量池部分数据的一块特殊区域。
字符串常量池: 包含在动态常量池里
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.5
官网对于运行时常量池有这么一句描述,Each run-time constant pool is allocated from the Java Virtual Machine's method area
,意思是说,每个运行时常量池都是从Java虚拟机的方法中分配的区域。
运行时常量池是在JVM完成所谓的一个类装载的过程之后,会将所谓的class文件中的常量池载入到内存中间,并且它会保存在方法区当中。
我们常说的常量池指的最多的就是方法区中的运行时常量池。打开一个反编译字节码文件
可以看到class文件中的常量池Constant pool下面有一堆常量用来记录符号引用和字面量信息,它占用了class文件内容中非常大的比重,这种常量池也一般称作为静态常量池 。我们会将这些常量汇总,并在内存中间专门开辟出一块专门为它服务的区域,用以对这些数据进行存储。
静态常量池也会被加载到内存当中。
因为如果说静态常量池不被加载进我们的内存当中,就意味着静态常量池它的数据是在JVM的外部完成了一系列的解析,或者说已经在外部转变成了我们想要的数据,不然就必须进到所谓的JVM的内存当中去,显然在进入JVM当中之前,它并没有做这样的一件事情。
JVM方法区中,实际上它储存了class文件的信息以及我们的运行时常量池,那么这个时候,实际上我们的class文件的信息就包含有两部分:
①类信息:所谓的类信息:包括魔术开头、版本号信息、以及后面的类信息,我们可以将类信息描述成一个框架,一个规范
②常量池:规范中具体的内容,就是填充的所谓的常量池,内容由常量池来进行储存
运行时常量池参照官方文档的定义:就是每个类、每个接口在JVM进行run的过程中间,在我们的内存开辟出一块用来储存我们静态常量池一部分数据的一个特殊区域。并且我们会在运行时常量池所需要内存不足的情况下,抛出OOM,可以将这个区域看作是方法区的一个细分部分。
Sting中的intern()可以将值动态的添加到字符串常量池中。字符串常量池也叫做String常量池,它通常是包含在运行时常量池中间的。包括运行时常量池里面其实还包括了一类,叫Java基本类型封装类的常量池,比如Integer、这Boolean些,但是不包括浮点数,比如float、double。
这里就又衍生出一个问题,Java1.8当中,它对于方法区的实现做了一些改变,也就是说1.7以前的版本,我们用到的方法区在虚拟机中,是虚拟机的内存,再大不会超过JVM内存的上限,而在jdk1.8中,方法区的实现元空间使用的是直接内存,那就意味着可以超出虚拟机的内存范围之外。如果说元空间超出了JVM的内存范围之外,也就是说元空间需要使用直接内存,那么各个常量池到底是如何分布的呢?是否用到的都是直接内存呢?
上图可以看到,运行时常量池中的字符串常量池和基本类型包装类常量池都使用的是堆内存,也就是隶属于JVM内存,而运行时常量池中的其它常量池是可以延伸到直接内存中的。
Java本身是一门面向对象的语言,那么必然会不断的产生对象。而常量池当中的内容一般情况下来说是使用的比较频繁的数据,并且一般来说它的生命周期不会太短,那么如果说JVM不设计常量池这个东西,就需要频繁的创建和销毁对象,从而会影响系统的性能。比如说字符串常量池,在编译阶段就将所有相同的字符串常量合并,只占用一个空间,这样可以大幅度的减少运行时间。那么如何设计这样一个东西?就需要考虑到能否实现对象的共享,也就是一个类的缓存操作(但其实不是真正的缓存操作)。
举个例子,比如比较字符串的时候,“==”和“equals()”进行对比,“==”必然是是比“equals()”快的。
==: 在Java中只是运算符号,只会判断两个引用是否相等,比较的是他们在内存中的存放地址,也就是堆内存地址。
equals(): 是Java中的一个方法,是用来比较两个对象的内容是否相等,是判断两个变量或实例所指向的内存空间的值是不是相同。由于所有的类都是继承自java.lang.Object类的,因此如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的是“==”的判断。这种设计是一种另类的资源共享逻辑,线程池的设计就是参照了这样的思想进行设计的。
类加载阶段: 实际上new String ("aaa");
在类加载的阶段,就已经需要对这个对象进行创建,并且类只会加载一次,这个时候这个字符串"aaa"会放到全局共享的字符串常量池当中,这是类加载阶段干的事情。这个时候实际上就已经创建了一个“aaa”
在常量池中。
运行阶段: 会对常量池进行寻找,找到"aaa"
,而这里由于是new String()
,这种new的方式,意味着在堆中创建新对象,这个新对象会在堆上分配一个新的内存地址。会将将常量池这个"aaa"拷贝一份放到实际的堆中,并将这个新对象的引用交给s1。
综上所述:
String s1 = new String ("aaa");
在整个生命周期创建了两个对象,而且每调用一次就new String ()会产生一个对象。
一个是在常量池中,常量池的为"aaa"
,
一个是在堆内存中,堆内存中为new String("aaa")
,
而String s1 = new String("aaa");
,“=”
将这个对象的引用交给s1。
流程如下图:
String s="aaa";
这种赋值形式在java中叫字面量,是一种简单的字符串字面量的创建方式,也是Java中唯一不需要new 就可以产生对象的途径,它是在常量池中而不是像new一样放在压缩堆中。
这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"aaa"的对象,如果有,就会把它赋给当前引用,即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个"aaa"
,下一次如果有String s3 = "aaa";
又会将s3指向"aaa"
这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象。
综上所述:
JVM会在常量池中先查找有有没有一个值为"aaa"的对象,如果有,就会把它赋给当前引用,即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个"aaa"
。
总结,String str="i";
和String str=new String("i");
二者生成的字符串是相同的,但是存储方式是不同的。Java中尽量使用 String str="i";
这种字面量的方式去创建字符串,这样创建出来字符串可以被视作一个字符串常量,能够赋值给其他字符串对象和变量,还能够被 JVM 优化,提高代码的效率和性能。而使用 new String()
创建字符串则会在堆上生成新的对象,浪费内存资源。
思考一个问题:
经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会相应的生效,那么初始化完成了,后续就是使用,那怎样才能被使用到?换句话说里面内容怎样才能被执行?我们主函数main调用其他方法,这种方式实际上是main线程执行之后调用的方法,也就是说要想使用里面的各种内容,需要以线程为单位,执行相应的方法才行。那一个线程执行的状态如何维护?一个线程如何执行多个方法?这样的关系怎么维护呢?
栈是一种执行“后进先出”算法的数据结构,具有先进后出的特点。栈的优点是存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈数据可以共享,但缺点是运行时动态分配内存,存取速度较慢。栈数据的应用非常广泛,如操作系统、编译器、数据结构等。
Java是一个进程,进程跑起来之后,里面会有非常多的线程,线程会去执行我们的方法,那么就需要一个能够记录这些执行方法的状态的数据结构,而这个数据结构选用的就是栈。
官网 :https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6
frame实际上就是栈帧,它就是用来储存我们数据以及部分的结果,以及执行的动态链接,以及方法的返回值,和调度异常。
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
栈帧信息:
符号引用变成直接引用不是在类加载的过程当中就已经转变了吗?为什么还会在方法的执行阶段再做一次呢?
Java有个特性叫做多态,在调用某个方法的时候,它会有父类,也有可能有子类,无法确定真正调到哪里去了,也许可能调到其它文件去了;而我们在编译的时候,编译的是当前的文件的一些东西,它并不知道你调用到其它的class文件中去调用的是什么,那么这个时候,它不会去确定它到底调用的是哪一个子类,因为加载阶级上就不确定,所以说买这个时候只有在方法的执行过程中间,我们才能够确定我们某一些情况,完成符号引用变成直接引用,因为到了这一步才知道到底指向的是哪里,所以还会在方法的执行阶段又再做一次将符号引用变成直接引用的转变。包括程序计数器:
Java中的方法又会分为几类:
记录这些执行方法的状态的栈,也会根据Java方法种类的不同再去划分:
一个线程当中肯定会有非常多的方法,那么必然会有一个最小的存储单位,这个就是栈帧。说白了,就是代表一个方法的执行,有多少个方法,就往当前的这个栈中压多少个栈帧。比如线程T执行Java方法时:
void a(){
b();
}
void b(){
c();
}
void c(){
}
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.2
这是jdk1.8官网中的堆的介绍:
1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
Each Java Virtual Machine thread has a private Java Virtual Machine stack,created at the same time as the thread.
2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出,这也是栈的特性,先进后出。
A Java Virtual Machine stack stores frames (§2.6).
A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes.
官方也说明了,栈中不能进行无限制的栈帧压入,如果对栈进行无限制的压入的话,会出现throws a StackOverflowError,栈溢出。Java中栈是一种数据结构。
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.6
本地方法栈它执行的是native方法。比如说Object中就有很多这种方法,如hashCode()。通常会在创建每个线程的时候,为每个线程分配它自己的本地方法栈,也就是说,本地方法栈也是线程私有的。
思考一个问题:
一个JVM进程当中,会有多个线程执行,那么这个时候,线程中的内容想要拥有CPU的执行权,是靠抢占CPU的时间片,这个时候比如A线程执行到栈帧3执行到一半,这个时候下一个线程线程B抢到CPU时间片,那么A线程就失去了CPU的调度权,那么等到B执行完,A抢到了CPU的执行权,切换回A线程,那么这个时候,怎么判断栈帧执行到哪了呢?
所以一个线程肯定需要在线程中维护一个变量,记录我们所谓的线程执行到的位置,它需要让我们的栈中的内容按照顺序一步一步执行下去。
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1
一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?
这就是需要在线程中维护一个变量,记录线程执行到的位置。
程序计数器,也叫PC寄存器。每个JVM线程都有自己的程序计数器,也就是程序计数器是每个线程私有的。
如果在栈帧中有一个变量,类型为引用类型,比如Object obj = new Object()
,这时候就是典型的栈中元素指向堆中的对象。
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象。
private static Object obj = new Object();
堆还能指向方法区?方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?
这就需要了解一个Java对象的具体信息。
JVM1:官网了解JVM;Java源文件运行过程、javac编译Java源文件、如何阅读.class文件、class文件结构格式说明、 javap反编译字节码文件;类加载机制、class文件加载方式
JVM2:类加载机制、class文件加载方式;类加载的过程:装载、链接、初始化、使用、卸载;类加载器、为什么类加载器要分层?JVM类加载机制的三种方式:全盘负责、父类委托、缓存机制;自定义类加载器
JVM3:图解类装载与运行时数据区,方法区,堆,运行时常量池,常量池分哪些?String s1 = new String创建了几个对象?初识栈帧,栈的特点,Java虚拟机栈,本地方法发栈,对象指向问题
JVM4:Java对象内存布局:对象头、实例数据、对齐填充;JOL查看Java对象信息;小端存储和大端存储,hashcode为什么用大端存储;句柄池访问对象、直接指针访问对象、指针压缩、对齐填充及排序
JVM5:JVM内存模型与运行时数据区的关系,堆为什么分区,分代年龄,Young区划分,Survivor区为什么分为S0和S1,如何理解各种GC:Partial GC、Full GC、Young GC
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出
JVM7:垃圾回收是什么?从运行时数据区看垃圾回收到底回收哪块区域?垃圾回收如何去回收?垃圾回收策略,引用计数算法及循环引用问题,可达性分析算法