Java-基础-05-JVM-3-运行时数据区

1 运行时数据区

运行时数据区的定义:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域 Java引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。 所以要深入理解JVM必须理解内存虚拟化的概念。 在JVM中,JVM内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等。 同时按照与线程的关系也可以这么划分区域: 线程私有区域:一个线程拥有单独的一份内存区域。 线程共享区域:被所有线程共享,且只有一份。 这里还有一个直接内存,这个虽然不是运行时数据区的一部分,但是会被频繁使用。你可以理解成没有被虚拟机化的操作系统上的其他内存(比如操作 系统上有 8 G内存,被JVM虚拟化了 3 G,那么还剩余 5 G, JVM是借助一些工具使用这 5 G内存的,这个内存部分称之为直接内存)

虚拟机栈是用来存放线程运行 java 方法所需的数据,指令、返回地址。 其实在我们实际的代码中,一个线程是可以运行多个方法的。 比如:

这段代码很简单,就是起一个main方法,在main方法运行中调用A方法,A方法中调用B方法,B方法中运行C方法。 我们把代码跑起来,线程 1 来运行这段代码, 线程 1 跑起来,就会有一个对应的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。 比如main开始运行,打包一个栈帧送入到虚拟机栈。

C方法运行完了,C方法出栈,接着B方法运行完了,B方法出栈、接着A方法运行完了,A方法出栈,最后main方法运行完了,main方法这个栈帧就 出栈了。 这个就是Java方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而每一个方法对应一个栈帧。

1.1 虚拟机栈

  • 栈的数据结构:先进后出(FILO)的数据结构,

  • 虚拟机栈的作用:在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址。

虚拟机栈是基于线程的:哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生 命周期是和线程一样的。 虚拟机栈的大小缺省为 1 M,可用参数–Xss调整大小,例如-Xss 256 k。 参数官方文档(JDK 1. 8 ):docs.oracle.com/javase/ 8 /docs/technotes/tools/unix/java.html

栈帧:在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。 栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)

1 、局部变量表 :

顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个 32 位的长度,主要存放我们的Java的八大基础数据 类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。

2 、操作数据栈 :

存放java方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型, 所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。 操作数栈本质上是 JVM 执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。

3 、动态连接 :

Java语言特性多态(后续章节细讲,需要结合class与执行引擎一起来讲)。

4 、返回地址 :

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定) 同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是 1 M。 如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会爆掉。

1.2 程序计数器

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。

程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

由于Java 是多线程语言,当执行的线程数量超过CPU核数时,线程之间会根据时间片轮询争夺 CPU资源。如果一个线程的时间片用完了,或者是其

它原因导致这个线程的 CPU资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

因为JVM是虚拟机,内部有完整的指令与执行的一套流程,所以在运行Java方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如 果是遇到本地方法(native方法),这个方法不是JVM来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器, 这个会记录本地代码的执行的地址,所以在执行native方法时,JVM中程序计数器的值为空(Undefined)。 另外程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域。

1.3 本地方法栈

本地方法栈跟Java 虚拟机栈的功能类似,Java虚拟机栈用于管理 Java函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是 用 Java实现的,而是由 C 语言实现的(比如Object.hashcode方法)。 本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。

虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。

1.4 方法区

方法区(Method Area)是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool) 字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法 方法区是JVM对内存的“逻辑划分”,在JDK 1. 7 及之前很多开发者都习惯将方法区称为“永久代”,是因为在HotSpot虚拟机中,设计人员使用了 永久代来实现了 JVM 规范的方法区。在JDK 1. 8 及以后使用了元空间来实现方法区。

1.5 栈帧执行对内存区域的影响

对class进行反汇编 javap–cXXXX.class 字节码助记码解释地址:cloud.tencent.com/developer/a… 1333540

在JVM中,基于解释执行的这种方式是基于栈的引擎,这个说的栈,就是操作数栈。

虚拟机栈:

每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。

栈的大小缺省为 1 M,可用参数 –Xss调整大小,例如-Xss 256 k 在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分 配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

局部变量表 :

顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个 32 位的长度,主要存放我们的Java的八大基础数据类型,一般 32位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。(基本数据类型、对象引用、returnAddress类型).

操作数据栈:

存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类 型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作.

动态连接:

Java语言特性多态(需要类加载、运行时才能确定具体的方法,后续有详细的讲解)动态分派.

完成出口(返回地址):

正常返回:(调用程序计数器中的地址作为返回)

三步曲: 恢复上层方法的局部变量表和操作数栈、 把返回值(如果有的话)压入调用者栈帧的操作数栈中、 调整程序计数器的值以指向方法调用指令后面的一条指令、 异常的话:(通过异常处理表<非栈帧中的>来确定)

2 JVM 整体知识模块

JVM能涉及非常庞大的一块知识体系,比如内存结构、垃圾回收、类加载、性能调优、JVM自身优化技术、执行引擎、类文件结构、监控工具等。但是在所有的知识体系中,都或多或少跟内存结构有一定的关系:比如垃圾回收回收的就是内存、类加载加载到的地方也是内存、性能优化也涉及到内存优化、执行引擎与内存密不可分、类文件结构与内存的设计有关系,监控工具也会监控内存。所以内存结构处于JVM中核心位置。也是属于我们入门JVM学习的最好的选择。同时 JVM 是一个虚拟化的操作系统,所以除了要虚拟指令之外,最重要的一个事情就是需要虚拟化内存,这个虚拟化内存就是我们马上要讲到的 JVM 的内存区域。

2.1 元空间

方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息,而这个类还没有装入 JVM,那么此时就只允许一个线程去加载它,另一个线程必须等待。 在 HotSpot虚拟机、Java 7 版本中已经将永久代的静态变量和运行时常量池转移到了堆中,其余部分则存储在 JVM 的非堆内存中,而Java 8 版本 已经将方法区中实现的永久代去掉了,并用元空间(classmetadata)代替了之前的永久代,并且元空间的存储位置是本地内存。

元空间大小参数:

jdk 1. 7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize; jdk 1. 8 以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize jdk 1. 8 以后大小就只受本机总内存的限制(如果不设置参数的话)

JVM参数参考:docs.oracle.com/javase/ 8 /docs/technotes/tools/unix/java.html Java 8 为什么使用元空间替代永久代,这样做有什么好处呢? 官方给出的解释是: 移除永久代是为了融合 HotSpotJVM 与JRockitVM 而做出的努力,因为JRockit没有永久代,所以不需要配置永久代。 永久代内存经常不够用或发生内存溢出,抛出异常java.lang.OutOfMemoryError:PermGen。这是因为在 JDK 1. 7 版本中,指定的 PermGen区大小 为 8 M,由于 PermGen中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen分配多大的空间 很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的class总数、常量池的大小和方法的大小等。

2.2 运行时常量池

运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编 译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。 运行时常量池是方法区的一部分。运行时常量池相对于Class常量池的另外一个重要特征是具备动态性(Class常量池在类加载章节会具体讲)。

2.3 堆

堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。

堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。

随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作GC(GarbageCollection)。

那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java类中存在的位置。 Java的对象可以分为基本数据类型和普通对象。 对于普通对象来说,JVM会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。 当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

堆大小参数:

  • Xms:堆的最小值;
  • Xmx:堆的最大值;
  • Xmn:新生代的大小;
  • XX:NewSize;新生代最小值;
  • XX:MaxNewSize:新生代最大值; 例如-Xmx 256 m

2.4 直接内存(堆外内存)

直接内存有一种更加科学的叫法,堆外内存。 JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。

它不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;

这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM 异常。

小结:

1 、直接内存主要是通过 DirectByteBuffer申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。

2 、其他堆外内存,主要是指使用了Unsafe 或者其他 JNI 手段直接直接申请的内存。 堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。后续章节会详细讲。

同时,要注意 Oracle 之前计划在 Java 9 中去掉 sun.misc.UnsafeAPI 。这里删除 sun.misc.Unsafe 的原因之一是使 Java 更加安全,并且有替代方案。 目前我们主要针对的 JDK 1. 8 , JDK 1. 9 暂时不放入讨论范围中,我们大致知道 java 的发展即可。

3 深入理解 JVM 的内存区域

3.1 深入理解运行时数据区

代码示例:

  1. JVM 向操作系统申请内存:

JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止 地址分配给 JVM,接下来 JVM 就进行内部分配。

  1. JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小
  • Xms 30 m-Xmx 30 m -Xss 1 m-XX:MaxMetaspaceSize= 30 m
  1. 类加载(类加载的细节后续章节会讲): 这里主要是把class放入方法区、还有class中的静态变量和常量也要放入方法区

  2. 执行方法及创建对象: 启动 main线程,执行 main方法,开始执行第一行代码。此时堆内存中会创建一个student对象,对象引用 student就存放在栈中。 后续代码中遇到new关键字,会再创建一个 student对象,对象引用 student就存放在栈中。

总结一下 JVM 运行内存的整体流程

JVM在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。

方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。

同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理的。

3.2 从底层深入理解运行时数据区

3.2.1 堆空间分代划分

堆被划分为新生代和老年代(Tenured),新生代又被进一步划分为 Eden和 Survivor区,最后 Survivor由FromSurvivor和 ToSurvivor组成。 (先需要有概念,后续对象分配和垃圾回收会细讲这块)

3.2.2 GC 概念

GC-GarbageCollection 垃圾回收,在JVM中是自动化的垃圾回收机制,我们一般不用去关注,在JVM中GC的重要区域是堆空间。 我们也可以通过一些额外方式主动发起它,比如System.gc(),主动发起。(项目中切记不要使用)

3.2.3 JHSDB 工具

JHSDB是一款基于服务性代理实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的,主要基于Java语言实现的 API集合。

3.2.3.1 JDK 1. 8 的开启方式

开启HSDB工具

Jdk 1. 8 启动JHSDB的时候必须将sawindbg.dll(一般会在JDK的目录下)复制到对应目录的jre下(注意在win上安装了JDK 1. 8 后往往同级目录下有一个 jre的目录)

 

makefile

代码解读

复制代码

然后到目录:C:\ProgramFiles\Java\jdk 1. 8. 0 _ 101 \lib进入命令行,执行java-cp.\sa-jdi.jarsun.jvm.hotspot.HSDB

3.2.3.2 JDK1.9及以后的开启方式

进入JDK的bin目录下,我们可以在命令行中使用 jhsdbhsdb 来启动它

3.2.4 代码改造

VM参数加入:

  • XX:+UseConcMarkSweepGC

  • XX:-UseCompressedOops

3.2.5 JHSDB 中查看对象
3.2.5.1 实例代码启动

因为JVM启动有一个进程,需要借助一个命令 jps查找到对应程序的进程

在JHSDB工具中attach上去

3.2.5.2 JHSDB 中查看对象

查看堆参数:

上图中可以看到实际JVM启动过程中堆中参数的对照,可以看到,在不启动内存压缩的情况下。堆空间里面的分代划分都是连续的。

再来查看对象:

这里可以看到JVM中所有的对象,都是基于class的对象

全路径名搜索

双击出现这个Teacher类的对象,两个,就是T 1 和T 2 对象。

最后再对比一下堆中分代划分可以得出为什么T 1 在Eden,T 2 在老年代

3.2.6 JHSDB 中查看栈

从上图中可以验证栈内存,同时也可以验证到虚拟机栈和本地方法栈在Hotspot中是合二为一的实现了。

当我们通过 Java运行以上代码时,JVM的整个处理过程如下:

  1. JVM向操作系统申请内存,JVM第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间。
  2. JVM获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。
  3. 完成上一个步骤后, JVM 首先会执行构造器,编译器会在.java文件被编译成.class文件时,收集所有类的初始化代码,包括静态变量赋值语句、 静态代码块、静态方法,静态变量和常量放入方法区
  4. 执行方法。启动 main线程,执行 main方法,开始执行第一行代码。此时堆内存中会创建一个 Teacher对象,对象引用 student就存放在栈中。 执行其他方法时,具体的操作:栈帧执行对内存区域的影响。栈帧执行对内存区域的影响
3.2.3 内存溢出(重点)
3.2.3.1 栈溢出

参数:-Xss 1 m,具体默认值需要查看官网:docs.oracle.com/javase/ 8 /docs/technotes/tools/unix/java.html#BABHDABI

HotSpot版本中栈的大小是固定的,是不支持拓展的。

java.lang.StackOverflowError一般的方法调用是很难出现的,如果出现了可能会是无限递归。 虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。

OutOfMemoryError:不断建立线程,JVM申请栈内存,机器没有足够的内存。(一般演示不出,演示出来机器也死了) 同时要注意,栈区的空间JVM没有办法去限制的,因为JVM在运行过程中会有线程不断的运行,没办法限制,所以只限制单个虚拟机栈的大小。

3.2.3.2 堆溢出

内存溢出:申请内存空间,超出最大堆内存空间。

如果是内存溢出,则通过 调大 -Xms,-Xmx参数。 如果不是内存泄漏,就是说内存中的对象却是都是必须存活的,那么久应该检查JVM的堆参数设置,与机器的内存对比,看是否还有可以调整的空间, 再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行时的内存消耗。

3.2.3.3 方法区溢出

( 1 )运行时常量池溢出

( 2 )方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。

注意 Class 要被回收,条件比较苛刻(仅仅是可以,不代表必然,因为还有一些参数可以进行控制): 1 、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。 2 、加载该类的ClassLoader已经被回收。 3 、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

代码示例:

cglib是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。 CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell, 也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

3.2.3.3 本机直接内存溢出

直接内存的容量可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常; 由直接内存导致的内存溢出,一个比较明显的特征是在HeapDump文件中不会看见有什么明显的异常情况,如果发生了OOM,同时Dump文件很小, 可以考虑重点排查下直接内存方面的原因。

4 对象的分配

4.1 JVM 中对象的创建过程(重点)

4.1.1 对象的内存分配

虚拟机遇到一条new指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程。 类加载就是把class加载到JVM的运行时数据区的过程。

4.1.1.1 检查加载

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用:符号引用以一组符号来描述所引用的目标),并且检查类是否已经被加载、解析和初始化过。

符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续JVM类加载会具体讲到)就是为了把这个符号引用转化成为真正的地址的阶段。 一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址。

4.1.1.2 分配内存

接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

4.1.1.2.1 指针碰撞

如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅 是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。

4.1.1.2.2 空闲列表

如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上 哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

如果是Serial、ParNew等带有压缩的整理的垃圾回收器的话,系统采用的是指针碰撞,既简单又高效。

如果是使用CMS这种不带压缩(整理)的垃圾回收器的话,理论上只能采用较复杂的空闲列表。

4.1.1.2.3 并发安全

除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

CAS 机制:

解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;

分配缓冲:

另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块私有内存,也就是本地线程分配缓冲(Thread LocalAllocationBuffer,TLAB),JVM在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer, 如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请 一块继续使用。 TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。 TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满 (分配指针top撞上分配极限end了),就新申请一个TLAB。 参数:

  • XX:+UseTLAB 允许在年轻代空间中使用线程本地分配块(TLAB)。默认情况下启用此选项。要禁用TLAB,请指定-XX:-UseTLAB。

docs.oracle.com/javase/ 8 /docs/technotes/tools/unix/java.html

4.1.1.3 内存空间初始化

(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为 0 ,boolean值为false等等)。这一步操作保证了对 象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.1.1.4 设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Javaclasses在JavahotspotVM内部表示为 类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

4.1.1.5 对象初始化

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。 所以,一般来说,执行new指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。

4.2 对象的内存布局(重点)

在HotSpot虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线 程ID、偏向时间戳等。 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 如果对象是一个java数组,那么在对象头中还有一块用于记录数组长度的数据。

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpotVM的自动内存管理系统要求对对象的大小必 须是 8 字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。

4.3 对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

句柄:

如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改.

直接指针:

如果使用直接指针访问, reference中存储的直接就是对象地址。这两种对象访问方式各有优势,使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。对SunHotSpot而言,它是使用直接指针访问方式进行对象访问的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值