JVM 尚硅谷
- 一、JVM整体结构
- 二、类加载子系统
- 三. 运行时数据区
- 四.本地方法库
- 五 对象的实例化
- 六. 执行引擎
- 七. String Table
- 八. 垃圾回收算法
- 八. 垃圾回收的一些概念
- 九. 垃圾回收器
一、JVM整体结构
HotSpot VM是市面上高性能代表作之一。
执行引擎:解释运行,高级语言翻译成机器语言。
包括解释器,JIT 编译器,垃圾回收GC。
1.Java代码执行流程
java虚拟机执行字节码文件,操作系统只识别机器指令。
2.JVM架构模型
Java编译器输入的指令流是一种基于栈的指令集架构,
另一种是基于寄存器的指令集架构(x86)。
基于栈的指令集架构:
使用零地址指令方式分配,指令集小,8位,但用的指令多。
javap -v .class//反编译
基于栈的指令集架构:
跨平台性,指令集小,指令多;执行性能比寄存器差。
设计实现简单。
3. JVM的生命周期
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(inital class)来完成。
自定义类 通过 系统类加载器加载。
虚拟机的退出:Runtime类或System类的exit方法,Runtime类的halt方法。
JNI也有
二、类加载子系统
1. 结构
2.类加载子系统作用
负责从文件系统或者网络中加载.class文件
加载 链接 初始化
加载 loading:
通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class, 作为方法去这个类的各种数据的访问入口
链接linking
初始化
任何一个类声明以后,至少存在一个类的构造器。
3. 类加载器的分类:
引导类加载器是用c++写的
下面四个是包含关系,不是继承关系。
ClassLoader 是一个抽象类(abstract) 都不是抽象方法
对于用户自定义类来说:默认使用 系统类加载器 进行加载
Java的核心类库都是使用引导类加载器进行加载的。
为什么要自定义类加载器?
隔离加载类
修改类的加载方式
扩展加载源
防止源码泄漏
4. 双亲委派机制
向上委托,如果父类不能加载,才由自己处理。
优点:
避免类的重复加载
保护程序安全,防止核心API被篡改
5. 沙箱安全机制
自定义String 类,加载时先使用引导类加载器加载,防止引导类加载器被破坏。
保护
6. 类的主动使用和被动使用
区别在于会不会导致类的初始化。
除了七种主动使用,其他都是被动使用。
三. 运行时数据区
1. 总体结构
运行时数据区:
一个JVM实例对应一个Runtime实例
2. 线程说明
JVM系统线程
不包括main线程以及所有这个main线程自己创建的线程。
有虚拟机线程,周期任务线程,GC线程,编译线程,信号调度线程。
3. 程序计数器 (PC register)
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,
由执行引擎读取下一条指令。
程序控制流的指示器,它是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域,也没GC。
栈结构没有GC,但有OOM。
PC register 例子 + 2个面试题
执行引擎去取PC寄存器对应行的指令
使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停地切换各个线程,这时候切换回来后,就得知道接着从哪开始继续执行。
JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么是线程私有?
准确记录各个线程正在执行的当前字节码指令地址,独立计算,互不干扰。
CPU时间片
每个程序轮流执行
4. 虚拟机栈(重要)
JVM就是基于栈设计,
优点:跨平台,指令集小,编译器容易实现,
缺点:性能下降
栈是运行时的单位, 堆是存储的单位(对象在堆中)。
栈的存储单位,栈帧,一个栈帧对应一个java方法。
作用:主管Java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。
a. 栈的特点:
访问速度仅次于PC寄存器。
只有两个操作:入栈,出栈。
不用GC,但有OOM。
b. 虚拟机栈的常见异常?
Java虚拟机栈的大小是动态的或者是固定不变的,
StackOverFlowError:
如果采用固定不变的栈,那每一个线程的JVM栈容量可以在线程创建时单独选定,线程请求分配的栈容量超过最大容量,抛出异常。(自己调自己 栈溢出)
OutOfMemoryError:
如果采用动态扩展的栈,在尝试申请扩展的时候 没有足够的内存 或 在创建新的线程时没有足够的内存创建对应的虚拟机栈,抛OOM异常。
设置栈内存大小:-Xss
c. 栈的存储结构和运行原理
每个线程都有自己的栈,栈中的数据以栈帧的格式存在,在这个线程上正在执行的每个方法都各自对应一个栈帧。
当前栈帧,当前方法,当前类。
方法的结束方式:
正常return;方法执行中出现未捕获的异常,抛出异常返回。
d.栈帧的内部结构
局部变量表和操作数栈是重点
局部变量表(1 很重要)
定义一个数字数组,
局部变量表是建立在线程的栈上,线程私有,不存在安全问题。
方法嵌套调用的次数由栈的大小决定,栈帧里主要影响它的大小的就是局部变量表。
用jclassjib查看
变量槽slot
局部变量表最基本的存储单元是slot
局部变量表里,32位以内的类型只占用一个slot,64位占两个 有long,double
静态方法中是不允许引入this的,this变量不存在于当前方法的局部变量表中。
非静态方法:this在索引为0处的slot
静态变量和局部变量的对比
变量的分类:
- 按照数据类型:基本数据类型(8种),引用数据类型(类,接口,数组)
- 按照在类中声明的位置:成员变量(会默认初始化),局部变量(必须初始化,显式赋值)。
在栈帧中,与性能调优关系最密切的部分就是局部变量表,其中的变量也是重要的垃圾回收根节点(根搜索算法?),只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈 operand stack(2)
栈:可以用数组或链表实现。
- 保存计算过程中的中间结果,同时作为计算过程中变量的临时存储空间。不是通过索引,push pop。
- JVM的解释引擎就是基于栈的执行引擎,其中的栈就是操作数栈。
- 当一个方法开始执行的时候,一个新的栈帧被会随之被创建出来,此时这个方法的操作数栈是空的。
栈顶缓存技术 Top of Stack Cashing
栈式架构,零地址指令,指令集小,指令多。
所以 将栈顶元素全部缓存到物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
动态链接(3)
每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
在字节码文件中,有一个区域是常量池 constant pool。(方法区)
动态链接的作用就是在每一次运行期间将符号引用转换为调用方法的直接引用。
静态解析:在类加载的时候就转化为直接引用。
常量池: 提供一些符号和常量,便于指令的识别。
静态链接(目标方法在编译期确定):
动态链接(运行期):
虚函数,在运行期才能确定下来
JVM提供5条方法调用字节码指令,
虚方法和非虚方法:
非虚方法:在编译期就确定了具体的调用版本,包括静态方法,私有方法,final方法,实例构造器,父类方法。
不加super. 说明子类可能会重写,无法在编译期间确定,就是虚方法。多态
动态类型语言(判断变量值类型)和静态类型语言(判断变量类型):
对类型的检查是在编译期还是运行期。
java里的lambda表达式是invokedynamic(java7中新加的),动态语言。
虚方法表:
为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找。
方法返回地址(4)
方法正常退出时,存放调用该方法的pc寄存器的值。即返回到方法被调用的位置;方法异常退出时,返回地址是要通过异常处理器表来确定。
附加信息(5)
例如 对程序调试相关的信息,可以没有。
e.虚拟机栈的五个面试题
1 举例栈溢出的情况? StackOverFlowerror
- 通过设置-Xss 设置栈的大小
2 调整栈的大小,能保证不溢出嘛?不能
如果本身是死循环 肯定不行。
3 分配的栈内存越大越好吗?
使得线程数变小,其他资源变少。
4 垃圾回收是否涉及到虚拟机栈?不是!
5 方法中定义的局部变量是否线程安全?具体分析
StringBuilder 线程不安全,转为 String 就安全了
StringBuffer 线程安全,有syno同步
线程安全?
如果只有一个线程可以操作此数据或。多线程共享数据,且不考虑同步机制,会存在线程安全问题。
5.堆
- 最大空间
- 方法区和堆对于一个进程来说是唯一的,一个JVM实例
- 堆在物理上不连续,逻辑上应该被视为连续
- 方法结束后,堆中的对象不会马上被移除,仅在垃圾回收时才被移除
- 所有的对象实例及数组都应在运行时分配在堆上,栈里放引用对象的地址值
但其实 堆空间只有 新生区,养老区。
-Xmx 分配的最大内存是电脑内存的1/4
-Xms 初始化内存1/64
-XX: PrintGCDetails 打印垃圾回收信息
OOM 异常
堆空间内存溢出,对象或数组实体 撑爆内存空间。
堆区结构
8:1:1 Eden:from:to的分配,新生:老年 1:2
两个survivor区 由于碎片化。
对象分配过程
来对象,Eden区满,进行young GC/MInor GC,把不是垃圾的 提到幸存者区,第二次young GC后,把所有不是垃圾的 放到 to区(from区的也要判断一下),直到年龄计数器达到15,晋升promotion到老年代。
最大的15临界值可以修改,
触发young GC:Eden区满,幸存者区满不会触发,但是Eden区满触发young GC,也会一起回收S0,属于被动回收。
- 复制之后有交换,谁空谁是to
- 关于GC:频繁收集新生区,很少收集养老代几乎不在永久区/元空间收集。
垃圾回收过程
Minor GC 与Major GC与FUll GC
JVM在进行GC时,并非每次都对新生代,老年代;方法区 一起回收,大部分回收的都是新生代。
from to区的GC是被动的
full GC触发条件
分代思想:
优化GC性能
内存分配策略
堆空间为每个线程分配的TLAB
Thread Local Allocation Buffer
堆区是线程共享,由于对象实例的创建在JVM中很频繁,因此在并发环境下从堆区中划分内存空间线程不安全;为避免多个线程操作同一地址。
- 对Eden区继续划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间内
- 避免线程不安全,TLAB是内存分配的首选
堆空间的常用参数设置
堆是分配对象存储的唯一选择吗?
逃逸分析技术:栈上分配,标量替换优化。
如果逃逸分析后发现,一个对象并没有逃逸出方法的话,就可能栈上分配,不用GC,堆外存储。
是否发生逃逸?
- 一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。可以栈上分配
- 被外部方法所引用,发生逃逸,如 作为调用参数传递到其他地方
能使用局部变量的,就不要在方法外定义。
优化 83-86 没看
6.方法区
方法区里面放的类
栈 堆 方法区的交互关系
栈中的局部变量表里放的 对象引用。
- 方法区落地实现在元空间,线程共享。
jdk8之前,方法区的实现是永久代;jdk8开始,方法区的实现是元空间,元空间使用本地内存。
//参数设置 jdk8
-XX:MaxMetaspaceSize=size
-XX:MetaspaceSize=size
如何解决OOM?(重要)
dump文件
内存泄漏
没有必要静态,有没有必要方法外使用。
方法区内部结构
字节码文件的方法区记录了加载器,彼此互相记录。
常量在编译期赋值,静态变量在初始化阶段赋值
有静态属性,进行过显式赋值或静态代码块赋值,对应有类构造器方法。
编译器会把
- 类变量初始化
- static{}块
-
收敛到 类构造器 <clinit>()方法 中,
<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物; <clinit>()方法是由编译器自动收集类中的所有 类变量的赋值动作 和 静态语句块(static{}块)中的语句 合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的;
运行时常量池vs常量池
常量池存放编译期生成的字面量和符号引用。
类加载器把字节码文件中的常量池加载到方法区 对应的结构叫运行时常量池。
运行时常量池具备动态性。
#几就是constant pool里的第几行
图解方法区
jdk6 7 8 方法区的演进
HotSpot中方法区的变化
永久代为什么要被元空间替代?
元空间在本地内存
- 为永久代设置空间大小很难确定:出现full GC,成本大;容易OOM,
- 对永久代调优很困难
String Table和静态变量为什么要放到堆空间?
堆空间中,回收频率更高。
如何证明静态变量存在哪?
arr 对象引用由方法区变化到了堆空间
方法区的垃圾回收
常量池中废弃的常量和不再使用的类型。
废弃常量:
不再使用的类型:
7.本地方法栈
- 管理本地方法的调用
- 本地方法可以通过本地方法接口来访问JVM的运行时数据区
- 可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
- hotspot JVM 把虚拟机栈和本地方法栈合二为一
打破双亲委派机制的三大机制?
常见面试题
四.本地方法库
Native method就是一个Java调用非java代码的接口。c
native关键字,使用的少。
- 与Java环境外交互
- 与操作系统交互:使用本地方法,实现底层交互
- sun‘s java Sun的解释器是用C实现的
五 对象的实例化
字节码角度看对象创建过程:
对象创建的六个步骤
1.
2
标记压缩算法
给对象的属性赋值的操作:
- 属性的默认初始化 (4 中的初始化)
- 显式初始化
- 代码块中初始化
- 构造器中初始化
5
6.调用类的构造器,进行显式初始化
对象的内存布局
上述代码对应对象的内存图
对象的访问定位
-
句柄访问:空间浪费,效率低;优点:引用地址很稳定,如果对象发生了移动(标记整理算法)
-
直接指针(更优):一步到位,不需要开辟空间记录句柄
直接内存(108 - 109 no see)
是Java堆外的,直接向系统申请的内存空间。
IO: NIO(New IO/ Non-blocking IO)JDK1.4引入
两个重要结构,传输工具
- byte[]/ char[] Buffer
- Stream Channel
六. 执行引擎
1. 概述
执行引擎就是将字节码指令解释/编译为对应平台上的本地机器指令,这里的编译不是前端编译。让字节码运行在操作系统之上。
输入:字节码二进制流
输出:执行结果
2 Java代码编译和执行过程
Java半编译半解释语言
解释器和JIT编译器
3 机器码 指令 汇编语言
4 解释器
hotspot JVM 解释器和JIT编译器并存。
响应速度快,拿到字节码文件就执行,省去编译时间。
5 JIT 编译器
把代码翻译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高,后期速度快
115-117 no see
七. String Table
1 String的基本特性
String的不可变性
String 字符串常量池不会存储相同内容的字符串
String 的String pool是一个固定大小的Hashtable,默认值大小长度是1009,jdk7中60013
intern()方法:如果字符串常量池中没有对应的data字符串,则在常量池中生成。
2 String的内存分配
字符串拼接操作
常量和常量的拼接:
变量拼接:
jdk5之后是StringBuilder 线程不安全,之前是Stringbuffer
3 intern()的理解
new String(“ab”) 创建了几个对象?几个面试题(混淆 jdk 6 8)
2个
- new关键字在堆空间
- 字符串常量池中的对象
变型:
jdk6
jdk8
G1的String去重操作
字符串常量池本身就不存在重复的,指的是去 char数组中的去重 堆空间对象
了解
八. 垃圾回收算法
1 概述
GC在执行引擎里
什么是垃圾?
在运行程序中没有任何指针指向的对象
早期垃圾回收
2 垃圾回收算法
- 标记阶段:引用计数算法,可达性分析算法(GC roots)。
- 清除阶段:标记清除算法,复制算法,标记压缩算法。分代手机算法,增量收集算法,分区算法。
finalize 对象的finalization机制
标记阶段:引用计数算法
区分哪些是存活对象,哪些是死的。用引用计数算法,可达性分析算法(GC roots)。
Java中没用引用计数算法。
标记阶段:可达性分析算法
也叫根搜索算法,追踪性垃圾收集。有效解决循环引用问题,防止内存泄漏发生。
- 直接或间接被GC roots连接到的 就是存活对象。
GC roots:
对象的finalization机制
对象被回收之前,调用finalize方法
MAT和JProfiler GC roots溯源
可以通过JProfiler 寻找GCroots,看是否需要用
清除阶段:标记-清除算法
- 优点:简单,基础
- 缺点:效率不算高,两次遍历。
- 在进行GC时,要停止整个应用程序
- 内存不连续,产生内存碎片,需要一个空闲列表(占据内存空间)
什么是清除?
清除阶段:复制算法
解决标记-清除算法效率不高的问题
如果系统中存活垃圾对象很多,复制算法就不理想。
在survivor区用的多,from to区,存活对象少,垃圾多的场景。
清除阶段:标记-压缩算法
碎片整理,对标记清除的压缩。移动式
三种算法对比
标记压缩 最中和
分代收集算法
- 年轻代:复制算法
- 老年代:标记清除和标记压缩
增量收集算法
增量收集:GC线程分阶段收集,与应用程序线程交替执行。
缺点:
分区算法
八. 垃圾回收的一些概念
System.gc()
内存溢出和内存泄漏
内存溢出:针对堆空间 OOM
没有空闲内存:
- 堆内存设置不够
- 代码中创建了大量大对象,长时间不能GC收集(存在被引用)
一般OOM之前都调了GC - 内存泄漏:(java虚拟内存)
对象不用了,但GC又回收不了。
举例:a. 某个对象的生命周期很长,宽泛意义上的泄漏
b. web应用程序存储到会话级别 但没必要
内存泄漏可能导致内存溢出,但不一定。
跟外部资源链接要及时关闭,不然会引起内存泄漏。
Stop the world (STW)
垃圾回收的并行与并发
操作系统
- 并发:一个CPU快速切换进程,
- 并行:多核,一个cpu执行一个进程,同时进行
垃圾回收
安全点和安全区域
- 安全点
- 安全区域
强 弱 软 虚 终结器引用
- 强引用:最多,不回收
- 软引用:内存不足才回收
- 弱引用:发现即回收
- 虚引用:对象回收跟踪
- 终结器引用
九. 垃圾回收器
GC分类与性能指标
在最大吞吐量优先的情况下,降低暂停时间。
七种经典的垃圾回收器
新生代垃圾回收器
- Serial:复制算法,串行回收,单线程,STW 高效
- ParNew:复制算法,并行回收,多线程,STW
- Parallel scavenge:复制算法,并行回收,多线程,STW
老年代垃圾回收器
- Serial old:标记压缩,串行回收,单线程,STW
- Parallel old:标记压缩,并行回收,多线程,STW
- CMS:并发!标记清除算法
Serial: 串行回收
Parallel回收器:吞吐量优先
CMS:低延迟
G1:区域化分代式 用的最多
CMS Concurrent Mark-Sweep
真正意义上的并发,标记清除算法
整堆垃圾回收器 G1
区域化分代式,jdk7引入
并行回收,垃圾优先
G1回收过程
p196