如何学习JVM—>看康师傅的JVM视频。
关于复习JVM—>我参考了之前我的笔记还有一些博客,归类为以下几个部分:
·
要是把上面的全理解了。JVM就只用复习调优部分了
2.JVM内存空间相关(包括了类的加载过程等)
JVM杂谈
- Java虚拟机是一台执行Java字节码的虚拟计算机。
- 他负责装载字节码到其内部->解释/编译为对应平台上的机器指令
- 特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
- HotSpot VM是目前性能最好的虚拟机之一
- 采用解释器与即时编译器并存的架构
- 发展历史
- Sun Classic VM 世界上第一款商用虚拟机,只提供解释器。(1996年JDK1.0)
- Exact [iɡ’zækt] VM 具备现代高性能虚拟机的雏形,有热点探测/编译器解释器混和工作 (JDK2.0)
- 重点:SUN公司 HotSpot VM 热点代码探测/即时编译栈上替换/编译解释器协同工作 (JDK3.0成默认的)
- 重点:BEA的JRockit 专注于服务端应用,不关注程序启动速度,不包含解释器,全部代码又即时编译器编译后执行,世界上最快,被Oracle收购并在JDK8 被整合进了HotSpot
- 重点:IBM的J9,广泛用于IBM自己的产品
- 其他VM:CDC/KVM 低端设备使用|Azul VM与特定硬件平台绑定(饿了么)|微软JVM 被SUN告了|TaoBaoJVM 淘宝(将Java对象移到堆外)|未来发展Graal VM 跨语言的全栈虚拟机
JVM的生命周期(启动执行退出)
- 启动
- 执行
- 退出
jps 打印进程
什么是字节码?采用字节码的好处是什么?
JVM能理解的代码就叫字节码
跨平台
Java类加载器有哪些
BootStrap ClassLoader(引导类)启动类加载器 :
- 使用C/C++语言实现,嵌套在JVM内部
- 加载Java核心类库(JVM自身需要的类)
%JAVA_HOME%
rt.jar包 - 出于安全考虑,只加载包名为java、javax、sun开头的类
- 并不继承于ClassLoader,没有父加载器。
Extension ClassLoader 拓展类加载器
- 派生于ClassLoader类 ,父类加载器是 启动类加载器 并不是直接的继承关系
- 主要用于加载核心库之外的拓展类包
- 如果用户自己创建的jar放在拓展目录下,也会由拓展类加载器加载
AppClassLoader 应用程序加载器(系统类)线程上下文加载器
- 派生于ClassLoader类 ,父类加载器是 拓展类加载器
- 是程序的默认加载器,一般来说java应用的类都是由他加载完成
- 负责加载环境变量或系统属性java.class.path指定路径下的类库
必要的时候 用户自定义类加载器(继承ClassLoader,要想完成双亲委派parent属性必须是AppClassLoader)
- 隔离加载类
- 修改类加载方式
- 拓展加载源
- 放置源码泄漏
如何获取类加载器
双亲委派模型?好处
JVM对class文件采用的是按需加载,用到这个类才把他的class文件加载到内存生成class对象,而加载某个类的class文件时,JVM采用双亲委派机制
Main java程序入口,就是用的AppClassLoader。
- 向上委派 查找缓存
- 向下查找 查找加载路径
好处:
- 安全性,避免用户自己编写的类替换Java的一些核心类 比如String (同时自定义类不能以 .java开头)
- 避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类
沙箱安全机制(简单情景介绍)
类的初始化过程
- 如果该类还未被加载到内存中,JVM通过—>加载、连接(验证、准备和解析)、初始化三个步骤来对该类进行初始化。
- 加载Loading 将类的class文件读入内存,并创建一个java.lang.Class对象,这个过程由类加载器通过双亲委派完成。
- 链接Linking 将类的二进制代码合并到JVM的运行状态中
- 验证 确保加载的类的信息符合JVM规范,无害。四种验证
- 准备 为类变量(static)分配内存并设置默认初始值,这些内存都将在方法区分配,不包含final修饰的static。
- 解析 将常量池中的符号引用转化为直接引用的过程。(还有虚方法表的创建)
- 初始化Initialization 执行类构造方法
clinit()
的过程,clinit是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并出来的。(这里就是实实在在的赋值了)
注意点1 再理解一下类变量(非final修饰)的赋值过程,如private static int number = 10;
链接的准备阶段 number = 0 ——> 初始化阶段 number: 0 -> 10
注意点2 类构造器方法clinit()不等同于我们常说的java类的构造器,java类的构造器的实际表现在虚拟机中是init()
类构造器是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并出来的。
若该类有父类,JVM会保证先执行完父类的clinit
注意点3 虚拟机必须保证一个类的clinit方法在多线程下被同步加锁(因为类只加载一次)
Java代码的执行流程
在类的加载过程基础上,谈谈加载前java->class ,加载后解释器和JIT编译器
翻译字节码和JIT编译器就是执行引擎的工作
解析执行:主要是保证响应时间。逐行对字节码文件进行解释执行
JIT编译器(执行性能):针对字节码指令中要反复执行的热点代码 再编译(二次编译)成机器指令 同时把机器指令缓存起来 放在方法区
说一下JVM内存结构吧?有哪些区?分别是做什么的,每个区具体放什么?(重点)
- 程序计数器(PC寄存器) 用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
- 虚拟机栈 1024k+8*2 主管Java程序的运行,内部保存一个个栈帧,对应一次次Java方法调用 ,(栈帧:局部变量表、操作数栈、动态链接、方法返回地址、附加信息)
- 本地方法栈 用于管理本地方法的调用 Hotspot 将本地方法栈和虚拟机栈合二为一,当线程调用一个本地方法,它就和虚拟机拥有同样的权限。
- 堆 所有的对象实例以及数组都应当在运行时分配在堆上。所有线程共享Java堆,在这里还可以划分线程私有的缓冲区、TLAB。
堆空间分为好几个部分:新生代(Eden、Survivor区)、老年代、永久代->元空间
- 方法区 逻辑上属于堆,但实际Non-Heap非堆,不占用JVM内存,占用本地内存(Hotstop JDK7 方法区叫永久代,JDK8叫元空间),用来存储被虚拟机加载的类型信息(不仅仅是类,还有接口、枚举、注解)、常量、静态变量、即时编译器、编译后的代码缓存。JDK版本变动对于静态变量和字符串常量池变化挺大。
JVM内存之 程序计数器相关问题
可能会问为什么程序计数器是线程私有的?为什么使用PC寄存器记录当前线程的执行地址?
很简单,因为并发,一个线程可能执行一半就去执行另外一个线程,所以我们通过程序计数器记住线程代码执行的位置。
JVM内存之 虚拟机栈相关问题
虚拟机栈:
- 局部变量表 主要用于存储方法参数和定义在方法体内的局部变量,定义为一个数字数组(Slot[] 槽),容量大小编译期就确定了(编译期就知道有多少个变量)
- 操作数栈 由数组实现,主要用于保存计算结果的中间结果,同时作为计算过程中变量临时的存储空间;在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据pop/push,最大深度在编译期就确定了;执行引擎是基于栈的执行引擎。
动态链接 在Java源文件被编译到字节码文件的时候,所有的变量和方法的引用都做作为符号引用(#)被保存到字节码文件的常量池中。
动态链接的作用就是将这些符号引用转换为调用方法的直接引用。
方法返回地址 我的理解就是方法来时的路,存放调用该方法的PC寄存器的值,除此之外好像还有异常处理表。
- 附加信息 对程序调试提供支持的信息。
来个图理解理解一下动态链接
i++ 和 ++i问题
i++
int stack_top = local_variable[1];//把下标为1的局部变量表中的变量-->加载到操作数栈顶
local_variable[1] = local_variable[1] + 1;//下标为1的局部变量表的变量自增1
local_variable[1] = stack_top;//用操作数栈顶的值覆盖下标为1的局部变量表中的变量
++i
local_variable[1] = local_variable[1] + 1;//下标为1的局部变量自增1
int stack_top = local_variable[1];//把下标为1的局部变量加载到栈顶
local_variable[1] = stack_top;//用栈顶的值覆盖下标为1的局部变量
栈顶缓存技术是什么
为什么静态方法中不能调用this?(挖坑回答法qwq)
- 因为static修饰的内容随着类的加载而加载,且只加载一次。 this一般指类的对象,当static修饰的静态方法在类中加载的时候,this都是不存在的,访问一个内存中不存在的东西就会出错。
- 更具体点,从JVM的虚拟机栈的局部变量表中,只有类的构造器和实例方法的slot槽中才有该对象的引用this,而静态方法/代码块的局部变量表中没有this。
重点面试题:为什么需要使用常量池(方法区我也写了为什么需要 运行时常量池)
- 常量池的作用就是为了提供一些符号和常量,以便于指令的识别。同时能节约内存开销,提高效率。
- 具体谈:我们把java文件编译为class字节码文件,在JVM中,对于我们写的方法调用实际上是保存成虚拟机栈中的一个又一个的栈帧中,在栈帧中通过动态链接的方式去引用运行时常量池的方法和变量还有字段(如int void各种东西),从而能获取我们想要的方法和对应信息。
- 对于程序而言,如果没有常量池,就意味着我们需要在栈帧中去保存有关方法的所有信息,运行速度就会变慢,内存占用就会很大。
- 对于不同线程而言,也正式因为方法区运行时常量池的出现,我们能简单的通过引用去分别调用运行时常量池中反复出现的这些方法和字段。
静态链接与动态链接,虚方法和非虚方法,虚方法表,方法重写的本质(稍微过一下,一般问得少)
简单解释一下就是:
如果调用的方法在编译期可知,运行期保持不变,就称为静态链接(早期绑定)非虚方法
如果调用的方法在编译期无法确定, 只能在程序运行期才能将调用方法的符号引用转化为直接引用,称为动态链接(晚期绑定)虚方法
重写与虚方法表:
在OOP的编程中,会很频繁的使用到动态分配,如果每次在动态分配的时候都去在类的元数据搜素,就会很影响执行效率,为了提高性能,JVM采用在类的方法区创建一个虚方法表,使用索引代替查找。每个类都有一个虚方法表,表中放的是各个方法的实际入口。
举例栈溢出的情况
StackOverflowError 栈溢出 我们可以通过-Xss设置栈的内存大小。
OOM OutOfMemory 内存耗尽 栈空间可以是固定的,也可以是动态扩容的,要是扩容到整个内存空间都不足了,就OOM
调整栈大小,就能保证不出现溢出吗?
不能保证。
调整栈大小,只能让StackOverflowError出现的时间点晚一点。
分配的栈内存越大越好吗?
不是。
理论上来说,延缓了StackOverFlowError的发生时间,相对来说单位时间发生栈溢出的可能就会变小,但是避免不了出现。
栈的内存大了,而整个内存空间有限,就会挤占其他内存空间(譬如线程数),其他的内存空间就会变小。
垃圾回收是否会涉及虚拟机栈?
不会。
刚好回顾一下运行时数据区哪些会error那些有gc(有gc可以理解可调优)
可能出现Error | 有GC | |
---|---|---|
PC寄存器(程序计数器) | X | X |
java虚拟机栈 | O | X |
本地方法栈 | O | X |
堆 | O | O |
方法区 | O | O |
方法中定义的局部变量是否线程安全?
具体问题具体分析
- 如果方法变量是在内部产生,内部消亡,那么就是线程安全的。
- 不是内部产生的,或者是内部产生又返回到外面(发生逃逸),生命周期没有结束,那么也是不安全的
具体代码见链接
JVM内存之 堆相关问题
堆和栈的区别,堆的结构。
1、存储
栈:变量、对象的引用
堆:实例对象
2、速度
栈:存取速度快
堆:存取速度慢
3、线程访问
栈:每个线程都有一个栈区
堆:所有线程共享一个堆区
4、垃圾回收
栈:比较频繁
堆:不频繁
详情:
- 栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
- 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
Jvm内存为什么要分新生代,老年代,持久代;新生代中为什么要分Eden和survivor区
说白了就是在问为什么需要把Java堆分代——->优化GC性能
为什么两个survivor区?
- 有两个能解决碎片化问题,一个永远为空、一个非空永无碎片,用的是复制算法。
讲讲Jvm运行时数据库区什么时候进入老年代
- 默认15次 对象在Eden,出生并经过一次MinorGC[‘mainə]后仍然存活,年龄设置为1,移入Survivor区。对象在Survivor区每熬过一次GC,年龄就加1
- 当然也有特殊情况,如下
对象晋升规则
什么是TLAB?为什么要有TLAB?
堆空间常用指令
- 设置堆空间大小的参数
-Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
-X 是jvm的运行参数
ms 是memory start
-Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
默认堆空间的大小
初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理电脑内存大小 / 4手动设置:-Xms600m -Xmx600m
开发中建议将初始堆内存和最大的堆内存设置成相同的值。(频繁的扩容和释放会给系统造成压力)查看设置的参数:
方式一: jps / jstat -gc 进程id(命令行下) 一般埋个Thread.sleep
方式二: -XX:+PrintGCDetails
常用指令
-XX:NewRatio : 设置新生代与老年代的比例。默认值是2.
-XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
-XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)没用阿,关不掉,还是直接用上面的SurvivorRatio设置比例
-Xmn:设置新生代的空间的大小。 (一般不设置)
-XX:MaxTenuringThreshold=
打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否设置空间分配担保
堆是分配对象存储堆唯一选择吗?现在 看来是的,因为逃逸分析没有被应用(逃逸分析、栈上分配、标量替换)
标量替换:我们分析一个对象没有逃逸,要是可以把它拆为几个基本数据类型,我们就拆,这样就会分配到栈上了,而不是堆里。
关于空间分配担保(了解)
JVM内存之 方法区相关问题
栈、堆、方法区之间的关系
我建议直接以new对象开始举例。
搞清楚概念!运行时常量池 vs 常量池
运行时常量池: JVM中方法区包含了运行时常量池(不同版本包含不同、不仅仅是类,还有接口、枚举、注解)、常量、静态变量、即时编译器、类被哪个类加载器加载、编译后的代码缓存。这里就是具体内容了)
常量池:字节码文件,内部包含了常量池(包括各种字面量和对类型、域和方法的符号引用)。
- Class文件的常量池表:用于存放编译器生成的各种字面量与符号引用。
- 方法区运行时常量池:存放Class文件在类加载后的常量池表信息,如字面量,也包括解析期间才获得的方法和字段引用(动态性),还有就是,这里就不是像Class文件中简单的符号引用说明了,而是存放的真实地址。
为什么需要运行时常量池
最基本的一点就是,复用,复用,复用。
- 我们字节码文件中存的是方法啊、类型啊、修饰词的符号引用,就是一个简单描述(符号引用)。—>字节码文件常量池
- 当我们真真需要用这些东西的时候,是通过动态链接的形式,从字节码文件的符号引用去调用运行时常量池的具体信息(直接引用)。
- 除此之外,运行时常量池具备动态性。
- 常量池的作用就是为了提供一些符号和常量,以便于指令的识别。同时能节约内存开销,提高效率。
- 具体谈:我们把java文件编译为class字节码文件,在JVM中,对于我们写的方法调用实际上是保存成虚拟机栈中的一个又一个的栈帧中,在栈帧中通过动态链接的方式去引用运行时常量池的方法和变量还有字段(如int void各种东西),从而能获取我们想要的方法和对应信息。
- 对于程序而言,如果没有常量池,就意味着我们需要在栈帧中去保存有关方法的所有信息,运行速度就会变慢,内存占用就会很大。
- 对于不同线程而言,也正式因为方法区运行时常量池的出现,我们能简单的通过引用去分别调用运行时常量池中反复出现的这些方法和字段。
方法区在JDK6 7 8有什么变化(也算是堆空间的变化)
卧槽一定要注意啊别混淆概念了,运行时常量池还在方法区 只是字符串常量池放到了堆
永久代为什么要被元空间替换?(了解)
方法区的垃圾回收
比较难,但有的时候又有必要,譬如Sun公司曾经出现过若干个严重Bug就是低版本的HotSpot未对此区域回收导致内存泄漏。
主要回收常量池中的废弃常量、不再使用的类型。
所以string table移动到了堆:方便垃圾回收
Minor GC 、Major GC、Full GC
Major GC和Full GC需要重点关注,因为当执行GC进程时,STW暂停用户线程,而这俩暂停的时间是YGC的10倍左右
三个内存空间:新生代、老年代;方法区(永久代/元空间)
对象实例化内存布局
创建对象的步骤
怎么访问对象
- 句柄访问
- 直接指针(HotSpot采用)