小白学习JVM

JVM基础的学习笔记,本人小白,笔记可能有点乱,但是想学习的心情停不下来,接受建议批评,持续更新……


2020.04.01
jvm中,常见的两个问题
GC (Garbage Collection垃圾回收机制)
    GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。

OOM(out of memory内存泄漏)
    即内存泄露。一个程序中,已经不需要使用某个对象,但是因为仍然有引用指向它垃圾回收器就无法回收它,当该对象占用的内存无法被回收时,就容易造成内存泄露。

面试中常问 如何调优jvm参数、如何解决GC、OOM问题

计算机本身不识别高级语言(c、java等)  高级语言编译成汇编语言。

虚拟机(Virtual Machine)就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。

作用:Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么去操作数,怎么处理操作数,处理结果放在哪里。

特点:
    一次编译,到处运行
    自动内存管理
    自动垃圾回收功能

Java程序---->字节码文件 使用的编译器为编译器前端

类装载器子系统:将字节码文件加载成一个大的Class对象

执行引擎:解释器、JIT即时编译器(编译器后端)、垃圾回收器

Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。

两种架构之间的区别:
基于栈式架构的特点:
    设计和实现更简单,适用于资源受限的系统;
    避开了寄存器的分配难题:使用零地址指令方式分配;
    指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现;
    不需要硬件支持,可移植性更好,更好实现跨平台。
基于寄存器架构的特点:
    典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机;
    指令集架构则是完全依赖硬件,可移植性差;
    性能优秀和执行更高效;
    花费更少的指令去完成一项操作;
    在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。

小总结:
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

基于栈的特点:跨平台性、指令集小、指令多;执行性能比寄存器差些。

虚拟机的三个生命周期:
    启动:Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
    执行:一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序;程序开始执行时他才运行,程序结束时他就停止;执行一个所谓的Java程序的时候,真真正正在执行的一个叫做Java虚拟机的进程。
    退出:有如下几种情况:1.程序正常执行结束;2.程序在执行过程中遇到了异常或错误而异常终止;3.由于操作系统出现错误而导致Java虚拟机进程终止;4.某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作;5.除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。

2020.04.04
现在比较火的是HotSpot虚拟机


类的加载过程:加载---->验证---->准备---->解析---->初始化
其中,验证、准备、解析可以总称为链接
加载:
    1.通过一个类的全限定名获取定义此类的二进制字节流;
    2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
补充:加载.class文件的方式
    1.从本地系统中直接加载
    2.通过网络获取,典型场景:Web Applet
    3.从zip压缩包中读取,成为日后jar、war格式的基础
    4.运行时计算生成,使用最多的是:动态代理技术
    5.由其他文件生成,典型场景:JSP应用
    6.从转悠数据库中提取.class文件,比较少见
    7.从加密文件中获取,典型的防Class文件被反编译的保护措施

链接:

    验证:
    1.目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
    2.主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

    准备:
    1.为类变量分配内存并且设置该类变量的默认初始值,即零值。
    2.这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。
    3.这里不会为实例变量分配初始化,类变量回分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
    
    解析:
    1.将常量池内的符号引用转换为直接引用的过程。
    2.事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
    3.符号引用就是一组符号来描述所引用的目标,符号引用的字面量行驶明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个简介定位的目标的句柄。
    4.解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对用常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

初始化:
    1.初始化阶段就是执行类构造器方法<clinit>()的过程。
    2.此方法不需定义,是javac编译器自动手机类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
    3.构造器方法中指令按语句在源文件中出现的顺序执行。
    4.<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())。
    5.若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
    6.虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。

 2020.04.05
类的加载器
    虚拟机自带的加载器
    启动类加载器(引导类加载器,Bootstrap ClassLoader)
        1.这个类加载使用C/C++语言实现的,嵌套在JVM内部;
        2.它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类;
        3.并不继承自java.lang.ClassLoader,没有父加载器;    
        4.加载扩展类和应用程序类加载器,并指定为他们的父类加载器;
        5.出于安全考虑,Bootstrap启动类加载器只加载报名为java、javax、sun等开头的类。

    扩展类加载器(Extension ClassLoader)
        1.Java语言编写,由sun.misc.Launcher$ExtClassLoader实现;
        2.派生于ClassLoader类
        3.父类加载器为启动类加载器
        4.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

用户自定义类加载器
    在Java的日常应用程序开发中,类的加载几乎是由上述3中类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类的加载器:1.隔离加载类;2.修改类加载的方式;3.扩展加载源;4.防止源码泄露。

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包含启动类加载器)


双亲委派机制
Java虚拟机堆class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的时双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理:
    1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
    2.如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
    3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

优势:1.避免类的重复加载;2.保护程序安全,防止核心API被随意篡改。

沙箱安全机制
    自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

在JVM中表示两个Class对象是否为同一个类存在两个必要条件:
    1.类的完整类名必须一致,包括包名;
    2.加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

主要的后台系统线程在Hotspot JVM里主要是以下几个:
    虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要的JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括“stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
    周期任务线程:这种线程是时间周期时间的体现(比如中断),他们一般用于周期性操作的调度执行。
    GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
    编译线程:这种线程在运行时会将字节码编译成到本地代码。
    信号调度线程:这种线程接受信号并发送给JVM,在它内部通过调用适当的方法进行处理。

PC寄存器
作用:PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。

它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)。

PC寄存器两个常见的面试问题

使用PC寄存器存储字节码指令地址有什么用呢?
    因为CPU需要不停地切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。

为什么使用PC寄存器记录当前线程的执行地址呢?
    JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

虚拟机栈

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。是线程私有的。

生命周期和线程一致。

作用:主管Java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址)、部分结果,并参与方法的调用和返回。局部变量 vs 成员变量;基本数据类型变量 vs 引用数据类型变量

栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序的计数器。
JVM直接对Java栈的操作只有两个:
    每个方法执行,伴随着进栈(入栈、压栈)
    执行结束后的出栈工作
对于栈来说不存在垃圾回收问题
    
面试题:开中遇到的异常有哪些?
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
    如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
    如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

例如可以方法内调用自己,递归方法,类似死循环,可以产生StackOverflowError异常。

栈中存储什么?
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈运行原理
    JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
    在一条活动线程中,一个时间点上,指挥有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被成为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Mothod),定义这个方法的类就是当前类(Current Class)。
    执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
    如果在该方法钟调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

不同线程中所包含的栈帧是不允许存在互相引用的  ,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧回传会此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法由两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

每个栈帧钟存储着:
    局部变量表(Local Variables)
    操作数栈(Operand Stack)(或表达式栈)
    动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
    方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
    一些附加信息

局部变量表(Local Variables)也被称之为局部变量数组或本地变量表
    定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
    由于局部变量表是剑灵在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
    局部变量表所需的容量大小是在编译器确定下来的,并保存在方法的Code属性的maximum local variables数据项钟。在方法运行期间是不会改变局部变量表的大小的。

方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对于一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
如果需要访问的局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放index为0的slot处,其余的参数按照参数表顺序继续排列。

在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
    某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再将结果压入栈。
    比如:执行复制、交换、求和等操作。

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈时空的。

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。

栈中的任何一个元素都是可以任意的Java数据类型。
    32bit的类型占用一个栈单位深度
    64bit的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

面试题:i++和++i的区别


动态链接(或指向运行时常量池的方法引用)
每一个栈帧内部包含一个指向运行时常量池中该栈帧所述方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokeddynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了这些符号引用转换为调用方法的直接引用。

方法的调用
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定时一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

早期绑定
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

方法的调用:虚方法与非虚方法
非虚方法:
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法成为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
其他方法成为虚方法。

多态的使用前提:1.类的继承关系;2.方法的重写

虚拟机中提供了以下几条方法调用指令
普通调用指令
    1.invokestatic:调用静态方法,解析阶段确定唯一方法版本
    2.invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
    3.invokevirtual:调用所有虚方法
    4.invokeinterface:调用接口方法

动态调用指令
    5.invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法成为非虚方法,其余的(final修饰的除外)成为虚方法。

动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之就是动态类型语言。

直白,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

方法返回地址(return address)
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

栈的相关面试题
举例栈溢出的情况?(StackOverflowError)
    通过-Xss设置栈的大小;OOM

调整栈大小,就能保证不出现溢出吗?
    不能保证,

分配的栈内存越大越好吗?
    不是,只是栈溢出的概率变小了,不能避免出现。

垃圾回收是否会涉及到虚拟机栈?
    不会的!

方法中定义的局部变量是否线程安全?
    

为什么要使用Native Method
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

与Java环境外交互:
优势Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
    如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
    如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常
本地方法是使用C语言实现的。
它的具体做法时Native Method Stack中等级native方法,在Execution Engine执行时加载本地方法库。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值