JVM系列之——JVM内存管理

JAVA 语言相对于其他有许多优势和优点,其中一个优点便是JAVA开发者只需要关注代码,而不需要关注内存分配与回收的细节。
因为对于不同数据需要有不同的管理策略,所以JAVA虚拟机将内存划分成不同的区域,各个区域有不同的用途以及管理策略。有些进程随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
在这里插入图片描述

一、程序计数器

程序计数器是当前线程所执行的字节码的行号指示器。通俗一点来说,程序计数器是用来控制线程的指令执行的。
JAVA的多线程也是通过轮流切换处理器(单核)的执行时间来实现的。所以,程序计数器记录线程下一步要执行的指令的地址。当该线程分配到处理器时间片时,处理器会执行指令地址处的指令。所以程序计数器是线程隔离的,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响。

二、虚拟机栈

JAVA虚拟机栈主要用于存储与线程相关的数据结构,栈的数据结构特性(先入后出),只有与程序指令顺序有关的变量存放在栈中每个线程维护一个单独的栈,每个栈的基本单位为栈帧每个方法在执行的同时都会创建一个栈帧,每个方法的调用直至执行完成的过程都会对应一个栈帧在栈中入栈和出栈。
在这里插入图片描述
每个栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表存储在方法内声明的编译器可知的各个变量(基本类型和引用类型)
例如:
Student s=new Student();
int a=10;
基本类型变量a和引用类型变量s都会存储在局部变量表中。
栈结构还控制了变量的作用域
每个方法的调用伴随着一个栈帧的入栈,所以栈顶的栈帧也叫当前栈帧,说明程序指令到该方法内,也说明该方法是最内层的方法。
例子:
在这里插入图片描述
如图所示的程序的栈结构即为:
在这里插入图片描述

三、本地方法栈

本地方法栈与虚拟机栈非常相似,区别在于虚拟机栈为虚拟机执行JAVA方法,本地方法栈则为虚拟机使用的native服务。

四、JAVA堆

堆是JAVA虚拟机管理的最大的一块内存区域,所有线程共享,即不是线程隔离的,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配
所以现在有一个创建对象实例并赋值给引用变量的语句例子:
Stuent s=new Student(18,“jon snow”);
那么在虚拟机中的栈和堆的存储结构为:
在这里插入图片描述
变量S是一个引用类型,存储对象实例的地址。对象实例的实际存储在堆。
所以,该语句的执行为:
(1)在栈帧的局部变量表声明一个引用类型变量S
(2)在堆中分配内存存放Student类的对象实例
(3)将对象实例的地址赋值给S,这样程序就可以操作对象实例。

前面说到,堆是线程不隔离的,所以所有线程共享堆。刚才描述的是单线程下创建对象实例,并且没有详细描述对象创建的过程。那么在文章后边会阐述对象创建的过程以及对象在堆中的内存分布。

另外因为对象实例都存放在堆中,所以JAVA堆是JAVA虚拟机垃圾回收的主要区域。
涉及到垃圾回收,堆被细分为新生代和老年代,新生代还可以细分为Eden空间、From Survivor空间、To Survivor空间。

五、方法区

方法区也是线程共享的,即线程不隔离的。方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区存储的一般是伴随整个程序的或者变化不频繁或者不变化的数据,生命周期很长。所以一般方法区不涉及垃圾回收或者称为永久代。一般方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。

注:运行时常量池在的位置存在争议,在JDK1.6常量池在方法区,JDK1.7在堆中,JDK1.8在元空间(直接内存)。

六、运行时常量池

常量池主要存放编译期生成的各种字面量以及符号引用
字面量是指:例如语句String a=“jon snow”;
那么"jon snow"这个字符串是存储在常量池中的,称为字面量,变量a指向这个字面量。
符号引用是指:
每个对象都有指向他的类的指针引用,这个引用一开始是符号引用,如:String,符号引用到直接地址引用的映射存放在常量池中,类加载的解析阶段会将符号引用转换为直接引用。

七、直接内存

直接内存是堆外内存,JAVA程序可以借助本地Native方法直接操作堆外内存。

下面再详细阐述一下堆中内存创建以及内存结构

堆中对象的创建以及对象的内存布局

一、对象的创建

在JAVA堆中,对象的创建就是在JAVA堆中分配一段确定的内存空间供对象实例使用。给对象分配内存空间的方式有两种:(1)指针碰撞(3)TLAB缓冲

1.指针碰撞
指针是一个指示器,挪动指针指定分配内存空间,使用指针碰撞也要考虑两种情况:
(1)内存分配规整,即已用空间和未用空间是绝对规整的:
在这里插入图片描述
这种情况下每次分配空间只需要让指针向后移动确定大小长度(对象占用空间可以确定),然后空间分配给对象实例,这称为简单的指针碰撞。这种情况对内存空间布局有要求,即**已分配空间和未分配空间是绝对规整并且分离的,这也间接说明堆得垃圾回收算法是有整理功能的。**因此像Serial和ParNew这种带有整理的算法的收集器可以使用简单的指针碰撞。

(2)内存分配不规整,即已分配空间和未分配空间是交错的。
这个时候给对象分配内存就不能简单地移动指针了,而是维护一个空闲列表,记录哪块儿空间是可用的,每次创建对象都要从列表找出空间内存分配给对象实例。
使用CMS这种采用标记清除算法的收集器时,可以使用这种方法。
2.指针加锁、TLAB本地线程分配缓冲
前边说到的都是单线程下的分配内存空间,当场景切换为多线程时,就会出现多线程并发需要考虑的问题:即两个线程同时使用指针会引发混乱。
解决方案:
(1)对指针加锁。
对分配内存的指针加锁,这样就可以防止出现混乱,但也会有分配内存较慢的缺点。
(2)使用TLAB,给每个线程都会分配一块较大的线程私有内存空间,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完分配新的TLAB时,才会同步锁定。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值