一,PC寄存器
功能:主要负责记得当前线程的执行指令地址,每个 线程都会有各自的pc寄存器,因为在多线程坏境中,需要知道切换线程后,当前线程执行到哪个位置,当切换回来的时候,通过寻找到这个位置继续执行程序
既不会发生gc,也不会发生oom,是线程私有的
二,虚拟机栈
管理java程序运行,保存方法的局部变量(如果是引用类型则报存引用地址),部分结果,参与方法的调用和返回。内部保存的基本单位是栈帧,每个栈帧对应一个方法。
生命周期和线程一样,不会触发gc,是线程私有的
优点:栈是一种快速有效的分配存储结构,访问速度仅次于寄存器。实现简单,栈只有入栈出栈操作
栈的大小是固定的时候,当需要申请栈操作大于栈大小,则会抛出栈溢出异常
栈的大小是可以动态扩展的时候,当需要尝试扩展的时候无法申请到足够的内存,则抛出内存溢出异常
栈的方法结束方式有两个,一个是正常结束,一个是抛出异常
在方法的调用中,a调用b,b调用c,如果b出现了异常且没有捕获,那么b的返回结果是抛出异常给a,如果a捕获了异常,那么a的返回是正常返回,如果a没有捕获,那么a的返回也是抛出异常
栈帧的组成:局部变量表,操作数栈,动态链接,方法返回地址,附加信息
局部变量表:定义为一个数字数组,主要用于存储方法的参数和方法体内的局部变量,容量大小在编译的时候确定下来的,不会更改。局部变量表越大,栈帧越大,就会导致栈帧占用空间过多,所以调用的次数就会减少。局部变量表会随着栈帧的摧毁而摧毁。最基本类型是槽(slot),按照变量的声明顺序存储变量,32位以内占一个槽(byte,short,char,boolean,float,int,引用,返回),64位占2个槽(long,double)。char和boolean会被转换成ascall码和0/1。jvm会根据槽去调用局部变量,如果存储的时候用到两个槽,则用起始槽表示当前变量的索引。如果当前方法不是静态方法(实例方法或者构造方法),则会在index为0的slot处存储当前对象引用this。槽是可以重复利用的,当一个变量超出作用域之后,就会被回收,后续的变量则重新利用槽。
操作数栈:在方法执行过程中,根据字节码指令往栈写入或者提取数据(入栈/出栈)
大小是在编译的时候确认的,当一个方法刚开始执行的时候,操作数栈就会被创建,但是是空的。
32位占用一个栈单位,64位占用两个栈单位,操作数栈并非是采用访问索引的方式来进行数据范文的,只能通过入栈和出栈操作来完成数据访问。
如果有返回值,则需要把返回值压入操作数栈中,且更新pc寄存器的值
动态链接:
编译成字节码文件的时候,所有变量和方法引用都作为符号引用存放在常量池。动态链接是把符号引用转换成直接引用。
方法返回地址:存放着该方法pc寄存器的值
方法结束有两种方式:正常执行完成和出现未处理异常,非正常退出
正常执行完成的方法返回地址是下一条指令的地址
出现未处理异常的方法返回地址是要通过异常表来确定的
返回指令包含:
ireturn:bolean,byte,char,short,int
lreturn:long
freturn:float
dreturn:double
areturn:引用类型
return:void
正常完成出口和异常完成出口的区别:正常完成出口会给上层调用者产生一个返回值,如果是异常完成出口不会产生任何的返回值
附加信息
根据jvm实现相关,可能会带有栈帧相关的信息
栈顶缓存技术:
jvm使用栈式架构,很多入栈和出栈指令,影响执行速度,因此提出栈顶缓存技术
把栈顶元素全部缓存在物理cpu的寄存器中,降低内存读写次数,提高执行效率
方法的调用:
在jvm中把符号引用转换成直接引用于方法的绑定机制相关。
在编译期间可以确认被调用方法且运行期间保持不变,则称之为静态链接
被调用的方法无法在编译期间确定下来,则称之为动态链接
非虚方法定义:在编译期间确定了调用版本,这样的方法称为非虚方法
非虚方法有:静态方法,私有方法,final方法,实例构造器,父类方法
其他都是虚方法。
虚拟机提供一下几条方法调用指令:
普通指令:
1.invokestatic:调用静态方法,解析阶段确定唯一版本
2.invokespecial:调用<init>方法,私有和父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法
jdk7新增动态指令:
invokedynamic:动态解析出调用方法,然后执行
普通指令固话在虚拟机内部,外部人员不可干预,动态指令可以由用户确定方法版本
前两条指令调用非虚方法,后两条是调用虚方法(final方法属于invokevirtual,是一个例外)
invokedynamic指令:在jdk8中的lamada表达式中开始出现
java是一门静态语言,变量的类型是通过声明去确定的
动态语言是:变量的类型是通过值去确定的,例如js
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
为什么没有非虚方法表?因为非虚方法都是确定的,不需要再去寻找
一些问题总结:
栈溢出的情况(stackoverflowError):通过-Xss设置栈大小
调整栈的大小就能确保不出现溢出吗?不能
分配栈内存越大越好嘛?不一定
三,本地方法栈
本地方法栈用于管理本地方法调用,类似虚拟机栈
当某个线程调用了一个本地方法,他就进入了一个全新并且不再受虚拟机限制的世界。他和虚拟机拥有同样的权限。本地方法甚至可以通过本地方法接口来访问虚拟机内部的运行时数据区,直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存。
HotspotJVM 把本地方法栈和虚拟机栈合二为一
线程私有
本地方法接口:
一个native方法就是一个java调用非java代码的接口
用java调用c语言库
四,堆
4.1 基础和设置大小
基础
一个jvm只存在一个堆内存,堆在jvm启动的时候创建,其大小也就确定了(可以通过配置jvm来设置堆大小)。
堆是jvm管理最大一块内存空间。
多个线程共享一个堆空间,在这里还能划分线程私有的缓冲区(TLAB,解决并发性能问题)。
java虚拟机规范中规定,堆在物理上不连续,在逻辑上连续。
几乎所有的对象实例和数组都在堆里边分配内存。(有些在栈里边)
数组和对象可能永远不会存储在栈上,栈帧保存的是引用,这个引用指向堆内存位置。
方法结束后,堆里的对象不会马上被移除,只有在垃圾收集的时候才被移除。
堆,是gc执行垃圾回收的重点区域。
内存细分
jdk7前:新生区,老代区,永久区 jdk8及后:新生区,养老区,元空间
新生区分成Eden区和Survivor区(又分成from区和to区)
-Xms 表示堆区起始内存,等价于 -XX:InitialHeapSize
-Xmx 表示堆的最大内存,等价于 -XX:MaxHeapSize
一旦堆区大小超过 -Xmx 指定最大的内存时候,就会抛出OutOfMemoryError异常
通常将 -Xms和-Xmx两个参数配置相同大小,目的是为了能够java垃圾回收机制清理完堆之后不需要重新分割计算堆区的大小,提高性能。
默认情况下,初始内存大小:物理电脑内存大小/64
最大内存大小:物理电脑内存大小/4
RunTime这个类代表运行时数据区,是单例的,可以获取jvm相关信息。
查看设置参数:
方式一 : jps / jstat -gc 进程id
方式二 : -XX:+printGCDetails
4.2 OOM
不是Exception,属于Error。
4.3 年轻代与老年代
存储在jvm中的java对象可以划分成两类:
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
一类生命周期非常长,某些极端的情况下还能够与jvm生命周期保持一致
java堆区进一步细分的话,可以划分成年轻代和老年代,其中年轻代又可以划分成Eden空间,Survivor0空间和Survivor1空间(也叫做from区,to区)
Young: 1/3堆空间
Eden 8/10 Young空间
from 1/10 Young空间
to 1/10 Young空间
Old:2/3堆空间
配置新生代和年老代大小参数:
默认:-XX:NewRatio=2 表示新生代占1,老年代占2,新生代占整个堆的1/3
修改:-XX:NewRatio=4 表示新生代占1,老年代占4,新生代占整个堆的1/5
一般情况下不修改,如果程序中的对象生命周期都是非常长,才考虑修改,把老年区大小调大一点。
配置Eden区和from,to区的大小参数
默认:-XX:SurvivorRatio=8 Eden占8/10,from占1/10,to占1/10,合起来就是整个年轻代的大小。from和to大小一致、
几乎所有的java对象都是在Eden区被new出来的
绝大部分的java对象销毁都是在新生代进行
可以使用-Xmn设置新生代最大内存大小,这个参数一般不做修改。
4.4图解对象分配过程
new对象先放在Eden区,此区有大小限制
当Eden区填满,程序又要创建对象,JVM垃圾回收期将对Eden区进行垃圾回收(YGC/Minor GC,也叫Young GC),将不再被其他对象所引用的对象进行销毁。再加载新的对象到Eden区(如果Eden区不够大的话,也有可能直接加载到老年代,如果老年代也放不下,则会触发老年代的GC,FGC)。YGC同时回收Eden区和to区的对象。
然后将原本Eden区剩余的对象移动到to区,将他们的年龄设置成1。注意。to区满了并不会触发YGC,但是to区满了有可能会直接进入到老年代。
如果再次发生垃圾回收,则把幸存下来的对象放到to区,然后把他们的年龄+1(此时的to区其实是之前的from区,在jvm里边,to区和from区是交替使用的,存放了对象的区永远都是from区,to区永远都是空的,这两个区只不过是用过交换对象存储的地方而已。)
对象的年龄到达15(默认)了之后,就会移动到年老区。
阈值可以通过-XX:MaxTenuringThreshold来设置。
常见的调优工具:
JDK命令行
Eclipse:Memory Analyzer Tool
Jconsole
VisualVM
Jprofiler
Java Flight Recorder
GCViewer
GC Easy
4.5 MinorGC MajorGC FullGC
YGC=MinorGC 老年代的GC:MajorGC
JVM在进行GC的时候,并不是每次都对三个内存区域(新生代,老年代,元空间)一起回收,大部分的时候回收的是新生代。
针对HotSopt VM的实现,他里边的GC又分成两大类型:一种是部分回收,一种是整堆回收
部分回收:不是完整回收整个java堆。其中又分成:
新生代回收(Minor GC/Young GC):只回收新生代的垃圾
老年代回收(Major GC/Old gc):只回收老年代的垃圾
目前,只有CMS GC会有单独收集老年代的行为
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合回收(Mixed GC):回收整个新生代和部分老年代的垃圾
目前只有G1 GC有这种行为
整堆回收(Full GC):收集整个java堆和方法区的垃圾
新生代GC:
MinorGC会触发STW,暂停其他用户的线程,等待垃圾回收结束,用户线程才恢复运行。
老年代GC:
出现Major GC,经常伴随着至少一次Minor GC(并非绝对)。也就说,在老年代空间不足的情况下,会尝试触发MinorGC,如果之后空间还是不足,才触发MajorGC。
MajorGC的速度比MinorGC慢10倍以上,STW时间更长。
如果MajorGC之后内存还不足,就报OOM。
FullGC触发的情况
(1)调用System.gc(),系统建议执行Full GC
(2)老年代空间不足
(3)方法区空间不足
(4)通过MinorGC后进入老年代的平均大小 大于 老年代的可用内存
(5)由Eden区,from区向to区复制的时候,对象的大小大于to区的可用内存,则把对象移动到老年区,且老年区的可用内存也不足。
Full gc是开发中要尽量避免的。
4.6 堆空间分代思想
分代就是为了优化GC性能。如果没有分代,所有的对象都在一块,gc需要找到没用的对象,就很复杂。
4.7 内存分配策略
对象分配原则:
(1)悠闲分配到Eden区
(2)大对象直接分配到老年代
尽量避免出现过多的大对象
(3)长期存活对象分配到老年代
(4)动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小总和 大于 Survivor空间的一半,年龄大于或者等于改年龄的对象可以直接进入到老年区。
(5)空间分配担保
-XX:HandlePromotionFailure
4.8 TLAB,为对象分配内存空间
为什么需要TLAB?
堆区是线程共享的区域,任何线程都可以访问到堆区的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆中划分内存空间是线程不安全的
避免多个线程操作同一地址,需要使用加锁机制,进而影响分配速度
什么是TLAB?
从内存模型而不是垃圾收集的角度,对Eden区域继续划分,JVM为每个线程分配了一个私有的缓存区,它包含在Eden空间里。
多线程同时分配内存,使用TLAB可以避免一系列非线程安全问题,同时还能拿提升内存分配的吞吐量,我们可以将这种分配方式称为 快速分配策略
可以通过-XX:UseTLAB来设置是否开启TLAB空间,默认开启。
TLAB空间占整个Eden的1%,通过-XX:TLABWasteTargetPercent设置TLAB空间占Eden空间的百分比
一旦对象从TLAB空间分配内存失败,JVM就会尝试通过加锁机制来确保数据操作的原子性,从而直接在Eden空间分配内存。
4.9 小结堆空间参数设置
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-XX:+PrintFlagsFinal 查看所有参数的最终值(可能会被修改,不再是初始值)
查看某个进程参数: jps:查看当前运行的线程
jinfo -flag (命令) 进程id
-Xms:初始堆空间内存 默认物理内存的1/64
-Xmx:最大堆内存 默认物理内存的1/4
-Xmn:新生代的大小 初始值和最大值
-XX:NewRatio 配置新生代和老年代的占比
-XX:SurvivorRatio 配置新生代中Eden和from/to空间的比例
-XX:MaxTenuringThreshold 配置新生代垃圾的最大年龄
-XX:+PrintGCDetails 输出详细gc处理日志
-XX:HandlePromotionFailure 是否配置空间分配担保
4.10 堆是分配对象存储的唯一选择吗
不一定。如果经过逃逸分析发现,一个对象并没有逃逸出方法的话,那么久可能被优化成栈上分配。
如何通过将堆上的对象分配到栈,需要使用逃逸分析手段。
可以减少内存堆分配空间的压力。
通过逃逸分析,java虚拟机的编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象的动态作用于:
当一个对象在方法被定义后,对象只在方法里使用,则认为没有发生逃逸(可以分配到栈上)
当一个对象在方法中定义后,他被外部方法引用,则认为发生逃逸。例如,作为返回值传递到其他地方。
分配到栈上的好处:
每一个栈帧,使用完就会出栈,不需要GC,而且栈是线程独有的,也不涉及共享变量的问题。
jdk7之后默认开启逃逸分析,通过-XX:PrintEscapeAnalysis查看逃逸分析的筛选结果。
使用逃逸分析后,编译器会对代码做一下优化:
①栈上分配:将堆分配转换为栈分配
②同步省略:一个对象被发现只能从一个线程访问,就不考虑同步问题。如果锁对象只被一个线程访问,则会把锁消除掉
③分离对象或标量替换:有的对象可能不需要作为一个连续的内存存储结构也可以被访问到,那么对象的部分(或者全部)可以不存储在内存,而是存储在CPU的寄存器中。
变量:无法再分解成更小的数据结构,java中原始的数据类型,例如:int,float等等
聚合量:可以分解的数据结构,例如:对象
如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象分解成多个成员变量来替代,这个过程就是标量替换。
五,元空间/方法区
5.1 概述
Stundet stu = new Stundet();
(方法区)(栈) (堆)
5.2 方法区的理解
方法区和堆一样,和各个线程所共享的内存空间
方法区在jvm启动的时候被创建,并且他实际的物理内存空间中和java堆一样都是不连续的
方法区的大小跟堆空间一样,可以选择固定或者可扩展
方法区的大小决定了系统可以报错多少个类,如果系统定义了太多类,导致方法区移除,虚拟机同样会跑出内存溢出的错误。
关闭jvm就会释放这个区域的内存
元空间和永久代最大的区别:元空间的存储空间是本地内存中,永久代则是在虚拟机内存中
5.3设置方法区大小与OOM
jdk7和之前:
-XX:PermSize 设置永久代初始分配空间,默认20.75M
-XX:MaxPermSize 设置永久代最大空间,32位机器默认64M,64位机器是82M
如果加载类信息超过这个值,会报OOM
jdk8和之后:
-XX:MetaspaceSize 设置元空间初始内存
-XX:MaxMetaspaceSize 设置元空间最大内存
为了避免出现过多次数的Full GC,建议把MetaspaceSize设置一个相对较高的值。
5.4方法区的内部结构
5.4.1类型信息
对每个加载的类型(类class,接口interface,枚举enum,注解annotation),jvm必须在方法区中存储一下类型信息
①这个类型的完整有效名称(全类名)
②这个类型直接父类的完整有效名(对于interface或者java.lang.Object,都没有父类)
③这个类型的修饰符(public ,abstract,fianl的某个子集)
④这个类型直接接口的一个有序列表
5.4.2域信息
jvm必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
域的相关信息包括:域名称,域类型,域修饰符
5.4.3方法信息
方法名称
方法返回值
方法参数的数量和类型(按顺序)
方法的修饰符
方法的字节码,操作数栈,局部变量表的大小(abstract和native方法除外)
异常表(abstract和native方法除外)
每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引
non-final的类变量
静态变量和类是关联在一起的,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例你也可以访问到他
全局常量:final statis
被声明为final的类变量处理方法不同,每个全局常量在编译期间就会被分配。
5.4.4 运行时常量池与常量池
运行时常量池=常量池加载到方法区里
方法区,内部包含运行时常量池
字节码文件,包含常量池
要弄清楚方法区,需要理解清楚ClassFile,因为加载类信息都在方法区
要弄清楚方法区的运行时常量池,需要理解清楚ClassFile的常量池
常量池:
常量池表,包含各种字面量和对类型,域或者方法的符号引用。
运行时常量池:
运行时常量池时方法区的一部分。
常量池表是Class文件的一部分,用于存放编译期间生成的各种字面量与符号引用,这部分的内存将在类加载后存储到方法区的运行时常量池中。
运行时常量池在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型都维护一个常量池。池中的数据项像数组一样,通过索引访问。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里替换为真实地址。
运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性
运行时常量池类似传统编译语言中的符号表,但是它锁包含的数据却比符号表更加丰富一些
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所提供的最大值,则jvm会跑出OOM异常。
5.5方法区使用举例
5.6方法区演进细节
jdk1.6和之前:有永久代,静态变量存放在永久代上
jdk1.7:有永久代,但是已经逐步去除,字符串常量池,静态变量移除,保存在堆中
jdk1.8和之后:无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但是字符串常量池,静态变量仍然在堆中。
为什么会修改?
因为永久代设置空间大小是很难确定的
对永久代进行调优是很困难的
为什么要把字符串常量池放到堆中?
因为我们在开发的过程中,经常使用到大量的字符串,而方法区很少会发生回收,这就会导致回收的效率很低。
放到堆里可以及时地清除掉不用的字符串。
5.7方法区的垃圾回收
jvm虚拟机规范并没有强制要求虚拟机实现方法区的垃圾回收,方法区的垃圾回收可有可无。
方法区的垃圾回收对象主要分成两部分:废弃的常量和不再使用的类型
废弃的常量:字面量和符号引用
在大量使用反射,动态代理,CGLIB等框架的时候,由于会产生很多类型信息,容易导致方法区内存压力过大,所以要求虚拟机具备卸载类型信息的能力。
6.对象的实例化与内存布局
6.1对象的实例化
6.1.1对象创建的方式
new
Class的newInstance()
Constructor的newInstance()
使用clone()
使用序列化
使用第三方Objenesls
6.1.2创建对象的步骤
判断对象对应的类是否加载,链接,初始化
为对象分配内存
如果内存规整,指针碰撞
内存不规整,虚拟机则要维护一个列表,空闲的列表分配
处理并发安全问题
采用CAS配上失败重试保证更新的原子性
每个线程先分配一块TLAB
初始化分配的空间
所有属性设置默认值,保证对象实例字段在不赋值的时候可以直接使用
设置对象的对象头
执行init方法进行初始化
6.2 对象的内存布局
对象头
运行时元数据:哈希值,GC分代年龄,锁状态标志,线程持有锁,偏向线程id,偏向时间戳
类型指针:指向类元数据InstanceKlass,确定该对象所属类型
说明:如果是数组,还需要记录长度
实例数据
说明:他是对象真正存储的有效信息,包括程序代码中定义的各种类型字段
规则:
相同宽度的字段总是被分配一起
父类中定义的变量会出现在子类前
如果CompactFileds参数为true(默认为true):子类的窄变量可能插入到父类的空隙
对其填充
不是必须的,起到占位符的作用
6.3 对象的访问定位