文章目录
JVM概述
JVM位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互
JVM体系结构
(橙色区域为共享区域,有GC,灰色区域为私有区域,无GC)
类装载器Class loader
(快递员的角色将class文件从硬盘加载到方法区,形成类模板Class)
负责加载class文件(字节码文件),class文件在文件开头有特定的文件标识(可以用以JVM判断class文件是否合法),将class文件字节码内容加载到内存中,并将这些内容转换成方法区 中的运行时数据结构(类模板),并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
Book book = new Book();
方法区class模板 栈,引用 实例对象,堆
类加载是一个将class字节码文件实例化成Class对象并进行相关初始化的过程。class是关键字用来定义类,而Class它是所有class的类。
类装载器分类
- 启动类加载器(Bootstrap)C++
- 扩展类加载器(Extension/Platform)Java
- 应用程序类加载器(AppClassLoader)
- 用户自定义加载器
根加载器为BootStrap,它是通过C++实现的并不属于JVM体系内,所以在查看根加载器时,会输出null
类加载器初始化时加载了什么?
- 根加载器(启动类加载器)将jdk底层核心类提前加载好(eg:rt.jar)
- 扩展类加载器提前加载扩展包
类加载器加载机制
❓ 双亲委派机制 ➕ Java的沙箱安全机制
(Java里面,如何加载一个类,它有三个类加载器,如何保证不冲突,且使用的类是同一个?)
双亲委派机制
自顶向下,逐步求精。从上开始往下找,出了事情,先往上捅。
当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次的加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求时(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
优点:比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同一个Object对象。防止内存中出现多份同样的字节码(安全性角度,即沙箱安全机制)
Execution Engine
执行引擎负责解释命令,提交操作系统
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为 Java 方法服务,本地方法栈为本地方法服务。
一般情况下,我们在java中所说的栈只是Java栈,不包含本地方法栈。
在本地方法栈中登记native方法,在Execution Engine执行时加载本地方法库
线程启动方法start()底层实际上是调用的start0()👇
# 只有方法的声明,没有方法体
# 方法声明为native或abstract时,可以只有声明,没有实现
# native,不归java管了,表示调用底层操作系统的或者C语言编写的第三方函数库
private native void start0();
PC寄存器(程序计数器,Program Counter Register)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间。(程序计数器在CPU中,作用于内存)
❗它是当前线程所执行的字节码的行号指示器。
这块内存区域很小,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
方法区(Method Area)
供各线程共享的运行时内存区域,它存储了每一个类的结构信息(Class),例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。
但方法区只是JVM中提出的一种规范,一种约束,一种接口,它必须有实现类,在不同虚拟机中实现是不一样的。典型实现:
Java8之前,永久代(PermGen space)
Java8之后,元空间(Metaspace)
永久代是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境所必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭了JVM才会释放此区域所占用的内存;
而元空间在本地内存中分配,在JDK8中,永久代中的所有内容中字符串常量移动到堆内存,其他方法包括类元信息、字段、静态属性、方法、常量等都移动到元空间中)
永久代使用的是JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存,因此,默认情况下,元空间的大小仅受本地内存限制。
类加载顺序
类的加载顺序,语法规定,优先级
- 静态的东西,是全局共有的,是必然注定被优先加载到方法区的(静态先行),加载时加载且仅加载一次
- 构造酷爱,每new一次,就加载一次,优先级高于构造方法
静态代码块>构造代码块>构造方法
普通代码块的加载顺序问题:“先出现先执行”。
构造代码块:直接在类中定义且没有static修饰没有名字的{}代码块称为构造代码块,构造代码块在每次创建对象时都会被调用,并且构造代码块的执行顺序优于类构造函数。
静态代码块:
栈
栈也叫栈内存,主管java程序的运行,是在线程创建时创建,它的生命周期是跟随线程线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程以接收苏该栈就over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
每一个方法就是一个栈帧(Java叫方法,虚拟机叫栈帧,本质上方法即栈帧)
栈管运行,堆管存储
栈存储什么?
栈帧中主要保存3类数据:
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量
- 栈操作(Operand Stack):记录出栈、入栈的操作(PC寄存器负责)。
- 栈帧数据(Frame Data):包括类文件、方法等。
栈运行原理
栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2就也被压入栈中,B方法又调用了C方法,于是产生栈帧F3也被压入栈中······执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧······
每个方法执行的同时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至运行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间
java.lang.StackOverFlowError
StackOverFlowError异常Exception还是错误Error?
Error
栈+堆+方法区的交互关系
HotSpot使用指针的方式来访问对象;
Java堆中会存放访问类元数据(Class模板)的地址;
reference存储的就直接是对象的地址。
堆Heap
Java7之前:
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以便执行器执行。
堆存放内容
Java堆是JVM内存中最大的一块,由所有线程共享,是由垃圾回收器管理的内存区域,主要存放对象实例,Java8现在主要有:
- 对象实例
类初始化生成的对象
基本数据类型的数组也是对象实例 - 字符串常量池
原本存放于方法区中,从jdk7开始放在堆中;
字符串常量池存储的是String对象的直接引用,而不是直接存放的对象,是一张String table; - 静态变量
静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中; - 线程分配缓冲区(Thread Local Allocation Buffer)
线程私有,但不影响Java堆的共性;
增加线程分配缓冲区是为了提升对象分配时的效率。
堆体系结构概述
Java7堆内存逻辑上分为三部分:新生+养老+永久
Java8堆内存逻辑上分为三部分:新生+养老+元空间
Java7和Java8中堆内存的区别:
1)永久代——>元空间;
(永久代是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境所必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭了JVM才会释放此区域所占用的内存;
而元空间在本地内存中分配,在JDK8中,永久代中的所有内容中字符串常量移动到堆内存,其他方法包括类元信息、字段、静态属性、方法、常量等都移动到元空间中)
2)永久代使用的是JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存,因此,默认情况下,元空间的大小仅受本地内存限制。
- 新生区(1/3堆空间)
(1)伊甸园区 (8/10)
(2)幸存者0区(S0,from区) (1/10)
(3)幸存者1区(S1,to区)(1/10) - 养老区(2/3堆空间)
- 元空间
实际而言,方法区和堆一样,是各个线程共享的内存区域,它用于虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等。虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫Non-Heap,目的就是要和堆分开。
GC = 发生在新生代的轻量级GC(GC,minorGC)+ 发生在养老区的重量级GC(FullGC,majorGC)
新生代发生的垃圾回收,用的算法就是复制算法
GC回收中,一般说GC就是用复制算法进行垃圾回收
小口诀:(新生代)GC(复制)之后有交换,谁空谁是to
java.lang.OutOfMemoryError:Java heap space
OOM,属于Error,表明Java虚拟机堆内存不够,原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整;
(-X常量,固定写法;m单位mb;s表示start;x表示max,若要调整,要保证-Xms 与 -Xmx相等,防止内存抖动)
(2)代码中创建了大量大对象,并且长时间不能被GC回收(存在被引用)。
(Object obj = new Object(),一般创建对象大小16kb )
MinorGC流程(复制——>清空——>交换)
- 对象在创建时在新生代中的伊甸园区;
- 当伊甸园区满时会触发第一次minorGC,把还活着的对象拷贝到S0(From)区,当伊甸区再次触发GC时会扫描伊甸区和From区,并对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区(如果有对象的年纪已经到达了老年的标准,即经过15次GC依旧存活,则复制到养老区),同时把这些对象的年龄➕1;
- 清空伊甸区和From区,也即复制之后有交换,谁空谁是To;
- 最后,To区与From区互换,原To区成为下一次GC时的From区,部分对象会在From区和To区中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终如果还是存活,就存入养老区;
- 大对象特殊情况:
如果分配的新对象比较大伊甸区放不下但养老区可以放下时,对象会直接被分配到养老区。
❓新生区为什么需要幸存者区?
幸存者区的预筛选保证只有经历15次MinorGC还能在新生代中存活的对象才会被送到老年代,减少被送到老年代的对象数量,进而减少FullGC的发生。
养老区对象越来越多会触发FullGC,这是重量级操作,可能会导致JVM暂停,越少越好。
❓新生区为什么要有两个幸存者区?
设置两个幸存者区最大的好处是解决内存碎片化。
总结
GC
Java8内存结构
GC
GC垃圾回收机制遵守分代收集的算法思路:
- 频繁收集新生代
- 很少收集老年代
- 基本不动元空间(永久代)
对象存活判断(判断对象是不是垃圾)
- 引用计数
JVM一般不采用这种方法
缺点:1)每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗;2)较难处理循环引用 - GCRoots根可达算法
为了解决引用计数法的循环引用问题,Java使用了可达性分析算法。
GC roots即一组必须活跃的引用。
基本思路:通过一系列名为“GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可达到的)对象就被判定为存活,否则就被判定为死亡。
Java中可以作为GC Roots的对象
- 栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native方法引用的对象
GC三大算法
- 复制算法(Copying)
新生代中使用的是MinorGC,这种GC算法采用的是复制算法。
基本思想:将内存分为两块,每次只使用其中一块,当这一块内存用完后,就将还活着的对象复制到另外一块内存中。
优点:没有标记和清除的过程,效率高;复制算法不会产生内存碎片。
缺点:需要双倍的空间,浪费了to区的内存。 - 标记清除(Mark-Sweep)
在老年代中使用。
基本思想:算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。
优点:节约内存
缺点:会产生碎片 - 标记压缩(Mark-Compact)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
基本思想:标记清除后,再次扫描,并往一端滑动存活对象。(标记-清除-压缩-整理)
优点:节约空间,无碎片
缺点:效率低
内存效率:复制算法>标记清除算法>标记整理算法
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
面试题
- JVM内存模型以及分区,需要详细到每个区放什么
- 堆里面的分区及各自的特点
- GC三种收集算法各自原理及特点,分别用在什么地方
- Minor GC与Full GC分别在什么时候发生