搜网站的关键词,长沙网站优化掌营天下,做三轨网站犯法吗,北京网站建设怎么样前言
本文是JVM系列的内存模型篇#xff0c;参考资料为《深入理解Java虚拟机》#xff0c;本文章将会以HotSpot 虚拟机为介绍基础。
1.JVM简单介绍
Java Virtual Machine是运行Java程序的基础#xff0c;JVM基于C、C实现#xff0c;JVM有很多种类#xff0c;但是这些虚…前言
本文是JVM系列的内存模型篇参考资料为《深入理解Java虚拟机》本文章将会以HotSpot 虚拟机为介绍基础。
1.JVM简单介绍
Java Virtual Machine是运行Java程序的基础JVM基于C、C实现JVM有很多种类但是这些虚拟机都必须按照《Java虚拟机规范》来进行实现。目前JDK使用的是HotSpot虚拟机。
2.JVM内存模型
根据《Java虚拟机规范》的规定Java虚拟机所管理的内存将会包括以下几个运行时数据区域
程序计数器Java虚拟机栈本地方法栈方法区堆
分布如下图
3.程序计数器
程序计数器是Java中占用内存比较少的一个区域他的作用是记录当前线程所执行的字节码的行号指令通俗的理解就是代码执行到哪里了。
我们很容易思考到在多线程中是发生线程切换这种情况的那么一个线程被切换后它的状态就需要被记录到上下文中方便线程能正确执行到原来的位置那么为了记录这个位置就需要程序计数器来进行实现
为了线程切换后能恢复到正确的执行位置每个线程都需要一个独立程序计数器各个线程之间计数器互不影响独立存储。因此它也是“线程私有”的内存
这片区域也是唯一一个在《Java虚拟机规范》中没有任何OutOfMemoryError情况的区域。
4.Java虚拟机栈
Java虚拟机栈以“栈”命名的在内存模型中基本都是用来处理方法的所以Java虚拟机栈是用来处理Java语言实现的方法的。同理的这个栈也是线程私有的。他的生命周期与线程生命周期一样长。
一个线程在调用方法的时候会在虚拟机栈中创建一个栈帧这个栈帧会存放局部变量表、操作数栈、动态连接、方法出口等信息。 栈帧包含以下内容 局部变量表 栈帧用于存储方法的局部变量表中存放了编译期可知的基本数据类型和对象引用对象引用指针或者句柄。这些局部变量在方法调用时分配内存空间并在方法调用结束后被释放。 操作数栈 栈帧还包含一个操作数栈用于存储方法执行时的操作数。当方法需要进行计算或操作时操作数会被入栈或出栈。 动态链接 栈帧包含指向运行时常量池中当前方法引用的指针用于在方法中访问其他类或方法。 方法出口 当方法调用完成后程序需要返回到方法调用的地方继续执行。栈帧包含方法返回地址用于记录返回的位置。
额外提一嘴的是当Java虚拟机栈的深度被方法调用填满的时候就会出现StackOverFlowError如果栈的大小动态扩展到没办法扩展的时候会报OOMOutOfMemoryError的错误。
5.本地方法栈
这个栈和Java虚拟机栈是一样的功能但是作用的对象不一样Java虚拟机栈对应的是Java方法而本地方法栈对应的是被Native标志的方法这类方法一般都是C、C代码。其他东西基本和Java虚拟机栈一致。
6.方法区
方法区与Java堆一样是各个线程共享的内存区域这块区域是用来存储已经被加载的类元信息这些信息包含类型信息、常量、静态变量、即使编译后的代码缓存等信息。
6.1永久代与元空间
早在JDK1.8以前方法区使用的永久代的实现方式而在1.8后才正式确定使用元空间。那么二者实现上有什么区别呢
最大的区别就是前者是使用的虚拟机内存后者使用了直接内存也就是说永久代的内存大小受JVM限制而元空间内存大小受真实机子内存大小限制明显后者内存大小更大前者更容易OOM。
在方法区使用元空间后字符串常量池也从方法区移动到了堆内存中。
6.2运行时常量池
提到方法区就不得不提到一个叫运行时常量池的东西它也是方法区的一部分。
一个Class文件除了有类的版本、字段、方法、接口等描述信息外还有一项信息是常量池表用于存放编译期生成的各种字面量与符号引用这部分内容将在类加载后存放到方法区的运行时常量池中。
7.堆
堆内存是整个虚拟机中最大的一块这块区域是被线程共享的这块对象就是用来在程序执行时大部分对象存放的地方还有极小一部分可能会发生逃逸分析在栈上创建和销毁。
堆这块区域也是最容易发生OOM的地方原因可想而知公共的地方大家都来这里放东西时间一长没有空间也很正常所以这块区域也是发生GCGarbage Collected频率最高的一个场所。具体GC流程下篇文章会详细介绍
7.1 对象创建
堆中的对象普通对象创建过程也是比较讲究的下面我们带着问题一步一步理解这个过程
首先如何创建 很简单的new关键字 那么问题又来了对象创建依赖的信息从哪里来 当Java遇到一条字节码new指令的时候首先将去检测这个指令能否在常量池中定位到一个类的符号引用并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有那么必须先执行相应的类加载双亲委派模型。 对象依赖信息得到后内存大小该如何划分分配 当一个类被加载之后相应的对象创建所需的内存大小也就能被确定了。那么要在堆中创建对象就需要划分空间JVM中有两种划分空间的方式分别是“指针碰撞”和“空闲列表” 指针碰撞 假设Java堆中内存分配绝对规整使用过的和未使用的分成两边只需要在边界设置指针这个指针只需要挪动和对象大小一样的距离即可这种就是指针碰撞空闲列表 假如Java堆内存并不规整使用过的和未使用的都混在一起这种情况要分配内存就只能维护一个列表这个列表记录了哪些内存可以使用分配内存就需要在表中查找到足够到的区间进行分配即可这就是空闲列表 并发下对象创建的内存分配安全如何得到保证 为我们所知的堆内存是一个线程共享的这就意味着我们堆在划分内存大小的时候可能会出现线程安全问题。可能出现线程1在给A分配大小的时候还没来得及修改指针但是线程2在创建B时使用了这个指针就导致了内存数据被改写了。解决这个问题有两个方式 加锁同步 实际实现中虚拟机是采用CAS失败重试的方式保证更新操作的原子性TLABThread Local Allocation Buffer本地缓冲区也和ThreadLocal一样给每个线程各自划分好区域线程要创建对象就在这个区域内创建就行如果TLAB使用完了才需要进行同步锁定分配对象。如果JVM要使用TLAB可以通过-XX:/-UseTLAB参数来设定 实际上内存分配成功之后虚拟机还会对分配到的内存空间不包括对象头进行初始化工作零值处理。这步操作是为了保证对象实例字段在Java代码中可以不赋值就能直接使用。 经历以上步骤对象创建后对象还需要设置什么 需要设置“对象属于哪个类的实例”、“类的元数据信息“”、“对象hash码实际调用Object::hashCode才会生成”、“GC分代年龄”这些信息都被描述在对象头中 最终 在上面工作都完成后看似一个对象已经被创建了但实际上整个生命过程还差一步即初始化构造函数中的初始化工作还没有被真正执行也就是 init ()方法所以值都是默认为零值的所以当构造函数执行完成后一个对象就被完成创建了。 7.2 对象的内存布局
在了解一个对象的创建过程后我们来看看一个对象内部布局是如何的直接看下图 对象头这部分包含了两部分信息
第一部分HashCode、GC分代年龄、锁状态标记、线程持有的锁、偏向锁ID、偏向锁时间戳等信息等这部分信息官方称之为Mark Word这部分数据在32位和64位虚拟机未开启指针压缩中分别占用32bit和64bit。第二部分类型指针即对象指向它的类型元数据的指针Java虚拟机通过这个指针来确定这个对象是哪个类的实例。如果是数组对象对象头中还会记录数组长度如果不是则无记录。
实例数据这部分数据是对象真正存储的有效信息
对齐填充这部分的内容不是必然存在的也没有特殊含义这部分的主要作用就是保证这个对象大小是8字节的整数倍差多少尽可能补多少。
JVM执行流程 代码编译Java源代码通过Java编译器javac编译成字节码文件.class文件。 类加载JVM的类加载器将字节码文件加载到内存中并进行校验、准备、解析等处理。 内存分配JVM为加载的类分配内存包括方法区、堆、栈等。 初始化JVM对类进行初始化包括静态变量的赋值、静态代码块的执行等。 执行JVM开始执行字节码指令逐行读取字节码文件并执行。这个执行过程交给执行引擎将字节码翻译成CPU指令交给操作系统去执行 …
END 以上是本文全部内容希望对你有所帮助