概览JVM运行时数据区主要包括以下几个部分:程序计数器、虚拟机栈、本地方法栈、方法区、堆;其中 栈是运行时的单位,而堆是存储的单位!
程序计数器可以看作是当前线程所执行的字节码的 行号指示器 可以通过javap -c xxx.class执行查看字节码文件
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
虚拟机栈描述的是 Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧
,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧(Stack Frame)
是用于支持虚拟机进行方法调用
和方法执行
的数据结构,是虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里从入栈到出栈的过程。
简单来讲:每个方法都会对应在虚拟机栈中生成一个栈帧,以栈的数据结构进行存放所有的栈帧。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量:
(1)编译时就确定最大容量
在Java程序编译为Class文件时,就在Class文件的方法表的Code属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量。
(2)局部变量表的容量以变量槽(Slot)为最小单位
一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有8种: boolean 、byte、char、short、int、float、reference 和 returnAddress; 对于 64位的数据类型(long/double),虚拟机会以高位对齐的方式分配两个连续的Slot空间。
(3)索引定位
虚拟机通过索引定位的方式使用局部变量表,索引值范围是从0至最大Slot数量对32位数据类型的变量来说,索引n就代表了使用第n个Slot;而对64位数据类型的变量来说,则会同时使用n和n+1两个Slot;
其中,索引位置0,默认是this。
(4)索引分配
在方法执行时,虚拟机是使用局部变量表来完成参数值到参数变量列表的传递过程的如果执行的是实例方法,则局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数;其余参数按照参数表顺序排列,占用从1开始的局部变量Slot;参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
(5)Slot重用
局部变量表中的Slot是可以重用的,如果当前字节码PC计数器的值,已经超过了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,这就是入栈和出栈。
操作数栈是为字节码指令服务的:
例如,虚拟机执行字节码指令 iload ,会将一个int类型的局部变量从局部变量表加载到操作数栈,iadd会将操作栈顶的两个元素相加 ,并将相加后的结果入栈。
动态连接就是指向常量池
中该栈帧所属方法的引用。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化称为静态解析
另一部分将在每一次运行期间转为直接引用,这部分称为动态连接。
一个方法的退出方式有两种:
正常完成出口:执行引擎遇到任意一个方法返回的字节码指令。
异常完成出口:在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,也就是在本方法的异常表中没有搜索到匹配的异常处理器,这时就会导致方法异常退出。
方法返回地址
:
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈:
因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
Java堆在虚拟机启动时创建,唯一目的就是存放对象实例 以及 数组。
(1)从内存回收的角度来看,Java堆是垃圾收集器管理的主要区域。
由于目前的垃圾收集器都采用分代收集算法,因此Java堆中还可细分为:新生代和老年代,默认占比为Young:Old = 1:2。同时新生代中采用复制算法,将新生代分为三个区域,默认占比为:Eden:from:to=8:1:1。
(2)从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer)
方法区用于存储已被虚拟机加载的类元信息(即Class对象)
、常量池(运行时常量池)
、静态变量
、即时编译器编译后的代码
等数据。
额外解释:类元信息(class metadata),相当于java类编译后(.class类对象)在JVM中的信息。
类元信息中包含了:
Klass 结构,这个非常重要,把它理解为一个 Java 类在虚拟机内部的表示;
method metadata,包括方法的字节码、局部变量表、异常表、参数信息等;
注解;
方法计数器,记录方法被执行的次数,用来辅助 JIT 决策;
其他
如果一心想要深入研究元空间内,对象与类加载器之间的关系,和元空间中存放的数据,可以看看这个文档:
深入理解堆外内存 Metaspace.md (如果下面这个能看懂,那么可以来找我要文档)
理解
方法区
为一个理论接口,具体的实现方案有永久代和现在的元空间。
即时编译后的代码:即本地代码,优化提高了代码的运行效率,距离底层硬件更加接近!一般用于处理热点代码(编译器,都是以整个方法作为编译对象)
,也就是被多次调用的方法和被多次执行的循环体。JIT(just-in-time compilation)即时编译器编译才有了用武之地,否则,不如直接用解释器执行代码。
贴图:
编译的空间开销:
对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10几倍是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致“代码爆炸”。
这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。
对于HotSpot虚拟机来说,也称为“永久代”(Permanent Generation)
方法区的变化:
jdk1.6及以前:有永久代(方法区的实现),运行时常量池逻辑包含字符串常量池在方法区中
jdk1.7:有永久代,但已逐步“去永久代”,字符串常量池转移到堆中,运行时常量池还在方法区中
jdk1.8及之后:无永久代,替换为元空间,
字符串常量池还在堆, 运行时常量池还在方法区中(元空间)
虽然这个元空间还能往里挖,但是,不推荐了……后面的就越来越难理解了。
在HotSpot虚拟机中,对象的内存布局可分为:对象头、实例数据、对齐填充。
图解解释:
MarkWord对象头
:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、偏向线程ID等。
数组长度
:如果对象是数组,则对象头中还必须有一块用于记录数组长度的数据。
klass point类型指针
:即对象指向它的类元数据(MetaData)
的指针,并不是所有的虚拟机实现都必须在对象数据上保留类型指针。(最后这句话的意思是,如果使用句柄,在后面讲了,则不会在对象数据上保留类型指针,而是在堆里面专门开辟了一个句柄池空间,来存放)。
类元数据:
实例数据
:也即对象成员变量字段内容。
实例数据的存储顺序
:存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响,相同宽度的字段总是被分配在一起。在父类中定义的变量会出现在子类之前
。
对齐填充
:仅仅起到占位符的作用**,**由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也即对象的大小必须是8字节的整数倍
。而对象头正好是8字节的整数倍(1倍或者多倍),因此对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
与32位图解的区别:
前面多了25位未使用 unused,中间多了一位unused,剩下的差距不大,除了比32位长一点。
对象头信息,就是object header中的信息!
Java 程序通过栈帧中局部变量表中的 reference 数据来操作堆上的具体对象。
由于reference类型在Java虚拟机规范中规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
目前主流的访问方式有使用直接指针
和句柄(了解即可)
两种:
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置类型数据的相关信息(对HotSpot虚拟机而言,也即对象头中需要存放类型指针),而reference中存储的直接就是对象在堆中的内存地址。
HotSpot虚拟机就是使用此种方式,使用直接指针访问方式的最大好处就是速更快。
如果使用句柄访问,那么Java堆中将会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
该流程图详细解析:
(1)new指令
虚拟机遇到一条new指令时,会进行类加载检查
(2)类加载检查
首先检查这个指令的参数能否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
(3)对象堆内存分配
在类加载检查通过后,接下来虚拟机将为新生对象分配内存;
对象所需内存的大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
有两种内存分配方式:
指针碰撞(标记整理-压缩):要求堆规整,也即要求垃圾收集器带有压缩整理的功能,具体有 Serial、ParNew
空闲列表(标记清理):不要求堆规整,像CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表
线程安全问题的两种解决方案:
CAS+失败重试:对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
TLAB:将内存分配的动作放在线程本地空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
(4)初始化为零值
将对象实例数据初始化为零值。
(5)对象头设置
设置对象头,例如如何才能找到类的元数据信息(类型指针)、对象的哈希码(MarkWord)、对象的分代GC年龄(MarkWord)、是否启用偏向锁(MarkWord)等
(6)执行方法
即执行对象的构造方法。
简化
整个对象创建过程的图,合并了new指令、检查解析等过程,用于面试讲解用(面试不用讲上面那么细)!
了解对象的创建过程,能够处理,第一,上图中的各个地方的代码打印顺序;第二,也能更加明白一个对象创建的时候会不会有问题,在美团的一个面试题里面,就有一个问题是创建Object对象会发生什么?为什么DCL中需要加volatile?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类的整个生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备、解析3个部分统称为连接。
虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化“(而加载、验证、准备自然需要在此之前开始):
(1)遇到 new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main() 方法的那个类),虚拟机会先初始化这个主类。
(5)当使用JDK 1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
在加载阶段,虚拟机需要完成以下3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的 java.lang.Class对象(存在方法区),作为方法区这个类的各种数据的访问入口。
加载与连接阶段的部分内容是交叉进行的,这两个阶段的开始时间保持固定的先后顺序
验证的目的是:为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
主要进行如下验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
在方法区中,为类变量分配内存,并设置类变量初始零值。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:如全类名、字段描述符、方法描述符。
直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进。
初始化阶段是执行类构造器()方法的过程。
()方法是由编译器自动收集类中的所有 类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。虚拟机会保证在子类的 () 方法执行之前,父类的() 方法已经执行完毕。
双亲委派模型的工作过程是:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
额外自己研究资料:https://app.yinxiang.com/fx/e5892df0-7ff4-4df0-8bc5-516806d40275
核心词语:STW,stop the world!
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用。
引用计数法判定效率高,但很难解决对象之间相互循环引用的问题(不过JVM是使用的根可达算法解决的)。
通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots没有任何引用链相连时,则证明此对象是不可用的。
引用:
强引用:不回收
软引用:内存溢出前回收
弱引用:下一次垃圾收集时回收
可以作为GC roots根节点: 类加载器,Thread,虚拟机栈的本地变量表,static成员,常量引用,本地方法栈的变量等。
非标准算法,是JVM内存的GC回收方案,内部包含了2.1 到2.3 3种算法。
对象的内存分配,往大方向讲,就是在堆上分配(例外:逃逸分析)。
同时还需记住一点:分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合
,还有虚拟机中与内存相关的参数的设置。
(1)对象优先在 TLAB 上分配
Thread Local Allocation Buffer,即线程本地分配缓存区,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。
(2)对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
(3)大对象直接进入老年代
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
(4)长期存活的对象进入老年代
年龄计数:虚拟机给每个对象定义了一个对象年龄(Age)计数器,对象每经历一次Minor GC ,年龄就加1,当它的年龄增加到一定程度(默认为15岁,而且不能超过15),就将会被晋升到老年代中。年龄阈值:对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
(5)空间分配担保
Minor GC在发生之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间;
如果大于,那么Minor GC 可以确保是安全的。如果不成立,则虚拟机会 查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC也就是说,如果Minor GC不安全,虚拟机就进行 Full GC。
新生代GC(Minor GC):发生在新生代的垃圾收集动作。因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
Major GC的速度一般会比Minor GC慢10倍以上。
参考:Minor GC ,Full GC 触发条件是什么?
Full GC触发条件:
(1)System.gc()方法的调用
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
参数控制:-XX:+UseSerialGC 串行收集器
ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩参数控制:-XX:+UseParNewGC ParNew收集器。
-XX:ParallelGCThreads 限制线程数量
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩。
参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行。
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
**这个收集器是在JDK 1.6中才开始提供参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)
优点: 并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
参数控制:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:
空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。