1.运行时内存结构(这里以JDK8举例)
从上图中不难看出,运行时内存区域主要分为几大块,堆空间,元空间,虚拟机栈,本地方法栈和程序计数器,这里需要注意的就是其中哪些空间是线程私有的哪些空间是线程共享的
线程共享区域:堆+元空间
线程私有区域:程序计数器+本地方法栈+虚拟机栈
2.程序计数器
为什么需要程序计数器?
1.程序计数器能够保证程序/进程正常的执行下去,程序计数器的作用主要是记录下一条指令的地址,又称指令计数器
2.在程序开始执行之前,必须将起始地址,就是第一条指令所在的内存单元地址送给程序计数器,当执行指令的时候cpu就会自动的修改程序计数器的内容,以便于保持记录的总是要执行的下一条指令的地址
3.大多数指令都是顺序执行的,所以一般+1即可
总结:程序计数器是程序控制流的指示器,所有的分支循环,线程恢复等功能都需要依赖这个结构
注:程序计数器是Java虚拟机中没有规定OOM问题的区域
基本特征
事实上,JVM中的程序计数器命名就源于cpu中的寄存器,寄存器存储指令的相关内存信息,cpu只有把数据装载到寄存器中才能运行,这里并非是广义上的物理寄存器,你可以理解为JVM中的程序计数器实际上是物理PC技寄存器的一种抽象模拟,占用的空间很小所以基本不存在OOM问题,也是运行速度最快的区域.
为啥程序计数器需要记录字节码指令地址?
因为CPU要不停地切换执行各个线程,切换上下文之后就得知道从哪里开始继续执行,这里的JVM字节码解释器就得改变程序计数器的值来告诉CPU下一步该干嘛.
为啥线程私有?
这个问题也就很明白了,每个线程私有的保存自己进行到哪儿了,避免互相干扰
3.虚拟机栈
栈主要保存的是Java程序的运行,保存的是方法的局部变量,部分结果,并参与方法的调用和返回,栈解决的是程序的运行问题,堆解决的是程序的存储问题
栈存在GC嘛?
不存在,但是存在OOM问题
StackOverFlow和OOM问题的区别
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
如果使用固定大小的栈,但是请求的栈容量超过的虚拟机设置的最大容量,这时候就会发生栈溢出的问题了
但是如果是动态扩展的栈,扩展的时候无法申请到足够的内存,这时候就会抛出一个OOM异常
如何设置栈大小
使用-Xss size 来设定大小
一般默认为512k-1024k之间 取决于操作系统
栈的大小决定了函数调用的最大深度
jdk5之前默认是256k,之后哦默认是1024k的大小
栈的单位 - 栈帧
栈中的额数据以栈帧的方式保存,栈帧是一种内存区块,是一个数据集
在一个线程中,一个时间点上只会有一个栈帧活动,被称为当前栈帧,对应的方法和类也就称之为当前方法和当前类
JVM堆栈的操作只有方法执行的时候压栈,执行结束弹栈的操作
注:不同线程的栈帧不允许相互引用,栈帧可以通过return指令返回或者是抛出异常弹出
栈的内部保存着
1.局部变量表
2.操作数栈
3.常量池的方法引用
4.方法返回地址
5.一些附加的信息
局部变量表
注:局部变量表是定义的一个局部变量数组或者是本地变量表,存放的是字面量或者是对象的引用以及返回地址等,容量大小在编译期间就已经确认下来,局部变量表中的数据只在当前方法调用中有效,随着方法栈帧的销毁,局部变量表也会随之销毁
下面是对这段字节码的解释
iconst_0
:将整数常量0推送到操作数栈的顶部。istore_1
:从操作数栈的顶部弹出一个整数,并将其存储到局部变量表的第2个位置(索引从0开始)。iconst_1
:将整数常量1推送到操作数栈的顶部。istore_2
:从操作数栈的顶部弹出一个整数,并将其存储到局部变量表的第3个位置。iload_1
:从局部变量表的第2个位置加载一个整数,并将其推送到操作数栈的顶部。iload_2
:从局部变量表的第3个位置加载一个整数,并将其推送到操作数栈的顶部。iadd
:从操作数栈的顶部弹出两个整数,将它们相加,然后将结果推送到操作数栈的顶部。istore_1
:从操作数栈的顶部弹出一个整数,并将其存储回局部变量表的第2个位置,覆盖原来的值。
存在线程安全问题??
局部变量表是线程私有的,不存在线程安全问题
Slot的理解
局部变量表得救基本单位 - 变量槽位
在局部变量表中,32位以内的类型占用一个slot,64位的占用两个slot
JVM会为每个Slot分配一个索引,通过这个缩影就可以去访问到局部变量表中的变量值,访问64位的局部变量时直接使用第一个索引就可以访问
注:这里Slot是可以重用的,一个变量过了其作用域,新的局部变量就会占据他的槽位,这样就能达到节省空间的效果
操作数栈
操作数栈是JVM执行引擎的一个工作区,在方法执行的过程中,根据字节码指令,不是采用索引的方式而是使用标准的入栈出栈操作来往栈中写入和提取数据,主要存储和保存计算的临时结果.
注:操作数栈中元素的数据类型必须和字节码指令序列严格匹配,这在编译期间和类加载期间会进行两次认证.
栈顶缓存技术
栈在执行操作的时候一定会使用很多的入栈和出栈操作,这也就意味着需要执行多次的指令分派和堆内存的多次读写,所以HotSpot虚拟机的设计者提出了栈顶缓存技术,将栈顶元素缓存在物理CPU寄存器中,降低堆内存的读写操作,这也就提高了效率.
动态链接(指向运行时常量池的方法引用)
每个栈帧内部都包含一个指向运行时常量池中的方法引用,这个引用就是位置支持该方法的代码可以实现动态链接
在Java源文件被编译成字节码的时候,所有的变量和方法就会作为符号引用被保存到常量池中,然后在链接的时候把符号引用转化为直接引用,这就是动态链接
为什么需要常量池?
提供一些符号和常量来便于指令的识别
方法的调用
JVM中,符号引用转换为方法的直接引用的方法与绑定机制有关
静态链接:目标方法在编译器编译期间已知,并且运行期间保持不变,这种时候符号引用转换成直接引用叫做静态链接
动态链接:如果被调用的方法早编译器没有确定下来,这里在程序运行的时候将调用方法的符号引用转换为直接引用的过程叫做静态链接
早期绑定:目标方法在编译器已知,且运行的时候保持不变,这嫦娥职位早期绑定
晚期绑定:被调用的方法在编译器无法确定,但是在程序运行期间根据实际类型绑定相关的方法称之为晚期绑定
虚方法与非虚方法
类似Java这样的面向对象的语言都保持着封装继承多态三大特性,只要具有多态,那么就会有早期绑定与晚期绑定的绑定方式
Java任何一个普通方法都具备虚函数的特征,如果不希望这样,可以使用final修饰这个方法
非虚方法:
在编译期间就确定了具体的调用版本,在运行时是不可变的
例如静态方法,私有方法,final方法,实例构造器,父类方法都称为非虚方法
其他的方法称之为虚方法
其他的方法称之为虚方法
注:动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
方法重写的本质
这里先描述一下方法引用得救基本格式
虚拟机提供了一些经典的方法调用指令
1.invokestatic 调用静态方法.解析阶段唯一确定方法版本
2.invokespecial:调用<init>方法,私有方法以及父类方法,解析阶段唯一确定方法版本
3.invokevirturl:调用所有虚方法
4.invokeinterface:调用接口方法
5.invokedynamic:调用动态方法
好了,有了以上的前置知识,我们来聊聊关于方法重写的本质
1.找到操作数栈的第一个元素所执行的对象的实际类型,记作S类型
在过程结束的时候,如果S找到常量池中描述符合简单名称的方法,则进行访问权限校验,通过就直接返回这个方法的直接引用,没权限就返回illealAccsessError异常
2.按照继承关系依次对S的父类进行第二步的搜索和验证过程
找不到就返回AbstractmethodError异常
虚方法表:
面向对象编程中经常会遇到动态分派,每次分派都会影响效率,所以为了提高心梗,JVM采用虚方法表来实现,通过索引来查找匹配对应的方法,属于是用空间换时间的经典实现了
方法返回地址
存放该方法的pc寄存器的值
一个方法的结束往往有两种可能:1正常执行结束 2.出现异常退出
无论是哪种方式结束,都会有返回到该方法被调用的位置
正常退出返回程序计数器中的指令地址,异常退出则要通过异常表来确定
几个问题总结
1.栈溢出的情况
while true循环一直创建变量就可以模拟这种情况
固定大小可以通过-Xss来设置
动态的满了就会报OOm错误
2.调整大小能保证不溢出?
不行,大小不是无限的
3.栈的内存分配的越大越好??
不是,栈的内存分配的大了,能创建的线程数量就变少了
4.垃圾回收是否涉及虚拟机栈?
不涉及
5.方法中定义的局部变量是否线程安全?
在内部产生和消亡的是线程安全的,外界作为参数传入的和return的返回值不一定线程安全
本地接口和本地方法栈
本地方法就是Java调用非Java代码的接口,比如调用的底层是使用C++代码编写的
为啥使用本地方法?
其他语言实现比较方便而Java实现不太方便
比如与操作系统或者外环境交互等,但是目前而言是使用的越来越少了
堆(重点)
核心概述
1.一个JVM实例只存在一个堆内存,堆也是Java内存管理和核心区域
2.Java堆区在JVM启动的时候被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间
3.堆内存的大小是可以调节的
4.虚拟机规范规定,堆可以处于物理上不连续的内存空间,但在逻辑上视为连续的
对象都分配在堆上?
虚拟机规范描述是数组和对象永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆上的位置,结论是"几乎所有对象实例都在堆上分配内存"
注:所有的线程都共享Java堆,在这里还可以划分线程私有的缓冲区(TLAB)
堆的内部结构
JDK8相对于JDK7来说就是把方法区变成了元空间,具体的会在下面解释
堆区主要分为两类
年轻代和老年代
年轻代又分为伊甸区,幸存区1和幸存区2
注:几乎所有的Java对象都是在伊甸区被new出来的,且大多数对象都是朝生夕死的,只有大对象会直接放到老年代
堆内存大小的设置
-Xms 设置堆区的起始内存
-Xmx 设置堆区最大内存
注:一般两个餐宿都设为一样的值,目的是在垃圾回收机制清理完之后不需要重新分隔计算堆区的大小,从而提高性能
注:如果物理内存小于192M,那么heap的最大值就是物理内存的一半,大于1G就是物理内存的1/4,默认最小值不能小于8M,如果物理内存大于1G,那么默认是1/64
如何设置新生代和老年代的比例
-XX:NewRatio=2 默认是新生代占1/3,老年代占2/3
这个参数就是新生代和老年代的比例
-Xmn可以设置新生代最大内存大小
伊甸区和幸存区的比例为8:1:1
可以通过-XXSurvivorRatio来调整这个空间比例
对象分配:
针对两个幸存区:复制之后有交换,谁是空的谁就是to区
垃圾回收:频繁在新生代,很少在老年代,几乎不在方法区/元空间
对象产生过程剖析
1.先放到伊甸区
2.一点去填满之后,如果还要放对象就进行一次Minor GC
3.一点去剩余存活对象移动到s1区
4.再次幸存的就会放到s2区
5.在对象15岁的时候就可以去老年区了(默认值15)
6.老年区内存不足的时候会发生Major GC,执行之后如果还没办法保存对象就会产生OOM异常
内存分配原则
针对不同年龄段对象分配:
1.优先分配到伊甸区
2.大对象直接放到老年代(避免出现很多大对象)
3.长期存活对象分配到老年代
4.如果幸存区相同年龄的对象占其中的一半,就去查看老年区剩余空间够不够放,够就直接进老年代,不够则判断一下平均存活和剩余空间的比较,内存足够还是直接进老年代,如果还不够,进行youngGC/MinorGC,可能还有MajorGC的操作
注:FullGC指的是整个对空间进行垃圾清理,Major是针对老年区,MinorGC是针对年轻代
MajorGC,MinorGC,FullGC
MinorGC/YoungGC触发机制是伊甸区慢了,幸存区满了不会触发,因为大部分对象都是朝生夕死的,所以MinorGC十分频繁,一般回收速度也很快,会引发STW,等垃圾回收结束再继续运行
MajorGC一般会伴随着一次MinorGC,但不绝对,是在老年代空间不足的时候,尝试先触发MinorGC,还不行就触发MajorGC(目前只有CMS垃圾收集器会单独收集老年代)
一般速度很慢,STW时间更长
FullGC触发一般是调用System.gc()方法,或者是老年代空间不足,MinorGC+MajorGC等于FullGC,或者是方法区内存不足了,MinorGC后进入老年代的大小大于可用内存了等等(开发中尽量避免)
为啥要进行分代?
因为研究表名80%的对象是临时对象,是朝生夕死的,分代可以提高GC的性能,如果不分代,那么老年代新生代都在一块,一起扫描的开销太大了.
TLAB(现成私有缓存区域)
这是堆中的线程私有区域,对象在JVM的创建十分频繁,所以内存空间的线程不安全问题就值得考量,为了避免多个线程操作同一个地址,需要加锁等操作,这里使用了TLAB的方式,可以有效解决线程安全问题,占用空间仅仅是伊甸区的1%
伊甸对象在TLAB空间内存分配失败的时候,JVM就会尝试使用加锁机制来保证操作的原子性
方法区的演进
上方给了几个图,至于为啥需要将但永久代改成元空间,是因为类的卸载困难,因为大部分类是使用三个经典类加载器来加载的,这里如果想要把类卸载就得把类的加载器也干掉,这里是不可能做到的,所以我们就将方法区从内存中剥离出来了,改名为元空间,其实使用的就是本地内存了,这样内
存更大,大程度避免了OOM问题
方法区都保存什么内容?
类型信息:
1.类型完整有效名称
2.父类完整有效名
3.类的修饰符
4.接口列表
还包括属性和方法的各种信息
运行时常量池
这也是方法区的一部分
常量池表是class文件的一部分,用于存放编译期间各种此面料以及符号引用,这部分内容将在类加载的时候放到常量池中,池中的数据想数组元素一样,是通过索引来访问的
载的时候放到常量池中,池中的数据想数组元素一样,是通过索引来访问的
注:常量池可以看做一张表,虚拟机指令会根据这张表来找到要执行的类名方法名,参数,字面量等信息
为啥需要常量池?
因为一个java源文件中的类接口等编译后产生一个字节码文件,这个字节码就需要数据支持,通常数据很大不能直接放到字节码中,所以字节码中就会存常量池中的引用,在动态链接之后就睡用到运行时常量池
常量池有啥
1.数量值
2.字符串
3.类,方法,字段引用
方法区有没有GC?
有些人认为方法区不存在垃圾回收行为,其实也确实没实现或者是没有能完全实现类卸载的收集器存在,因为这个区域的回收效果很难让人满意,特别是类的卸载,但是回收优势有必要的
方法区回收主要回收两个部分,就是常量池中废弃的常量和不再使用的类型,只要常量池中的常量没有再被引用了,那么就直接可以被回收了
本篇粗略的介绍了运行时内存区的内容,如有问题,实属抱歉