一、java虚拟机内存结构:

JVM是一种规范。

1.线程共享:堆、方法区

线程独享:虚拟机栈、本地方法栈、程序计数器

2、堆:是垃圾回收的主要区域

堆的内存又根据垃圾回收分为:

(1).新生代:Eden、S0(Survive from)、S1(Survice to)

(2).老年代

3、方法区:它存储了每一个类的结构信息

字节码被JVM保存在方法区,主要是分成:访问权限和类的属性(access_flags)、类索引父类索引和接口索引集合、常量池、字段表、方法表、和字段和方法结构中的属性表(方法的内容字节码就是存在属性表的code[]属性中)。 可以理解成Class的结构就是一张表,在执行的时候提供需要的信息

需要注意的一点是方法区是规范,在不同类型的虚拟机中采用不同的实现方法,hotspot在jdk 1.7包括1.7之前方法是永久代实现的(目的是为了不用在给方法区单独写内存管理的代码了),永久代在物理内存上和老年代连续,所以老年代或者永久带的垃圾回收会同时触发两个区域的垃圾回收,因为永久代的实现更容易使方法区产生内存溢出(因为永久代有maxPermSize的上限,而不用永久代实现的方法区只要在进程的最大内存范围内就不会内存溢出),在jdk1.7开始逐渐去永久代,jdk1.7把字符串常量池和静态变量放到了堆中, 在 jdk 1.8取消了永久代,是用元空间代替了永久带,最大区别就是方法区的物理空间和老年代的物理空间不再连续,和堆相对独立了。

运行时常量池是一个统称 也包括字符串常量池,但是字符串常量池放的只是字符串,而运行时常量池中,还包括类信息,属性信息,方法信息,以及其他基础类型的的常量池比如int,long等
jdk1.7之前,运行时常量池(包含着字符串常量池)都在方法区,具体的hotspot虚拟机实现为永久代
jdk1.7阶段,字符串常量池从方法区移到堆中,运行池常量池剩下的部分依旧在方法区(剩下类信息、属性信息、方法信息等),同样是hotspot中的永久代
jdk1.8, 方法区的实现从永久代变成了元空间,因此 字符串常量池依然在堆中,运行时常量池在方法区,hotspot中的元空间(metaspace)

运行时常量池:

有一篇文章有助于理解常量池:

[java]JVM之运行时常量池里到底有什么 - 简书1. 概念 首先我们来复习一下java内存模型,java运行时数据区大概分为五块,分别是 方法区 虚拟机栈 本地方法栈 堆 程序计数器 而运行时常量池是方法区的一部分,文字解...https://www.jianshu.com/p/614e2b6a0f22https://www.jianshu.com/p/614e2b6a0f22

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符 号引用则属于编译原理方面的概念,包括了下面三类常量:

类和接口的全限定名(Fully Qualified Name)

字段的名称和描述符(Descriptor)

方法的名称和描述符

为啥要有这个常量池呢?

(1).Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟

机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段

的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正

的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的

符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

方法符号引用会被方法表中的那个方法结构的地址所替代,字段符号引用会被字段表中的那个字段结构所替代

(2).运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池

(3).因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。

字段表(field_info)、方法表(method_info)、属性表(attribute_info)会引用到常量池里的一些常量,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?

所以我理解的常量池的作用就是:提供一些常量(字面量),这些表用到这些常量时都引用一个地方,提供本Class文件中用到的字段和方法的符号引用(在jvm进行类加载的解析阶段这些符号引用会变成字段或者方法结构的地址),在使用的时候直接在常量池中找,这样就节省了内存空间。

在编译的时候所有在本Class文件中用到的字段和调用的方法(只要有方法调用就行例如上面的setAge中的getName方法),符号引用都会被保存在字节码的常量池中(包括其他类的也会存在自己的常量池表中),类加载后JVM会为每一个类都维护一个自己的常量池,在类加载的解析阶段,这些符号引用就被解析成了内存地址直接引用。

4、虚拟机栈:

虚拟机栈是线程独享的,和线程的生命周期一样,可以通过-Xss参数设置虚拟机栈的大小,hotspot默认是1M,不同的虚拟机默认值不同。

虚拟机栈越大是不是执行速度越快?

不是,虚拟机栈越大,递归或者循环执行方法的次数越大,不影响执行速度。虚拟机的内存大小是固定的,虚拟机栈越大代表着虚拟机可以同时工作的线程越少。

虚拟机栈存储当前线程运行方法所需的数据,指令、返回地址

运行时栈帧结构:

       栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟
机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部
变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的
过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
       每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附
加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完
全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受
到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
       一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来
说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack
Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有
字节码指令都只针对当前栈帧进行操作,在概念模型上,典型的栈帧结构如图
栈帧结构图:

栈帧的结构:

(1).局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。

既然前面提到了Java虚拟机的数据类型,在此再简单介绍一下它们。一个Slot可以存放
一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、
int、float、reference和returnAddress 8种类型。前面6种不需要多加解释,读者可以按照Java
语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java语言与Java虚拟机中的基
本数据类型是存在本质差别的),而第7种reference类型表示对一个对象实例的引用,虚拟机
规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机
实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在Java
堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方
法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束约束。第8种即
returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条
字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由
异常表代替。
对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java
语言中明确的(reference类型则可能是32位也可能是64位)64位的数据类型只有long和double
两种。值得一提的是,这里把long和double数据类型分割存储的做法与“long和double的非原
子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法有些类似,读者阅
读到Java内存模型时可以互相对比一下。不过,由于局部变量表建立在线程的堆栈上,是线
程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最
大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64
位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位
数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求
了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如
果执行的是实例方法(非static的方法),那局部变量表中第0位索引的Slot默认是用于传递方
法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体
内部定义的变量顺序和作用域分配其余的Slot。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,
其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的
作用域,那这个变量对应的Slot就可以交给其他变量使用。不过,这样的设计除了节省栈帧
空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到系
统的垃圾收集行为
例:局部变量表Slot复用对垃圾收集的影响之一
    public static void main(String[] args) throws Exception {byte[]placeholder=new byte[64*1024*1024];System.gc();}

代码很简单,即向内存填充了64MB的数据,然后通知虚拟机进行垃圾收集。我们在虚拟机运行参数中加上“-verbose:gc”来看看垃圾收集的过程,发现在System.gc()运行后并没有回收这64MB的内存,下面是运行的结果:

[GC 66846K->65824K(125632K),0.0032678 secs]

[Full GC 65824K->65746K(125632K),0.0064131 secs]

没有回收placeholder所占的内存能说得过去,因为在执行System.gc()时,变量placeholder还处于作用域之内,虚拟机自然不敢回收placeholder的内存。那我们把代码修改一下,变成代码清单8-2中的样子。

例 : 局部变量表Slot复用对垃圾收集的影响之二:

    public static void main(String[] args) throws Exception {{byte[] placeholder = new byte[64 * 1024 * 1024];}System.gc();}

加入了花括号之后,placeholder的作用域被限制在花括号之内,从代码逻辑上讲,在执
行System.gc()的时候,placeholder已经不可能再被访问了,但执行一下这段程序,会发现
运行结果如下,还是有64MB的内存没有被回收,这又是为什么呢?
结果:
[GC 66846K->65888K(125632K),0.0009397 secs]
[Full GC 65888K->65746K(125632K),0.0051574 secs]
在解释为什么之前,我们先对这段代码进行第二次修改,在调用System.gc()之前加入一行“int a=0;
例 : 局部变量表Slot复用对垃圾收集的影响之三
    public static void main(String[] args) throws Exception {{byte[] placeholder = new byte[64 * 1024 * 1024];}int a = 0;System.gc();}
这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了。
结果:
[GC 66401K->65778K(125632K),0.0035471 secs]
[Full GC 65778K->218K(125632K),0.0140596 secs]

placeholder能否被回收的根本原因是:局部变量表中
的Slot是否还存有关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了
placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所
占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对
它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方
法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际上已经不
会再使用的变量,手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量表
Slot清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象
占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件)下
的“奇技”来使用。Java语言的一本非常著名的书籍《Practical Java》中把“不使用的对象应手
动赋值为null”作为一条推荐的编码规则,但是并没有解释具体的原因,很长时间之内都有读
者对这条规则感到疑惑。

上面三个示例说明了赋null值的操作在某些情况下确实是有
用的,但笔者的观点是不应当对赋null值的操作有过多的依赖,更没有必要把它当做一个普
遍的编码规则来推广。原因有两点,从编码角度讲,以恰当的变量作用域来控制变量回收时
间才是最优雅的解决方法,如上面示例的场景并不多见。更关键的是,从执行角度
讲,使用赋null值的操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上
的。在虚拟机使用解释器 执行时,通常与概念模型还比较接近,但经过JIT编译器后,才是虚拟机执行代码的主要方式,赋null值的操作在经过JIT编译优化后就会被消除掉,这时候将变量设置为null就是没有意义的。
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的
类变量那样存在“准备阶段”。通过第7章的讲解,我们已经知道类变量有两次赋初始值的过
程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始
值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确
定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用
的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样
的默认值。

例:

(2).操作数栈

是执行引擎的一部分,有点像CPU的寄存器

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First
Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的
max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和
double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任
何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,
会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算
术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进
行参数传递的。
另外, 在概念模型中, 两个栈帧作为虚拟机栈的元素, 是完全相互独立的。 但在大多虚
拟机的实现里都会做一些优化处理, 令两个栈帧出现一部分重叠。 让下面栈帧的部分操作数
栈与上面栈帧的部分局部变量表重叠在一起, 这样在进行方法调用时就可以共用一部分数
据, 无须进行额外的参数复制传递如图:

(3).动态连接

每个栈帧都包含一个指向运行时常量池[1]中该栈帧所属方法的引用, 持有这个引用是为
了支持方法调用过程中的动态连接( Dynamic Linking) 。  我们知道Class
文件的常量池中存有大量的符号引用, 字节码中的方法调用指令就以常量池中指向方法的符
号引用作为参数。 这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接
引用, 这种转化称为静态解析。 另外一部分将在每一次运行期间转化为直接引用, 这部分称
为动态连接。

(4).完成出口,方法返回地址

记录上一个栈帧执行的代码行数,恢复现场

当一个方法开始执行后, 只有两种方式可以退出这个方法。 第一种方式是执行引擎遇到
任意一个方法返回的字节码指令, 这时候可能会有返回值传递给上层的方法调用者( 调用当
前方法的方法称为调用者) , 是否有返回值和返回值的类型将根据遇到何种方法返回指令来
决定, 这种退出方法的方式称为正常完成出口( Normal Method Invocation Completion) 。
另外一种退出方式是, 在方法执行过程中遇到了异常, 并且这个异常没有在方法体内得
到处理, 无论是Java虚拟机内部产生的异常, 还是代码中使用athrow字节码指令产生的异
常, 只要在本方法的异常表中没有搜索到匹配的异常处理器, 就会导致方法退出, 这种退出
方法的方式称为异常完成出口( Abrupt Method Invocation Completion) 。 一个方法使用异常
完成出口的方式退出, 是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式, 在方法退出之后, 都需要返回到方法被调用的位置, 程序才能
继续执行, 方法返回时可能需要在栈帧中保存一些信息, 用来帮助恢复它的上层方法的执行
状态。 一般来说, 方法正常退出时, 调用者的PC计数器的值可以作为返回地址, 栈帧中很可
能会保存这个计数器值。 而方法异常退出时, 返回地址是要通过异常处理器表来确定的, 栈
帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈, 因此退出时可能执行的操作有: 恢复
上层方法的局部变量表和操作数栈, 把返回值( 如果有的话) 压入调用者栈帧的操作数栈
中, 调整PC计数器的值以指向方法调用指令后面的一条指令等。
(5).方法调用

方法调用并不等同于方法执行, 方法调用阶段唯一的任务就是确定被调用方法的版本
( 即调用哪一个方法) , 暂时还不涉及方法内部的具体运行过程。 在程序运行时, 进行方法
调用是最普遍、 最频繁的操作, 但前面已经讲过, Class文件的编译过程中不包含传统编译中
的连接步骤, 一切方法调用在Class文件里面存储的都只是符号引用, 而不是方法在实际运行
时内存布局中的入口地址( 相当于之前说的直接引用) 。 这个特性给Java带来了更强大的动
态扩展能力, 但也使得Java方法调用过程变得相对复杂起来, 需要在类加载期间, 甚至到运
行期间才能确定目标方法的直接引用。
(6) 解析

继续前面关于方法调用的话题, 所有方法调用中的目标方法在Class文件里面都是一个常
量池中的符号引用, 在类加载的解析阶段, 会将其中的一部分符号引用转化为直接引用, 这
种解析能成立的前提是: 方法在程序真正运行之前就有一个可确定的调用版本, 并且这个方
法的调用版本在运行期是不可改变的。 换句话说, 调用目标在程序代码写好、 编译器进行编
译时就必须确定下来。 这类方法的调用称为解析( Resolution) 。

在Java语言中符合“编译期可知, 运行期不可变”这个要求的方法, 主要包括静态方法和
私有方法两大类, 前者与类型直接关联, 后者在外部不可被访问, 这两种方法各自的特点决
定了它们都不可能通过继承或别的方式重写其他版本, 因此它们都适合在类加载阶段进行解
析。
与之相对应的是, 在Java虚拟机里面提供了5条方法调用字节码指令, 分别如下。
invokestatic: 调用静态方法。
invokespecial: 调用实例构造器< init> 方法、 私有方法和父类方法。
invokevirtual: 调用所有的虚方法。
invokeinterface: 调用接口方法, 会在运行时再确定一个实现此接口的对象。
invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法, 然后再执行该方
法, 在此之前的4条调用指令, 分派逻辑是固化在Java虚拟机内部的, 而invokedynamic指令
的分派逻辑是由用户所设定的引导方法决定的。

只要能被invokestatic和invokespecial指令调用的方法, 都可以在解析阶段中确定唯一的
调用版本, 符合这个条件的有静态方法、 私有方法、 实例构造器、 父类方法4类, 它们在类
加载的时候就会把符号引用解析为该方法的直接引用。 这些方法可以称为非虚方法, 与之相
反, 其他方法称为虚方法( 除去final方法, 后文会提到)

Java中的非虚方法除了使用invokestatic、 invokespecial调用的方法之外还有一种, 就是被
final修饰的方法。 虽然final方法是使用invokevirtual指令来调用的, 但是由于它无法被覆盖,
没有其他版本, 所以也无须对方法接收者进行多态选择, 又或者说多态选择的结果肯定是唯
一的。 在Java语言规范中明确说明了final方法是一种非虚方法

解析调用一定是个静态的过程, 在编译期间就完全确定, 在类装载的解析阶段就会把涉
及的符号引用全部转变为可确定的直接引用, 不会延迟到运行期再去完成。 而分派
( Dispatch) 调用则可能是静态的也可能是动态的, 根据分派依据的宗量数可分为单分派和
多分派。 这两类分派方式的两两组合就构成了静态单分派、 静态多分派、 动态单分派、 动态
多分派4种分派组合情况, 下面我们再看看虚拟机中的方法分派是如何进行的

(7). 分派

众所周知, Java是一门面向对象的程序语言, 因为Java具备面向对象的3个基本特征: 继
承、 封装和多态。 本节讲解的分派调用过程将会揭示多态性特征的一些最基本的体现,
如“重载”和“重写”在Java虚拟机之中是如何实现的, 这里的实现当然不是语法上该如何写,
我们关心的依然是虚拟机如何确定正确的目标方法。

1.静态分派
在开始讲解静态分派[1]前, 笔者准备了一段经常出现在面试题中的程序代码, 读者不妨
先看一遍, 想一下程序的输出结果是什么。 后面我们的话题将围绕这个类的方法来重载
( Overload) 代码, 以分析虚拟机和编译器确定方法版本的过程

public class Test2{static abstract class Human{} static class Man extends Human{}static class Woman extends Human{}public void sayHello(Human guy) {System.out.println("hello,guy! ") ;}public void sayHello(Man guy) {System.out.println("hello,gentleman! ") ;}public void sayHello(Woman guy) {System.out.println("hello,lady! ") ;}public static void main(String[]args) {Human man=new Man() ;Human woman=new Woman() ;Test2 sr=new Test2() ;sr.sayHello(man) ;sr.sayHello(woman) ;}}

运行结果:

上面的代码实际上是在考验阅读者对重载的理解程度, 相信对Java编程稍有经
验的程序员看完程序后都能得出正确的运行结果, 但为什么会选择执行参数类型为Human的
重载呢? 在解决这个问题之前, 我们先按如下代码定义两个重要的概念。

Human man=new Man() ;

我们把上面代码中的“Human”称为变量的静态类型( Static Type) , 或者叫做的外观类型
( Apparent Type) , 后面的“Man”则称为变量的实际类型( Actual Type)或者叫运行时类型(Runtime type) , 静态类型和实际类型在程序中都可以发生一些变化, 区别是静态类型的变化仅仅在使用时发生(例如强转), 变量本身的静态类型不会被改变, 并且最终的静态类型是在编译期可知的; 而实际类型变化的结果在运行期才可确定, 编译器在编译程序的时候并不知道一个对象的实际类型是什么。 例如下面的
代码:

        //实际类型变化Human human= (new Random().nextBoolean() ? new Man() : new Woman());//静态类型变化sr.sayHello((Man)human);sr.sayHello((Woman)human);

对象human的实际类型是可变的,编译期间不清楚它到底是Man还是Woman,必须等到程序运行到这行时才能确定。而human的静态类型是Human,也可以在使用时像上面这段代码一样强制转型临时改变这个类型,但这个改变仍在编译期可知。

解释了这两个概念, 再回到静态分派第一段代码中。 main( ) 里面的两次
sayHello( ) 方法调用, 在方法接收者已经确定是对象“sr”的前提下, 使用哪个重载版本, 就
完全取决于传入参数的数量和数据类型。 代码中刻意地定义了两个静态类型相同但实际类型
不同的变量, 但虚拟机( 准确地说是编译器) 在重载时是通过参数的静态类型而不是实际类
型作为判定依据的。 并且静态类型是编译期可知的, 因此, 在编译阶段, Javac编译器会根
据参数的静态类型决定使用哪个重载版本, 所以选择了sayHello( Human) 作为调用目标,
并把这个方法的符号引用写到main( ) 方法里的两条invokevirtual指令的参数中。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。 静态分派的典型应用
是方法重载。 静态分派发生在编译阶段, 因此确定静态分派的动作实际上不是由虚拟机来执
行的。 另外, 编译器虽然能确定出方法的重载版本, 但在很多情况下这个重载版本并不
是“唯一的”, 往往只能确定一个“更加合适的”版本。 这种模糊的结论在由0和1构成的计算机
世界中算是比较“稀罕”的事情, 产生这种模糊结论的主要原因是字面量不需要定义, 所以字
面量没有显式的静态类型, 它的静态类型只能通过语言上的规则去理解和推断。

何为更适合的版本:

public class Test2{public static void sayHello(Object arg) {System.out.println("hello Object") ;}public static void sayHello(int arg) {System.out.println("hello int") ;}public static void sayHello(long arg) {System.out.println("hello long") ;}public static void sayHello(Character arg) {System.out.println("hello Character") ;}public static void sayHello(char arg) {System.out.println("hello char") ;}public static void sayHello(char...arg) {System.out.println("hello char……") ;}public static void sayHello(Serializable arg) {System.out.println("hello Serializable") ;}public static void main(String[]args) {sayHello('a') ;}}

上面的代码运行后会输出:

这很好理解, 'a'是一个char类型的数据, 自然会寻找参数类型为char的重载方法, 如果
注释掉sayHello( char arg) 方法, 那输出会变为:

这时发生了一次自动类型转换, 'a'除了可以代表一个字符串, 还可以代表数字97( 字
符'a'的Unicode数值为十进制数字97) , 因此参数类型为int的重载也是合适的。 我们继续注释
掉sayHello( int arg) 方法, 那输出会变为:

这时发生了两次自动类型转换, 'a'转型为整数97之后, 进一步转型为长整数97L, 匹配
了参数类型为long的重载。 笔者在代码中没有写其他的类型如float、 double等的重载, 不过实
际上自动转型还能继续发生多次, 按照char-> int-> long-> float-> double的顺序转型进行匹
配。 但不会匹配到byte和short类型的重载, 因为char到byte或short的转型是不安全的。 我们继
续注释掉sayHello( long arg) 方法, 那输出会变为:
 

这时发生了一次自动装箱, 'a'被包装为它的封装类型java.lang.Character, 所以匹配到了
参数类型为Character的重载, 继续注释掉sayHello( Character arg) 方法, 那输出会变为:
 这个输出可能会让人感觉摸不着头脑, 一个字符或数字与序列化有什么关系? 出现hello
Serializable, 是因为java.lang.Serializable是java.lang.Character类实现的一个接口, 当自动装箱
之后发现还是找不到装箱类, 但是找到了装箱类实现了的接口类型, 所以紧接着又发生一次
自动转型。 char可以转型成int, 但是Character是绝对不会转型为Integer的, 它只能安全地转
型为它实现的接口或父类。 Character还实现了另外一个接口java.lang.Comparable< Character
> , 如果同时出现两个参数分别为Serializable和Comparable< Character> 的重载方法, 那它
们在此时的优先级是一样的。 编译器无法确定要自动转型为哪种类型, 会提示类型模糊, 拒
绝编译。 程序必须在调用时显式地指定字面量的静态类型, 如: sayHello( ( Comparable<
Character> ) 'a') , 才能编译通过。 下面继续注释掉sayHello( Serializable arg) 方法, 输出会变为:
 

这时是char装箱后转型为父类了, 如果有多个父类, 那将在继承关系中从下往上开始搜
索, 越接近上层的优先级越低。 即使方法调用传入的参数值为null时, 这个规则仍然适用。
我们把sayHello( Object arg) 也注释掉, 输出将会变为:

7个重载方法已经被注释得只剩一个了, 可见变长参数的重载优先级是最低的, 这时候
字符'a'被当做了一个数组元素。 笔者使用的是char类型的变长参数, 读者在验证时还可以选
择int类型、 Character类型、 Object类型等的变长参数重载来把上面的过程重新演示一遍。 但
要注意的是, 有一些在单个参数中能成立的自动转型, 如char转型为int, 在变长参数中是不
成立的
上面的示例演示了编译期间选择静态分派目标的过程, 这个过程也是Java语言实现方法
重载的本质。 演示所用的这段程序属于很极端的例子, 除了用做面试题为难求职者以外, 在
实际工作中几乎不可能有实际用途。 笔者拿来做演示仅仅是用于讲解重载时目标方法选择的
过程, 大部分情况下进行这样极端的重载都可算是真正的“关于茴香豆的茴有几种写法的研
究”。 无论对重载的认识有多么深刻, 一个合格的程序员都不应该在实际应用中写出如此极
端的重载代码。

另外还有一点读者可能比较容易混淆: 笔者讲述的解析与分派这两者之间的关系并不是
二选一的排他关系, 它们是在不同层次上去筛选、 确定目标方法的过程。 例如, 前面说过,
静态方法会在类加载期就进行解析, 而静态方法显然也是可以拥有重载版本的, 选择重载版
本的过程也是通过静态分派完成的。

2.动态分派

了解了静态分派,我们接下来看一下动态分派的过程,它和多态性的另外一个重要体
现——重写(Override)有着很密切的关联。我们还是用前面的Man和Woman一起sayHello
的例子来讲解动态分派:
public class TestF {static abstract class Human {protected abstract void sayHello();}static class Man extends Human {@Overrideprotected void sayHello() {System.out.println("man say hello");}}static class Woman extends Human {@Overrideprotected void sayHello() {System.out.println("woman say hello");}}public static void main(String[] args) {Human man = new Man();Human woman = new Woman();man.sayHello();woman.sayHello();man = new Woman();man.sayHello();}
}

运行结果:

这个运行结果相信不会出乎任何人的意料,对于习惯了面向对象思维的Java程序员会觉得这是完全理所当然的。现在的问题还是和前面的一样,虚拟机是如何知道要调用哪个方法的?

显然这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?我们使用javap命令输出这段代码的字节码,尝试从中寻找答案:

main()方法的字节码:

public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new           #2                  // class com/example/fragmentadaptertest/testjava/TestF$Man3: dup4: invokespecial #3                  // Method com/example/fragmentadaptertest/testjava/TestF$Man."<init>":()V7: astore_18: new           #4                  // class com/example/fragmentadaptertest/testjava/TestF$Woman11: dup12: invokespecial #5                  // Method com/example/fragmentadaptertest/testjava/TestF$Woman."<init>":()V15: astore_216: aload_117: invokevirtual #6                  // Method com/example/fragmentadaptertest/testjava/TestF$Human.sayHello:()V20: aload_221: invokevirtual #6                  // Method com/example/fragmentadaptertest/testjava/TestF$Human.sayHello:()V24: new           #4                  // class com/example/fragmentadaptertest/testjava/TestF$Woman27: dup28: invokespecial #5                  // Method com/example/fragmentadaptertest/testjava/TestF$Woman."<init>":()V31: astore_132: aload_133: invokevirtual #6                  // Method com/example/fragmentadaptertest/testjava/TestF$Human.sayHello:()V36: returnLineNumberTable:line 26: 0line 27: 8line 28: 16line 29: 20line 30: 24line 31: 32line 32: 36LocalVariableTable:Start  Length  Slot  Name   Signature0      37     0  args   [Ljava/lang/String;8      29     1   man   Lcom/example/fragmentadaptertest/testjava/TestF$Human;16      21     2 woman   Lcom/example/fragmentadaptertest/testjava/TestF$Human;
0~15行的字节码是准备动作,作用是建立man和woman的内存空间、调用Man和Woman
类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表Slot之中,这个动作也
就对应了代码中的这两句:
 Human man = new Man();Human woman = new Woman();
接下来的16~21句是关键部分,16、20两句分别把刚刚创建的两个对象的引用压到栈
顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);17和21
句是方法调用指令,这两条调用指令单从字节码角度来看,无论是指令(都是
invokevirtual)还是参数(都是指向常量池中同一个常量,注释显示了这个常量是
Human.sayHello()的符号引用)完全一样的,但是这两句指令最终执行的目标方法并不相
同。原因就需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解
析过程大致分为以下几个步骤:

1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校
验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回
java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调
用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过
程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的
分派过程称为动态分派。
多态的根源在于虚方法调用指令invokevirtual的执行逻辑,那自然我们得出的结论就只会对方法有效,对字段是无效的,因为字段不使用这条指令。事实上,在java里只有虚方法存在,字段永远不可能是虚的,换句话说,字段永远不参与多态,哪个类的方法访问某个名字的字段时,改名字指的就是这个类能看到的那个字段。当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段。例:
字段没有多态:
public class TestF {public static class Father {public int money = 1;public Father() {money = 2;showMeTheMoney();}public void showMeTheMoney() {System.out.println("I am Father,I have $" + money);}}public static class Son extends Father {public int money = 3;public Son(){money = 4;showMeTheMoney();}public void showMeTheMoney() {System.out.println("I am Son,I have $" + money);}}public static void main(String[] args) {Father gay = new Son();System.out.println("This gay has $" + gay.money);}
}

执行结果:

输出两句都是"I am Son",这是因为Son类在创建的时候,首先隐式调用了Father的构造函数,而Father的构造函数中对showMeTheMoney()的调用是一次虚方法调用,实际执行的版本是Son的showMeTheMoney()方法,所以输出I am Son。而这时候虽然父类的money字段已经被初始化成2了,但Son的showMeTheMoney()方法中访问的却是子类的money字段,这时候结果自然还是 0,因为它要到子类的构造函数执行时才会被初始化。main的最后一句话通过静态类型(外观类型)访问到了父类中的money,输出了2

3.单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,这个定义最早应该来源于《Java与模
式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
单分派和多分派的定义读起来拗口,从字面上看也比较抽象,不过对照着实例看就不难
理解了。例:
public class TestF {static class QQ {}static class _360 {}public static class Father {public void hardChoice(QQ arg) {System.out.println("father choose qq");}public void hardChoice(_360 arg) {System.out.println("father choose 360");}}public static class Son extends Father {public void hardChoice(QQ arg) {System.out.println("son choose qq");}public void hardChoice(_360 arg) {System.out.println("son choose 360");}}public static void main(String[] args) {Father father = new Father();Father son = new Son();father.hardChoice(new _360());son.hardChoice(new QQ());}
}

运行结果:

在main函数中调用了两次hardChoice()方法,这两次hardChoice()方法的选择结果在
程序输出中已经显示得很清楚了。
我们来看看编译阶段编译器的选择过程,也就是静态分派的过程。这时选择目标方法的
依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的
最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向
Father.hardChoice(360)及Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量
进行选择,所以Java语言的静态分派属于多分派类型。
再看看运行阶段虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoice(new
QQ())”这句代码时,更准确地说,是在执行这句代码所对应的invokevirtual指令时,由于
编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的
参数“QQ”到底是“腾讯QQ”还是“奇瑞QQ”,因为这时参数的静态类型、实际类型都对方法的
选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型是
Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类
型。
根据上述论证的结果,我们可以总结一句话:如今(Java12 和 预览版Java13)的java语言是一门静态多分派、动态单分派的语言。
4.虚拟机动态分派的实现
前面介绍的分派过程, 作为对虚拟机概念模型的解析基本上已经足够了, 它已经解决了
虚拟机在分派中“会做什么”这个问题。 但是虚拟机“具体是如何做到的”, 可能各种虚拟机的
实现都会有些差别。
 
由于动态分派是非常频繁的动作, 而且动态分派的方法版本选择过程需要运行时在类的
方法元数据中搜索合适的目标方法, 因此在虚拟机的实际实现中基于性能的考虑, 大部分实
现都不会真正地进行如此频繁的搜索。 面对这种情况, 最常用的“稳定优化”手段就是为类在
方法区中建立一个虚方法表( Vritual Method Table, 也称为vtable, 与此对应的, 在
invokeinterface执行时也会用到接口方法表——Inteface Method Table, 简称itable) , 使用虚
方法表索引来代替元数据查找以提高性能。
 

虚方法表中存放着各个方法的实际入口地址。 如果某个方法在子类中没有被重写, 那子
类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的, 都指向父类的实现入
口。 如果子类中重写了这个方法, 子类方法表中的地址将会替换为指向子类实现版本的入口
地址。  Son重写了来自Father的全部方法, 因此Son的方法表没有指向Father类型数
据的箭头。 但是Son和Father都没有重写来自Object的方法, 所以它们的方法表中所有从
Object继承来的方法都指向了Object的数据类型

为了程序实现上的方便, 具有相同签名的方法, 在父类、 子类的虚方法表中都应当具有
一样的索引序号, 这样当类型变换时, 仅需要变更查找的方法表, 就可以从不同的虚方法表
中按索引转换出所需的入口地址。
 
方法表一般在类加载的连接阶段进行初始化, 准备了类的变量初始值后, 虚拟机会把该
类的方法表也初始化完毕。
 

5、程序计数器:

就是记录要执行的下一条机器码的地址。再多线程切换的时候需要线程独享的程序计数器来记录当前线程需要执行的下一句机器码。

6.对象的创建和内存布局

a.对象的内存布局

对象的内存可以分为3块区域:对象头、实例数据、对齐填充(占位符为了达到8字节的整数倍)

hotspot虚拟机中对象头分为两部分:第一部分是用于存储对象自身的运行时数据例如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,这部分数据长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为"mark word"

第二部分是用于存储类的类元数据的指针:注意类元数据的指针不一定存在对象头中,它的存储方式取决于虚拟机在查找类元数据时的实现,一般有两种方式:一、在堆中划分出一个句柄池,栈中存储对象的句柄地址,句柄中存储类的实例和类元信息地址。 二、像hotspot虚拟机就是栈中直接存储对象实例的地址,在对象头中存储类元数据的地址。

b.Java对象的创建过程
1.当虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有那必须先执行相应的类加载过程。
2.对象所需的内存大小在类加载完成后便可以完全确定(上面已经介绍过他的内存布局),对象内存划分的方式根据堆的内存空间是否规整分为两种:
(1).规整 : 指针碰撞
如果堆内存规整,使用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点的指示器,那分配内存的时候就是把这个指针向空闲的方向移动和对象大小相等的距离,这种方式被称为指针碰撞
(2).不规整 :空闲列表
如果堆内存是不规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,再分配内存的时候从列表中找到一块足够大的空间划分给对象用,并更新列表上的记录,这种分配方式被称为空闲列表
内存是否规整是由垃圾回收器是否有空间压缩整理的能力决定。例如:Serial和ParNew是带压缩整理过程的收集器,而CMS用的是基于清除(Sweep)算法的收集器就得使用空闲列表。
3.对象在创建时的线程安全的问题:
除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常
频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,
可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来
分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理
——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分
配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内
存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内
存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),
如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应
的零值。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找
到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。关于对象头的具体内容
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程
序的视角来看,对象创建才刚刚开始——构造方法,即Class文件中的<init>方法还没有执行,所有的字段都还为零,对象需要的其它资源和状态信息也没有按照预定的意图构造好,一般来说(由字节码中是否跟随invokespecial指令所决定,java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但是如果通过其他方式产生的则不一定如此),执行new指令之后会接着
执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完
全构造出来。
4.对象访问定位:
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的
具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定
义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是
取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中
存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信

如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的
相关信息
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳
定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中
的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,
由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成
本。就本书讨论的主要虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从
整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

java JVM 内存结构相关推荐

  1. 区分 JVM 内存结构、 Java 内存模型 以及 Java 对象模型 三个概念

    本文由 简悦 SimpRead 转码, 原文地址 https://www.toutiao.com/i6732361325244056072/ 作者:Hollis 来源:公众号Hollis Java 作 ...

  2. Java 内存模型和 JVM 内存结构真不是一回事

    这两个概念估计有不少人会混淆,它们都可以说是 JVM 规范的一部分,但真不是一回事!它们描述和解决的是不同问题,简单来说, Java 内存模型,描述的是多线程允许的行为 JVM 内存结构,描述的是线程 ...

  3. 【转】JVM内存结构 VS Java内存模型 VS Java对象模型

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途. 其中有些区域随着虚拟机进程的启动而 ...

  4. JVM内存结构 VS Java内存模型 VS Java对象模型

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚.比如本文我们要讨论的JVM内存结构.Java内存模型和Java对象 ...

  5. java多线程构造函数_java线程基础巩固---多线程与JVM内存结构的关系及Thread构造函数StackSize的理解...

    多线程与JVM内存结构的关系[了解]: 对于最后一个有疑问的构造中stackSize参数,其实学过编程滴人从参数字面就比较容易理解,栈大小嘛,这里从官方文档上来了解一下这个参数: 而之前在学习java ...

  6. java 堆内存结构_基于JDK1.8的JVM 内存结构【JVM篇三】

    在我的上一篇文章别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析[JVM篇二]中,相信大家已经对java类加载机制有一个比较全面的理解了,那么类加载之后,字节码数据在 ...

  7. 学习笔记:Java虚拟机——JVM内存结构、垃圾回收、类加载与字节码技术

    学习视频来源:https://www.bilibili.com/video/BV1yE411Z7AP Java类加载机制与ClassLoader详解推荐文章:https://yichun.blog.c ...

  8. java:JVM内存结构初步理解入门:堆、栈、方法区(浅显通俗易懂自记)

    自己整理的一些资料以及自己的一些理解,希望记下来的不是高大上而晦涩的概念,将自己此时此刻的理解最大程度地刻模,以便将来重温知新. JVM内存结构最简单可以初步分为:1栈 2堆 3方法区 4 程序计数器 ...

  9. 快速带你分清java内存结构,java内存模型,java对象模型和jvm内存结构!

    现如今你是否有这样的感觉,无论生活还是学习,节奏都是非常的快,每天面对海量的知识信息,自己感觉都要hold不住了,每天打开微信公众号,是不是发现有几十条未读,无论是技术文章还是其他类型的文章,我们大多 ...

最新文章

  1. python pip 错误 ModuleNotFoundError: No module named pip._internal 解决办法
  2. pandas mysql index_Pandas从入门到精通(3)- Pandas多级索引MultiIndex
  3. 2021年度公有云安全报告
  4. ExtJS中listener方法和handler方法的区别
  5. This is the default error page for nginx that is distributed with EPEL.
  6. sublime text3 常用配置
  7. Eclipse创建Java项目时提示Open Associated Perspective?
  8. JavaBridge install in ubuntu
  9. 通过I2C总线向EEPROM中写入数据,记录开机次数
  10. 在线渐变配色网站分享
  11. VS(visual studio)中使用ReportViewer控件和报表设计器 RDLC
  12. 什么是雷曼时刻(Lehman Moment)
  13. WebRTC语音对讲无声音
  14. selenium中的三种等待方法
  15. w ndows10摄像头设置,windows10系统电脑摄像头怎么打开
  16. 瑞吉外卖项目的购物车sub操作
  17. 2023年,如何管理你的绩效目标?
  18. 如何剪切视频,只截取视频中间的一部分
  19. 入门 | 我们常听说的置信区间与置信度到底是什么?
  20. python 斗破苍穹 词云

热门文章

  1. AAAI 2020 提前看 | 三篇论文解读问答系统最新研究进展
  2. 【STC15控制WS2812 RGB彩灯级联】
  3. Spring学习(二)—— 对象创建方式及依赖注入
  4. A53开发板命令操作wifi-wap主要命令
  5. 谷歌翻译服务退出中国大陆,使用SwitchyOmega仍需要全文翻译,恢复访问的方法
  6. Vivado 2020.1 and 2020.2 错误 arm-none-eabi-ar: *.o: Invalid argument
  7. Ubuntu安装ros rotors 以及中间出现的问题的解决办法
  8. Linux服务器批量管理工具 - TeamRemote
  9. Android 项目接入网易云信IM单聊,群聊
  10. 猿人学试题(非常简单js混淆、雪碧图、样式干扰 css加密、js混淆源码乱码、js混淆动态cookie、访问逻辑)