一:内部结构
一个进程对应一个jvm实例,一个运行时数据区,又包含多个线程,这些线程共享了方法区和堆,每个线程包含了程序计数器、本地方法栈和虚拟机栈接下来我们通过一个示意图介绍一下这个空间。
如图所示,当一个helloword程序编译成为可以运行的二进制编码程序helloword.class程序的时候,运行二进制文件,整个进程会放到一个本地电脑的内存环境中去,其中,一个程序就是一个JVM实例,代码中包含定义方法的方法区域,开辟空间的堆空间区域,以及控制好程序的运行走向的程序计数器,以及方法调用时候用到的本地方法栈,虚拟机栈等,在代码运行的过程中我们知道代码会产生很多的垃圾,这些垃圾是需要JVM中的垃圾回收器去回收的她不像C或CPP那样手动的垃圾清除,在JAVA中一个JVM空间会有一个GC垃圾回收器,通过各种有效的算法帮我们实现垃圾的自动清除,后面我会介绍GC如何实现垃圾回收机制的。
二:程序计数器
程序计数器是什么?相信大家第一次听到这个话题的时候应该很敏感,其实博主是嵌入式转JAVA的所以对于底层还是颇有了解的。
程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号),简单来说就是记住我现在是哪个线程在具体的运行,保存一下
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。(这里我来说明一下如下图所示,线程轮转简单来说就是时间片轮转,因为对于单核处理器而言,处理并发线程,就是在分配时间片给到每个并发线程不停地切换任务,所以所谓的并发并不是真的同时进行,只是无限接近并发而已,但是我要说明一下时间片轮转速度是非常快的,很多人疑问这个轮转速度是由什么保证的呢?----这就要考虑到硬件支持了,博主也是电子专业对其有一些了解,下次我再给你们介绍,现在画个饼哈哈哈)因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储(如图所示,程序计数器及记录着每个线程运行到哪了,这样下次时间片轮转再次轮到你,你可以接着运行),称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
不知道通过上面的例子大家有没有理解,不理解的话我下面举个例子:
线程A在看直播,突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会接电话,然后,视频电话结束,这时线程A究竟该干什么? (线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:程序计数器来记录)这样视频电话结束了以后又可以回到直播继续播放
三:栈
java虚拟机是线程私有的,它的生命周期和线程相同
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 如图所示,其中每个栈信息的属性解释
局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型(returnAddress中保存的是return后要执行的字节码的指令地址)
操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方(意思就是要在方法中转化到另外一个方法中去,这样就需要动态衔接)
出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落
如何设置栈的内存大小?
使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 (IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k)
思考:
一个方法调用另一个方法,会创建很多栈帧吗?
答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
栈指向堆是什么意思?
栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址,堆中的数据等下讲
递归的调用自己会创建很多栈帧吗?
递归的话也会创建多个栈帧,就是一直排下去,沒有結束条件就会产生oostack的风险,下面的例子就是爆栈的实际例子:
public class StackOverFlowErrorTest { public static void main(String[] args) { //递归调用main main(args);} }
四:堆
java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例
在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配
java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”(后面在说GC垃圾回收机制的时候你就会明白这个名词)
从内存回收角度来看java堆可分为:新生代和老生代
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存(其实都在堆区域,只不过为了加快回收算法就细化了一些区域,毕竟堆空间区域很大)
根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中(正因为这个不连续性所以我们堆空间的位置都需要一个指针去维护它,不然很容易丢失,导致堆空间无法释放,内存泄漏)。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
堆的细分内存结构
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速(存入新生代)
另外一类对象时生命周期非常长,在某些情况下还能与JVM的生命周期保持一致 (存入老年代)
Java堆区进一步细分可以分为新生代(YoungGen)和老年代(OldGen),新生代可以分为伊甸园区(Eden)、新生区1(from)和新生区2(to)
JDK8及以后
为什么要把Java堆分代?不分代就不能正常工作了么?
经研究,不同对象的生命周期不同。70%-99%的对象都是临时对象,新生代:有Eden、Survivor构成(s0,s1 又称为from to),to总为空,老年代:存放新生代中经历多次依然存活的对象。
其实不分代完全可以,分代的唯一理由就是优化GC性能。 如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。 GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描,而很多对象都是朝生夕死的。 如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
永久代为什么要被元空间替换?
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
这项改动是很有必要的,原因有:
(1)为永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生Perm区的O0M,比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。 “Exception in thread’ dubbo client x.x connector’java.lang.OutOfMemoryError: PermGenspace”而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
(2)对永久代进行调优是很困难的