说到JVM,很多工作多年的老铁,可能就有点发憷了,因为搬砖多年,一直使用java这个工具,对于JVM没有了解过,有句话面试造航母,上班拧螺丝,要啥自行车啊,知道如何搬砖就可以了,为啥要懂这么多,如果你有很强的商业头脑,不需要了解太多深入的东西,只要完成业务功能就可以了,如果你口才也不行,只有一个编程的大脑,老铁沉下心咱们一起了解下,你平常拧螺丝的扳手的结构把,这个真心有用。因为它可以让你走的更远,挣的更多!
JVM
JVM一些概念
- 什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
- JVM和普通虚拟机
- 1.JVM是Java Virtual Machine(Java虚拟机),执行java字节码的环境,一个程序自己独立的环境,必须要包含堆栈,寄存器,字节码指令。Java、android、Scala、Groovy等语言都是可以在JVM上运行的,它们都遵照JVM的指令集的,也就是class规范。
- 2.VMWare,VisualBox它是一个完完整整可以提供一个虚拟主机的PC,这种虚拟机上边必须安装操作系统的,它是模拟物理主机的CPU的指令集。
- JVM、JDK、JRE的关系
JVM是基本的单位,JRE是java的运行是环境,JDK是开发工具集
JVM 小于 JRE 小于 JDK
1.JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
2.JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
3.JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
- JVM产品有哪些
HotSpot,Jrockit,J9
- 为什么出现JVM
- 1.C,C++是基于os架构,CPU架构。64位的版本在32位无法运行的。性能非常高,编写底层实现。
- 2.JAVA可以一次编写到处运行,移植性好。
JVM运行流程
1.java编译成class文件,字节码文件,不是纯粹的二进制字节码文件,在操作系统上是不可以直接运行的,而纯粹的二进制文件是可以直接运行的。
2.JVM中有个执行引擎,当JVM编译好后,转换成对应到不同操作系统的指令。发送到操作系统上去。所有操作系统都可以识别自己的指令。
3.window上是dll程序,linux是.o的动态链接库。
JVM结构
1.类加载器
2.执行引擎
3.运行时数据区
4.本地接口
- ClassLoader类加载器
JVM加载的是.class文件。其实,类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
同时,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器会在程序首次主动使用该类时会生成错误报告(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
- 双亲委派模型
- 类加载过程
- 当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
- 双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
- 使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
- 使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
- 双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
- 加载
- 简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对戏那个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
- 链接
- 链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
- (1)验证
- 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
- 格式验证:验证是否符合class文件规范
- 语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
- 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
- (2)准备
- 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
- 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
- (3)解析
- 将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
- 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)
- 初始化
- 将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
- 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
- 如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。
- JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
JVM运行时数据区
JVM在执行Java代码时都会把内存分为几个部分,即数据区来使用,这些区域都拥有自己的用途,并随着JVM进程的启动或者用户线程的启动和结束建立和销毁。
- Program Counter Register
作用
当前线程执行的字节码的行号指示器,通过改变此指示器来选取下一个需要执行的字节码指令
特征
1.在线程创建时创建
2.每个线程拥有一个
3.指向下一条指令的地址
- Method Area(Non-Heap)
线程共享
存储
1.类信息
2.常量
3.静态变量
4.方法字节码
- VM Stack / Native Method Stack
线程私有
方法在执行时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
局部变量表所需的内存空间在编译期间完成分配,而且分配多大的局部变量空间是完全确定的,在方法运行期间不会改变其大小
出栈后空间释放
- Heap
线程共享
存储对象或数组
Heap划分
- JMM
线程工作内存,主内存
JMM模型
PS:梳理了JVM的概念,以及JVM和JDK的关系,为什么用JVM,JVM的认识,JVM的运行流程,class文件被JVM执行引擎,加载到数据区中,编译到操作系统识别的指令,针对操作系统不同的指令。JVM针对不同操作系统进行了class只有一份。实现跨平台。JVM的运行数据区分为线程共享的数据区(方法,堆)和线程独立的数据区(栈,程序计数器)。JVM真的基本上都是文字的东西。可能比较难理解,有工作经验稍微好理解些。确实太底层了。
GC这块,当java才入门的时候,老师说java不像c++,c语言需要对内存进行管理,java有垃圾回收机制,会自动进行回收,是实际的生产中也没关注过这些,现在回过头好好了解下,发现里面很有回收很多的机制。
GC
GC(Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
- 对新生代的对象的收集称为minor GC;
- 对旧生代的对象的收集称为Full GC;
- 程序中主动调用System.gc()强制执行的GC为Full GC。
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
- 强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
- 软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
- 弱引用:在GC时一定会被GC回收
- 虚引用:由于虚引用只是用来得知对象是否被GC
垃圾收集算法
- 标记-清除算法
标记阶段
标记存活对象
清除阶段
统一回收所有未标记的对象
缺点
会产生内存碎片,如果空间内存碎片太多,当程序产生大对象无法在堆中找到连续空间大小存放的时候,会强制发生GC
- 复制算法
原理
内存一分为二,每次只使用其中一块,当一块内存没有连续空间存储对象的时候,会把存活下来的对象复制到另外一块内存中,然后一次性清除之前的哪块空间
优缺点
- 没有内存碎片问题
- 代价就是讲内存减少了一半,空间利用率不高
- 不适用于存活对象较多的场景,比如老年代
- 而实际上我们并不需要按照1:1的比例来划分,因为大部分对象从创建到结束这个生命周期很短
- HotSpot虚拟机默认Eden:Survivor=8:1
- 标记-整理算法
原理
- 标记存活对象,然后把存活对象向一端移动
- 清理掉存活对象这端以外的所有空间
优缺点
- 适合用于存活对象较多的场合,如老年代
- 解决了空间碎片和效率问题:将所有的存活对象压缩到内存的一端,然后清理边界外所有的空间
- 分代收集算法
分代思想
- 堆划分为新生代和老年代
- 新生代中,能够存活的对象很少,可以使用复制算法
- 老年代中,对象存活率高,而且没有额外的空间用来做老年代的担保,可以使用标记清除或者标记整理算法
垃圾收集器-GC
- Serial 新生代串行收集器 新(复制算法),老(标记整理)
- ParNew 新生代并行收集器
- Parallel Scavenge 新生代并行收集器
- 目标:尽可能缩GC时用户线程的停顿时间
- 在注重吞吐量或CPU资源敏感的场合,可以优先考虑Parallel
- Scavenge收集器 + Parallel Old收集器
- Serial Old 老年代串行收集器
- Parallel Old 老年代并行收集器
- CMS 真正意义上的并发收集器(老年代收集器)
- 目标:最短的GC停顿时间
- G1
Trace跟踪参数
-verbose:gc
打印GC日志信息
-XX:+PrintGCDetails
打印GC日志信息
-Xloggc:d:/gc.log
GC日志目录
-XX:+PrintHeapAtGC
每次一次GC后,都打印堆信息
-XX:+TraceClassLoading
类加载信息
-XX:+PrintClassHistogram
配置了该参数后,在程序执行过程中,按下Ctrl+Break后,就可以打印类的信息
PS:其实说这么多啊,最重要的还是使用打印的方式看看那个发生内存溢出的情况与数据操作有很大的关系,有助于优化程序。
本次说说Class加载器,类的初始化,JDK自带故障排查工具介绍。
Class加载器
JVM结束生命周期:
1.System.exit() 直接就退出了
2.正常运行完
3.异常中止
4.手动kill进程
- 加载
- 1.取得类的二进制字节流,通过类的全限定名称(包名+类名),类加载器,加载到运行时数据区。
- 2.把二进制字节流中静态存储结果转化为方法区数据结构。
- 3.在内存中生成代表这个类的java.lang,Class对象,这里是放在堆中。
- 链接
验证
验证类的格式是否正确
(一)文件格式的验证,class
(二)元数据验证,是否有父类,有父类先加载父类,一般的类都有父类,object
(三)字节码验证,数据流是不是合法的符合逻辑的。方法体进行检测
(四)符号引用验证,访问的时候判断是否有权限来进行引用
准备
为类的静态变量(static修饰)分配内存(方法区),并将启初始化改成默认值
解析
常量池中的引用替换为直接引用
- 初始化
1.如果这个类还没有被加载和链接,那就先加载和链接
2.如果类存在直接的父类,先初始化直接父类
3.如果类存在初始化语句,那就依次初始化语句
类的初始化
- 主动使用
- new,创建类的实例对象
- 反射
- 调用类的静态方法
- 初始化类的子类,此时需要先要初始化父类
- 访问类中的静态变量或者给静态变量赋值
- Jvm启动的时候的启动类
JDK自带故障排查工具介绍
PS:了解好类的初始化,对于开发尤为重要,对于自带的故障排查工具,对于内存分析的时候很有用。