🔥JVM从入门到入土之JVM的运行时数据区

前言

文本已收录至个人GitHub仓库,欢迎Star:github.com/bin39232820…
种一棵树最好的时间是十年前,其次是如今
我知道不少人不玩qq了,可是怀旧一下,欢迎加入六脉神剑Java菜鸟学习群,群聊号码:549684836 鼓励你们在技术的路上写博客java

絮叨

前面的基础写完了,接下来也是很重要的一部分,把数据加载到内存中,每种数据加载到哪一个位置呢?git

概述

对于 C C++ 来讲,在内存管理领域,他们既拥有最高的权利的皇帝,可是同时他们又是从事最基础工做的劳动人员,由于他们担负着每个对象从开始到结束的维护责任,程序员

对于Java来讲,再虚拟机自动内存管理的帮助下,再也不须要为每个new操做去分配内存,不容易出现内存泄漏和内存溢出的状况,可是由于咱们Java程序员 不用管理内存,因此一旦出现内存问题,很容易让咱们手忙脚乱,因此呢咱们必需要了解Java虚拟器的内存管理机制,以便咱们能更好的处理各类各样的问题github

运行时数据区

Java虚拟机在执行Java程序的过程当中会把所管理的内存划分为若干个不一样的数据区域,这些区域都有各自的用途,以及建立和销毁时间。再Java 1.8中 从宏观上来讲分为线程共享,和线程私有 主要是分为如下几个区域算法

程序计数器

特色:线程内存独享,占用内存小,生命周期与线程相同(随线程诞生而诞生,随线程消亡而消亡)数组

功能:当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复(cpu在不断轮询执行任务)等基础功能都须要依赖这个计数器来完成安全

异常:该区域没有定义异常markdown

Java虚拟机栈

特色:先进后出,线程内存独享,生命周期与线程相同多线程

单位:栈帧jvm

功能:已先进后出执行方法体的方法,执行完成的栈帧出栈

例子

虚拟机压栈的过程

结论

  • 一个线程 表示的是一个Java虚拟机栈
  • 一个方法的执行,能够经过压栈的方式,也就是 方法对应的是栈帧

接下来咱们来谈谈栈的基本单位栈帧吧

栈帧(每个方法对应一个栈帧)

只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧 (Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method) 组成:

  • 局部变量表
  • 操做数栈
  • 动态连接
  • 方法出口信息
1.局部变量表:由基本数据类型和对象引用组成的

做用:用来存储方法中的局部变量
基本单位:slot

  • 局部变量表的大小在编译器就能够肯定其大小了,所以在程序执行期间局部变量表的大小是不会改变的。
  • 若是存储的是基本数据类型那么直接存储值
  • 若是存储的是对象引用那么存储对象的引用地址( reference)(堆中)
补充:比较reference的两种实现方式

直接引用 vs 使用句柄池

直接引用

reference直接指向对象,对象中指向对象类型数据

优势:速度快,节约指针开销。HotSpot采用的主要方式

使用句柄池:

java堆中会维护一个句柄池,句柄池分别指向对象实例(堆)的和对象类型数据(方法区)

优势:对象移动后只需改变句柄池的指向地址,而不须要改变引用的指向地址。稳定

其实用白话来讲 就是2我的是直接本身单线联系,仍是经过一个第三方联系,本身并不知道本身要联系的是谁,这个再抗战特务剧中很常见呀。

2.操做数栈

操做数栈的深度在编译器就能够肯定其大小了,所以在程序执行期间局部变量表的大小是不会改变的。

功能:实现程序功能

3.动态链接

补充下直接引用与符号引用

  • 直接引用:当类已经加载到虚拟机时,经过地址直接调用该类
  • 符号引用(常量池中):在编译的时候还不知道类是否被加载,先用符号代替该类,等实际运行时再用直接引用替换间接引用。

静态解析:符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用

动态链接: 将在每一次的运行期期间转化为直接引用

4.方法出口信息

当一个方法执行完毕以后,要返回以前调用它的地方,所以在栈帧中必须保存一个方法返回地址。

本地方法栈

  • 大致上都相似于虚拟机栈
  • 不一样点:栈执行的java方法服务
  • 本地方法栈执行的是Native方法(不必定是用java开发的)服务

Java堆

特色:存储对象,线程间内存共享,占用大量内存,垃圾回收关注的重点区域

异常:OutOfMemoryError

每次都向堆中存放对象,方法结束后,销毁栈帧的局部变量表时同时销毁引用,该对象就成了可回收的垃圾。咋看起来没什么不对呀,但是仔细思考下仍是存在两个问题 1.不断的来回增长删除对象,对于GC的工做量太大。 2.java使指针碰撞(堆中存入新对象的时候,指针根据对象大小移动到相应位置)来为对象分配内存。若是在多线程的环境下,就会出现两个对象同时移动当前前指针的状况,形成线程不安全的状况。

这里就要引入TLAB的概念了

TLAB的全称是Thread Local Allocation Buffer,这是一个线程专用的内存分配区域。每一个线程都会从Eden分配一块空间,当线程销毁时,咱们天然能够回收掉TLAB的内存。

使用TLAB指令 -XX:UseTLAB

优势:线程安全,减小垃圾回收的压力。

缺点:TLAB空间大小是固定的,面对大对象的时候不够灵活

方法区

特色:存储类,线程间内存共享

存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

异常:OutOfMemoryError

提到方法区不得不说的就是运行时常量池

补充:方法区不是永久代,只是Hotspot的实现方式而已。

远行时常量池

运行时常量池是方法区的一部分,Class文件除了有类的版本,字段,方法,还有常量池

Java虚拟机对class文件每一部分的格式都有严格规定,每个字节用于存储哪一种数据都必须符合规范才会被jvm承认。但对于运行时常量池,Java虚拟机规范没作任何细节要求。

运行时常量池有个重要特性是动态性,Java语言不要求常量必定只在编译期才能产生,也就是并不是预置入class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种特性使用最多的是String类的intern()方法。

既然运行时常量池是方法区的一部分,天然受到方法区内存的限制。当常量池没法再申请到内存时会抛出outOfMemeryError异常。

对象的建立

### 当虚拟机遇到一条New指令时:会进行以下步骤

  • 检查指令的参数(即工做中咱们New的对象),可否在常量池中找到它的符号引用。
  • 若是存在,检查符号引用表明的类是否被加载、解析、初始化过。(若是没有则执行类的加载-----相关加载过程参考我前面的文章类加载机制)。
  • 加载经过后,虚拟机将为新生对象分配内存。(所需内存大小在类加载完成后即可肯定)

两种内存分配的方式:

  • 指针碰撞:假设Java堆中的内存是绝对规整的,全部用过的内存都放在一边,空闲的内存放在另外一边。中间放着一个指针做为分界点的指示器,分配内存就仅仅是把指针往空闲空间那边挪动一段与对象大小相等的距离。这种方式则属于指针碰撞。

  • 空闲列表:若是堆中的内存并非规整的,已使用的内存和空闲内存相互交错,显然没法使用指针碰撞。虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新记录表上的数据。这种方式属于空闲列表。

具体选择哪一种分配方式由Java堆决定,而Java堆是否规整,则有GC收集器决定。所以使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞。而使用CMS这种基于Mark-Sweep算法的收集器时,一般采用的空闲列表。

如何保证分配内存时线程的安全性

  • 对分配内存的动做进行同步处理(实际上虚拟机采用CAS配上失败重试的机制保证了更新操做的原子性)
  • 把分配内存的动做按照线程划分在不一样的空间之中进行(即每一个线程在Java堆中预先分配一小块内存(称为本地线程分配缓冲))。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的布局能够分为3块区域:对象头,实例数据和对齐填充

对象头包括两部分信息:

  • 存储对象自身的运行时数据(如:哈希码、GC分代年龄、锁 等)
  • 类型指针(即对象指向他的类元数据的指针,虚拟机根据此指针来确认对象属于哪一个类的实例)
  • 若是是数据 记录数组的大小 实例数据:
  • 实例数据才是对象真正存贮的有效信息(即程序中所定义的各类类型的字段内容)。

对齐填充:

  • 不是必然存在的,仅仅起到占位符的做用,由于HotSpot虚拟机要求对象的起始地址必须是8个字节的整数倍。

来个例子把JVM的运行时的区域所有串起来

结尾

明天就是 咱们大头 垃圾回收算法,和垃圾回收器了,而后咱们再搞几个实战,对于JVM也算是有一个基础的认识了,以后就要靠你们本身多去累计实战经验了。

平常求赞

好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是真粉

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见

六脉神剑 | 文 【原创】若是本篇博客有任何错误,请批评指教,不胜感激 !

相关文章
相关标签/搜索