虚拟机篇 --- 浅谈JVM内存结构

前言

Java引以为傲的就是其内存管理机制,相比C++的手动内存管理、复杂难以理解的指针等,Java程序写起来方便的多。
本文中所说的JVM均是HotSpot版本

JVM运行时数据区(内存区域划分)

在这里插入图片描述
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区,包括:
虚拟机栈(VM Stack)
本地方法栈(Native Method Stack)
程序计数器(Program Counter Register)
方法区(Method Area)
堆(Heap)

线程隔离的数据区域

虚拟机栈

虚拟机栈是在JVM运行过程中存储当前线程运行方法所需要的数据、指令和返回地址

虚拟机栈的缺省大小是1M,可用参数 -Xss调整大小,例如 -Xss512k
在这里插入图片描述
JVM参数参考

Java虚拟机是基于线程的,每个方法都是在线程中运行,在线程的生命周期中,参与计算的数据会频繁的入栈和出栈,栈的生命周期是和线程的生命周期是一样的。

栈(虚拟机栈)里的每条数据,成为栈帧。
在每个Java方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,就出栈,当所有的栈帧都出栈后,线程也就结束了。
在这里插入图片描述
每个栈帧,都包括4个区域:
局部变量表
操作数栈
动态连接
返回地址

局部变量表

局部变量表用于存放局部变量。
局部变量表的长度是32位,主要存放Java的八大基本数据类型(int、short、long、byte、float、double、boolean、char),一般32位就可以存下,如果是64位就使用高低位占用两个存放;如果是局部的对象,只需要存放对象的引用地址。

操作数栈

操作数栈也是栈的结构,用于存放方法执行的操作数的,操作数栈操作的元素可以是任意的Java数据类型。
一个方法刚开始执行的时候,方法的操作数栈是空的,然后JVM一直执行入栈和出栈操作。

动态连接

Java语言特性多态,需要运行时才能确定具体的方法。

返回地址/完成出口

如果是正常返回,调用程序计数器中的地址作为返回
如果是异常返回,通过异常处理器表来确定

本地方法栈

本地方法栈用于管理native方法的调用

本地方法栈跟虚拟机栈功能类似,只是当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态连接并直接调用native方法,程序计数器也不会记录

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

程序计数器

记录当前线程正在执行的字节码指令的地址。分支、循环、跳转、异常、和线程恢复等都依赖程序计数器

由于Java是多线程的语言,当执行的线程数量超过CPU核心数的时候,线程之间会根据时间片轮转调度算法争夺CPU资源。此时如果一个线程的时间片用完了,或者是其它原因导致这个线程的CPU资源被提前抢夺,那么这个退出的线程需要一个单独的程序计数器,来记录下一条要运行的指令。

线程共享的内存区域

堆是JVM上最大的内存区域,几乎所有的对象存储在堆上(前提是触发JVM逃逸分析,对象不满足方法逃逸就在栈上存储),数组也存储在堆上。

堆空间的大小是程序启动的时候就申请的,但是不一定会全部使用
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;

堆是垃圾回收器(Garbage Collection 简称GC)的操作对象。随着对象的频繁创建,堆空间占用会越来越多,GC就会不定期对不在使用的对象进行回收。

方法区

方法区主要用来存放已被虚拟机加载的类相关信息,包括
类信息
静态变量
常量
运行时常量池
字符串常量池

常量池(Constant Pool Table):用户存放编译期间生成的各种字面量和符号引用
字面量:包括字符串、基本类型的常量(final修饰的变量)
符号引用:包括类和方法的全限定名(全类名)、字段的名称和描述符以及方法的名称和描述符

JVM在执行某个类的时候,必须先加载类(类的加载过程如下)
在这里插入图片描述
加载类的时候,JVM会先加载Class文件,Class文件中除了包含有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池。
当类加载到内存中后,JVM会将Class文件常量池里的内容存放到运行时常量池中,然后在解析阶段,JVM会将符号引用替换为直接引用(对象的索引值)。
(运行时常量池是全局共享的,多个类公用一个运行时常量池,常量池中多个相同的字符串在运行时常量池只会存在一份)

另外值得注意的是:
1.方法区在Hotspot虚拟机的不同版本有不同的实现
在JDK1.7以及其以前叫永久代,在JDK1.8以及其以后叫做元空间
2.方法区只是虚拟机规范中的一部分,并不是所有的虚拟机都有永久代一说,永久代只是Hotspot虚拟机对方法区的一种实现,Oracle的JRockit和IBM的J9就不存在永久代的概念。
3.元空间和永久代的区别,为什么用元空间代替永久代

Java 1.7之前方法区的划分还是跟堆是有关系的,划分堆里的新生代老年代,然后再以这样的规范划一个区域叫做永久代,但是有一个问题,1.7以前不单单要回收堆,还有回收方法区(永久代),方法区中存的是常量,静态变量等,回收效率低,成本比较高,划分了这么多区域,gc回收的时候并没有分别对待,jdk1.8以后另一种实现叫做元空间,元空间可以直接使用机器的内存,默认情况下不受堆的限制(1.7受制于堆的大小),可以使用直接内存(堆外内存)
好处:是方便拓展,随着永久代存的内容不断的添加,不会因为堆的参数的限制,造成OOM
坏处:可能会挤压堆的空间,机器加入只有10个G,元空间占了太多的话,堆可分配的大小就会减少,而用于设置堆中可分配内存的上线和下线这两个参数-Xmx和-Xms就没用了

在Oracle收购了两家JVM公司,产品一个是Hotspot JVM一个是JRocket JVM,移除永久代是因为JRockit JVM没有永久代,JRockit的性能还挺高,所以推出JDK1.8的时候,就剔除了永久代

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

4.堆和方法区都是线程共享的,为什么不合在一起,为什么用两个区域
因为方法区难回收 ,放的是类信息,常量 静态变量 即时编译期编译后的代码,回收的难度相当大,对象和数组都是频繁回收,动态创建,所以分开来放,便于垃圾回收的高效(动静分离)

栈和堆的区别

1.功能上:
①栈以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型变量(局部变量表中存储),以及其对象的引用变量,其内存分配在栈上吗,变量出了作用域就自动释放;
②堆用来存储Java中的对象,无论成员变量、局部变量还是类变量(静态变量),他们指向的对象都存储在堆内存中。
2.线程上:
①栈内存是线程私有的,归属于单个线程,每个线程都会有一个栈内存,其存储的变量(局部变量)只能在其所属的线程中可见。即,栈内存可以理解为线程的私有内存;
②堆内存是线程共享的,堆内存中的对象对所有线程可见,可以被所有线程访问。JVM进程结束以后,销毁。
3.空间上:
栈内存小于堆内存,以Hotspot为例,栈内存最大1M,栈的深度是有限的,可能会发生StackOverFlow

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值