灰色的为单独线程私有,红色的为多个线程共享,即:
- 每个线程:独立包括程序计数器、虚拟机栈、本地栈
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存等)
内存
内存是硬盘和CPU的中间仓库和桥梁
不同的JVM对于内存的划分方式和管理机制存在着部分差异
PC寄存器 程序计数寄存器(PC计数器)
JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟, 是线程私有的和线程生命周期保持一致
作用:PC寄存器用来存储指向下一条指令的地址,由执行引擎读取下一条指令
即没有GC,也没有OOM
1.使用PC寄存器存储字节码指令地址的用处
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
2.PC寄存器为什么被设定为线程私有的呢
CPU不停的做任务切换,这样必然导致经常中断恢复,为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器
虚拟机栈
栈是运行时单位,堆是存储时单位
虚拟机栈是线程私有的,栈里放的是一个个栈帧,一个栈帧对应一个方法
作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用与返回
访问速度仅次于程序计数器,每个方法执行伴随着入栈,执行结束后出栈。对栈来说不存在垃圾回收的问题
不需要GC,会有OOM(溢出),使用参数-Xss设置最大栈stack size
-Xss1m
栈运行原理
一个时间点只有一个活动的栈帧,即栈顶栈帧,这个栈帧叫做当前栈帧,对应的方法是当前方法,定义这个方法的类叫做当前类
不同线程所包含的栈帧是不允许存在相互引用的,方法return和抛出异常都会使栈帧出栈
栈中存储什么?
栈中的数据都是以栈帧的格式存在的,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
每个栈帧中存储着:
- 局部变量表
- 操作数栈(或表达式栈)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或异常退出的定义)
- 一些附加信息
局部变量表(Local Variables)
-
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,
这些数据类型包括各类基本数据类型、对象引用、以及returnAddress类型(都是数字) -
由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题,
-
局部变量表所取得容量大小是在编译期确定下来的,方法运行期间是不会改变局部变量表的大小的。
-
方法调用结束,随着方法栈帧的销毁,局部变量表也会随之销毁
局部变量表中的变量也是重要的垃圾回收的根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
Slot 槽
局部变量表的最基本的存储单元是 Slot 槽,
局部变量表里32位以内的类型只占一个slot(包括returnAddress类型),64位的类型(long和都double) 占用两个slot,
如果需要访问局部变量表中的64bigt的局部变量值时,只需要使用演一个索引即可。
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处
slot 的重复利用
栈帧中局部变量表中的槽位是可以重复利用的
public void localVal() {
{
int a = 0;
}
int b = 0; //此时b会复用a的槽位,因为a已经过了作用域了,再也用不了它了
}
静态变量和局部变量的对比
变量分类:按照在类中声明的位置分:
- 成员变量: 在使用前都经过默认初始化赋值
类变量(static)linking的prepare阶段默认赋值 initial阶段显式赋值
实例变量: 随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值 - 局部变量: 在使用前必须显式赋值,否则编译不通过
操作数栈(Operand Stack)
在方法执行过程中,根据字节码指令往操作数栈中写入或读出数据
主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
当一个方法开始执行的时候,一个新的栈帧也会随之被创建,同时栈帧里的操作数栈也被创建好了,栈所需的最大深度在编译期就定义好了
如果调用的方法有返回值的话,其返回值也会被压入当前栈帧的操作数栈中
Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈
栈顶缓存技术
将栈顶元素全部换成在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率
动态链接 (指向运行时常量池的方法引用)
Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。
动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用
为什么要常量池呢?
常量池的作用就是为了提供一些符号和常量,便于指令的识别,而不是一个字节码文件把所有相关的东西都得加载进来
方法的调用
JVM中将符号引用转换为调用方法的直接引用,与方法的绑定机制相关。绑定是一个字段、方法、或者类在符号引用被替换成直接引用的过程,这仅仅发生一次。
静态链接:
被调用的目标方法在编译期可知
动态链接:
被调用的方法在编译期间无法确定下来,哪个对象调哪个方法
非虚方法:
静态方法、私有方法、final方法、实例构造器、父类方法
其他方法称为虚方法
方法重写的本质!
先从所执行对象找方法,找不到再从父类找方法。
为了提高性能,采用在类的方法区建立一个虚方法表,使用索引表来代替查找
方法返回地址
存放该方法的PC寄存器的值,【PC寄存器存的是该方法要执行的下一条指令的值】
方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令地址
异常退出时,返回地址是通过异常表来确定的,栈帧中一般不会保存这部分信息。
栈相关面试题
- 举例栈溢出情况 StackOverflowError 通过-Xss设置栈的大
- 垃圾回收不会涉及栈空间
- 方法中定义的局部变量是否线程安全?内部定义且内部消亡的安全,外部传过来或者内部传出去的不一定
本地方法栈
Java虚拟机栈用于管理Java方法调用,本地方法栈用于管理本地方法的调用
当某个线程调用一个本地方法时,他就进入了一个全新的并且不受虚拟机限制的世界,他个虚拟机拥有相同的权限
并不是所有JVM都支持本地方法,因为Java虚拟机规范没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等
什么是本地方法
一个Native Method 就是一个Java调用非Java代码的接口
堆
堆的核心概述
一个JVM实例只存在一个堆内存,在JVM启动的时候即被创建,其大小空间就确定了,是JVM管理的最大的一块内存空间。堆内存的大小是可以调节的
堆可以处于在物理上不连续的内存空间中,但是逻辑上它应该被视为连续。
所有线程共享Java堆,但是在堆还可以划分线程私有的缓冲区 TLAB
几乎所有的对象实例以及数组都应该在堆分配内存。【逃逸分析】
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
内存细分
jdk8堆内存逻辑上分为三部分:新生区+养老区+元空间
jdk7 元空间是永久代
新生区又被划分为Eden区和Survivor区
堆空间大小的设置
-Xms 设置堆空间(年轻代+老年代)的初始内存大小,等价于 -XX:InitialHeapSize
-X 是jvm运行参数, ms是memory start
-Xmx 设置堆空间(年轻代+老年代)的最大的内存大小,等价于-XX:MaxHeapSize
默认初始堆空间内存大小:物理电脑内存大小/64 最大内存大小:物理电脑内存大小/4
建议初始堆内存和最大的堆内存设置成相同的值
-XX:+PrintGCDetails 打印堆内存情况
年轻代与老年代
-XX:NewRatio=2 :设置老年代与新生代的内存大小比值, 默认是2 即1/3新生代 2/3老年代
-XX:SurvivorRatio=8 :设置新生代中Eden区和Survivor区的比例
-XX: -UseAdaptiveSizePolicy 关闭自适应内存分配策略(暂时用不到)
-Xmn: 设置新生代的空间大小(一般不设置)
几乎所有对象都是在Eden区被new出来的
绝大部分的Java对象的销毁都在新生代进行了
对象分配过程
Eden区满了,来一次Minor GC 幸存的放到s0区,age = 1,垃圾回收掉
Eden区又满了,再来一次Minor GC, 把幸存的放到空(to)的s1区,s0区的如果还被占用就去s1区 【谁空谁是to区】
Eden区又满了,然后幸存区的对象年龄计数器到15【默认阈值】了 ,就晋升到老年代了
可以设置参数: -XX:MaxTenuringThreshold=进行设置
s0和s1区满的时候不会触发YGC,Eden区满的时候会触发YGC,顺便对s1,s0进行回收
总结:
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集
新对象往Eden区放,放不下就YGC,YGC还放不下就放Old,Old放不下就FGC,还放不下就报错了。
YGC之后Eden区就清空了,不是垃圾的对象放s0/s1区,不是垃圾的太多了,s0/s1放不下那就方法Old区
Minor GC 、Major GC 、 Full GC
部分收集:
新生代收集(Minor GC / Young GC): 只是新生代(Eden、s0 、s1)的垃圾收集
老年代收集(Major GC / Old GC):只是老年代的垃圾收集
目前,只有CMS GC 会有单独的收集老年代的行为
很多时候Major GC 会和Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
目前只有G1 GC 会有这种行为
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
Minor GC
当年轻代的Eden区满的时候会触发Minor GC,
Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束用户线程才恢复
Major GC
Major GC 的速度一般比Minor GC 慢10倍以上, STW时间更长
如果Major GC内存还不足,就报OOM
TLAB(Thread Local Allocation Buffer)
由于多线程操作同一个堆区,所以可能有线程安全问题。
TLAB是从内存模型而不是垃圾收集的角度,对Eden区进行划分,JVM为每个线程分配一个私有的缓存区域,它包含在Eden 空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,把这种内存分配方式称为快速分配策略。
默认情况下,TLAB空间的内存非常小,仅占整个Eden空间的1%
一旦对象在TLAB空间分配内存失败时,JVM会尝试通过加锁机制确保数据操作的原子性,从而直接在Eden区分配内存
堆空间参数设置总结
- -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
- -XX:+PrintFlagsFinal: 查看所有的参数的最终值(可能会存在修改,不再是初始值)
- -Xms:初始堆空间内存大学 默认物理内存的1/64
- -Xmx:最大堆空间内存 默认为物理内存的1/4
- -Xmn: 设置新生代的大小(初始值及最大值)
- -XX:NewRatio:配置新生代与老年代在堆结构的占比
- -XX:SurvivorRatio 设置新生代Eden区和S1、S0区的比例 (默认是8)
- -XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails 输出详细的GC日志
- -XX: HandlerPromotionFailure:是否设置空间分配担保 JDK7后失效,就是true
在Minor GC之前会检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,大于则尝试进行一次Minor GC(有风险),小于则改为进行一次Full GC。
堆空间是分配对象存储的唯一选择吗?
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化
如果经过逃逸分析后发现,一个对象并有逃逸出去的方法的话,那么就可能被优化成栈上分配,这样就无需再堆上分配内存,也无需垃圾回收,这是最常见的堆外存储技术。
逃逸分析
当一个对象在方法中被定义以后,对象只在方法内部使用,则认为没有发生逃逸。
就看new 的对象是否有可能在方法外被调用
JDK 7 后默认开启逃逸分析
结论:开发中能使用局部变量的,就不要使用在方法外定义
使用逃逸分析,编译器可以对代码做如下优化:
- 栈上分配
- 同步省略 。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
- 分离对象或标量替换 。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是储存在CPU寄存器(栈)中,
标量替换:将对象打散放在栈上的局部变量表里
总的来所,对象实例还是分配在堆上的,逃逸分析技术不够成熟,我觉得使用逃逸分析,主要靠标量替换对对象进行栈上分配,所以说对象还是在堆空间,
方法区
方法区是堆中的一个逻辑部分,jdk1.7用永久代实现,jdk1.8用元空间实现
方法区
方法区概述
方法区是堆中的一个逻辑部分,jdk1.7用永久代实现,jdk1.8用元空间实现
方法区逻辑上是属于堆的一部分,但是可能不会进行垃圾回收或者进行压缩,对于HotSpotJVM而言,方法区还有一个别名叫 Non-Heap(非堆),目的就是要和堆分开。
方法区可以看作独立于Java堆的内存空间
启动jvm方法区就被创建,关闭jvm方法区内存释放
方法区大小决定了系统可以保存多少个类,如果定义了太多类,会报OOM
例如:加载大量第三方jar包, Tomcat部署的工程过多(30-50个), 大量动态的生成反射类。
设置方法区大小与OOM
元空间数据大小可以使用参数: -XX:MetaspaceSize 和 -XX:MaxMetspaceSize指定,
对于一个64位的服务器端JVM来说,其默认MetaspaceSize值为21MB,这就是初始的高水位线,一旦触及这个水位线,就会Full GC,卸载没用的类,然后重置高水位线【提高或降低】
方法区内部结构
存储内容包括: 类型信息、常量、静态变量、即使编译器编译后的代码缓存
类变量被类的所有实例共享,即使你没有类实例时你也可以访问它
例如
public class 类变量 {
public static void main(String[] args) {
Order order = null;
order.hello();
System.out.print(order.count);
}
}
class Order{
public static int count = 1;
public static void hello(){
System.out.print(" hello");
}
}
输出: hello1
常量池【Class文件的一部分】
常量池中存储的数据类型包括:数量值,字符串值,类引用,字段引用,方法引用
常量池可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
运行时常量池
运行时常量池是方法区的一部分,常量池表是Class文件的一部分,这部分内容将在类加载后存放到方法区的运行时常量池中
运行时常量池中包含多种不同的常量,包括编译期就确定的数值字面量,也包括运行期解析后才能获得的方法或者字段的引用。此时已经不再是符号地址了,而是真实地址
运行时常量池具备动态性
方法区演变细节
只有HotSpot才有永久代
jdk1.6及以前 | 有永久代,静态变量存放在永久代上 |
---|---|
jdk1.7 | 有永久代,但是已经逐步去永久代,字符串常量池、静态变量移除,保存在堆中 |
jdk1.8及以后 | 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆 |
为什么要用元空间替代永久代?
- 为永久代设置空间的大小是很难确定的
- 对永久代进行调优是很困难的
StringTable(字符串常量池)为什么要调整?
因为永久代的回收效率很低,只有在Full GC时才会触发,而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致StringTable的回收效率不高,但是开发中会有大量字符串被创建,回收效率低,导致永久代内存不足。放到堆里能即使回收内存。
静态变量存在哪?
静态引用对应的对象实体始终存在堆空间【jdk6、7、8都是】,对象实体都在堆空间,静态变量位置变化指的是静态引用,也就是静态变量与类型在Java语言一端的映射和Class对象放在了一起,存储于Java堆中
方法区的垃圾收集
方法区的垃圾收集主要回收两部分内容: 常量池中废弃的常量和不在使用的类型
只要常量池中的常量没有被任何地方引用,就可以被回收
运行时数据区总结及大厂面试题
开发中遇到的异常
栈中可能出现的异常
- 如果采用固定大小的Java虚拟机栈,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,会抛出StackOverflowError
- 如果Java虚拟机栈可以动态扩展,如果在尝试扩展的时候无法申请到足够的内存,或者创建新的线程时没有足够的内存区创建对应的虚拟机栈,那么会报OutofMemoryError异常