深入理解JVM-内存篇

一,JVM与Java内存体系结构

1.java相对于c++的区别

动态内存分配,自动垃圾回收

2.java是跨平台的语言,jvm是跨语言的平台

Java是世界上最好的语言,jvm是世界上最好的虚拟机。

特点:一次编译,到处运行。

只要语言编译后的字节码文件符合JVM的要求,就可以再JVM上运行

3.虚拟机

一台虚拟的计算机,一款软件用来执行一系列的虚拟计算机指令

分类

系统虚拟机 程序虚拟机

java虚拟机属于程序虚拟机,专门为了执行单个计算机程序而设计,在java虚拟机中执行的指令我们称为Java字节码指令。

无论是系统虚拟机还是程序虚拟机,在上面运行的软件都受限于虚拟机提供的资源中。

作用

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

位置

用户 字节码文件 jvm 操作系统 硬件

jvm是运行在操作系统之上的,它与硬件没有直接的交互。

在这里插入图片描述

4.java虚拟机的整体结构

在这里插入图片描述

HotSpot是目前市面上高性能的虚拟机代表作之一。

它采用解释器与即时编译器共存的架构。

1.类装载器子系统装载字节码文件进入到运行时数据区,
2.在内存中生成大的Class实例。
3.运行时数据区包括方法区和堆(多线程共享),java栈,本地方法栈,程序计数器PC
4.执行引擎包括解释器,JIT即时编译器(热点代码),GC垃圾回收器

5.java代码执行流程

java程序编译成字节码文件,字节码执行后运行在不同的平台上。

.java文件->java编译器(词法分析-语法分析-语法、抽象语法树-语义分析-注解抽象语法树-字节码生成器)->.class文件->java虚拟机(类加载器-字节码校验器-翻译字节码(解析执行),JIT编译器(编译执行))-操作系统
在这里插入图片描述

6.jvm结构模型

1.java编译器输入的指令流基本上是一种基于栈的指令集架构,另一种指令集架构则是基于寄存器的指令集架构。
2.这两种架构之间的区别
	1.基于栈的指令集架构
        1.设计和实现更简单,适用于资源受限的系统
        2.避开了寄存器的分配难题,使用0地址指令方式分配
        3.指令流中的大部分指令是0地址指令,其执行过程依赖于操作栈,指令集更小
        4.不需要硬件支持,可移植性更好,更好实现跨平台
	2.基于寄存器的指令集架构
        1.典型的应用是x86的二进制指令集;比如传统的PC以及安卓的Davlik虚拟机。
        2.指令级架构则完全依赖于硬件,可移植性差
        3.性能优秀和执行更高效
        4.花费更少的指令去完成一项操作
        5.在大部分情况下,基于寄存器的指令集架构往往都是一地址指令,二地址指令和三地址指令为主,而基于栈式的指令集架构是以0地址指令为主。
3.0地址指令是只操作栈顶元素,所以舍弃了地址,只保留一个操作数,但是一进制指令是一个地址对应一个操作数。
4.总结:由于java语言跨平台的设计,所以java的指令都是根据栈来设计的,不同平台的CPU架构不同,所以不能设计为基于寄存器的,
	优点是跨平台性,指令集小,编译容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
5.时至今日,尽管嵌入式平台已经不是java程序的主流运行平台,那么为什么不讲java的指令集架构更换为基于寄存器的?
	因为没有必要,基于栈的指令级架构同样适用于非资源受限的操作系统。

7.JVM的生命周期

1.启动

java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。

2.执行

一个运行中的java虚拟机有一个任务:执行java程序。

程序开始执行时他才运行,程序结束时他就停止。

执行所谓的java程序的时候,真真正正执行的是一个叫做java虚拟机的进程。

3.销毁

程序正常执行结束

程序执行过程中发生异常或者错误

操作系统出现错误导致虚拟机终止

调用System或Runtime类的exit方法或者runtime类里面的halt方法,并且java安全管理器也允许这次退出

JNI API加载或者卸载java虚拟机的时候

4.类的加载过程

父类的静态代码块和静态属性

子类的静态代码块和静态属性

父类的普通代码块和普通属性

父类的构造器

子类的普通代码块和普通属性

子类的构造器

8.HotSpot虚拟机

jdk3开始 HotSpot虚拟机成为默认的虚拟机

特点:热点探测技术

通过计数器找到最具有编译价值的代码,出发即时编译或者栈上替换

通过编译器与解释器协同工作,在最优化的响应时间与最佳执行性能中取得平衡

对象不一定在堆中创建,也可以栈上分配,便于垃圾回收

二,类装载器子系统

1.内存结构概述

类加载器系统加载(加载,链接,初始化)字节码文件到运行时数据区

执行引擎(解释器,JIT即时编译器,GC垃圾回收器)根据字节码指令执行程序,本地方法接口

在这里插入图片描述

2.类加载器和类的加载过程

类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。

类加载器只负责class文件的加载,至于他是否可以运行,由执行引擎决定。

加载类的信息存放在元空间,除了类的信息,元空间还有运行时常量池(常量池加载到内存中就叫做运行时常量池)。

可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

过程:加载->链接(验证,准备,解析)->初始化

ClassLoader:class文件加载到jvm,过程中需要一个运输工具(类加载器)。

3.类的加载详细过程

1.加载

通过全限定类名加载一个类的二进制字节流,将静态结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象

2.链接

1.验证:确保class文件内容不会危害到当前虚拟机

2.准备:为类变量分配内存并设置初始值,不会为实例变量分配空间初始化,类变量分配在方法区,实例变量分配在堆空间。

3.解析:将常量池的符号引用转换为直接引用

3.初始化

执行类构造器方法(完成静态属性和静态代码块变量的赋值操作)的过程,此方法不需要定义,是javac完成的。

clinit()不同于类的构造器,团组织会加载一次。若该类具有父类,JVM会保证子类的clinit()执行前,父类的clinit()已经执行完毕。

虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。 任何一个类声明以后,内部至少存在一个类的构造器

4.类加载器分类

引导类加载器

启动类加载器 BootStrap ClassLoader

加载java核心类库,只加载java javax sun开头的类

系统类加载器

extends ClassLoader java ClassLoader.getSystemClassLoader();

加载用户自定义类,父类加载器为扩展类加载器

拓展类加载器

extends ClassLoader java SystemClassLoader.getParent();

用户自定义类加载器

extends ClassLoader

1.为什么要用户自定义类加载器?

隔离加载类
修改类加载的方式
扩展加载源
防止源码泄露

2.用户自定义类加载器步骤?

继承ClassLoader
jdk1.2之前,继承ClassLoader重写loadClass().jdk1.2之后建议重写findClass()
也可以继承URLClassLoader,避免了自己编写findClass()以及获取自己码流的方式。

3.ClassLoader

它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
getParent() 返回父类加载器
loadClass(String name) 返回Class实例
findClass(String name) 返回Class实例
defineClass(String name,byte[] b ,int off,int len) 把字节数组b中的内容转换为一个java类,返回结果为java.lang.Class类的实例

5.双亲委派机制

java虚拟机对class文件采用的是按需加载,而且加载某个类的class文件时,java虚拟机采用的是双亲委派机制,就是把请求交由父类处理,它是一种任务委派模式

工作原理

如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器。

如果父加载器可以完成类加载任务,就成功返回,倘若父加载器无法完成此类加载任务,子加载器才会尝试自己去加载。

举例

调用JDBC接口,接口是引导类加载器加载的,但是实现类是系统类加载器加载的。

优点

避免类的重复加载

保护程序安全,避免核心API被篡改

6.沙箱安全机制

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

7.其他

判断两个Class对象是否为同一个类的必要条件

全限定类名

加载这个类的类加载器实例对象必须相同

(在JVM中,即使这两个类对象来源于同一个Class文件,被同一个虚拟机所加载,只要加载他们的ClassLoader实例对象不同,那么这两个类对象也是不相等的)

类的主动使用和被动使用

主动使用

创建类的实例

访问某个类或接口的静态变量,或者对该静态变量赋值

调用类的静态方法

反射

初始化一个类的子类

java虚拟机启动时候被表明为启动类的类

java7提供的动态语言支持

某些类clinit方法的执行

被动使用

除了主动使用的,都是被动使用

三,运行时数据区

1.内部结构

JVM内存布局规定了java在运行过程中内存申请,分配,管理的策略,保证了JVM高效稳定的运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
在这里插入图片描述

2.结构

线程私有的:java虚拟机栈 本地方法栈 程序计数器

线程间共享的:堆(0.95的垃圾回收) 方法区(0.05的垃圾回收)

3.线程

线程是一个程序里面的运行单元,JVM允许一个应用有多个线程并行的执行。

在HotSpot虚拟机,每个线程都与操作系统的本地线程直接映射。

当一个Java线程准备好执行以后,此时操作系统的本地线程也同时创建。java线程执行终止后,本地线程也会回收。

操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用Java线程中run();

当run正常执行完,或者出现异常后有相应的异常处理机制,也都算正常执行完,这时候java线程和本地线程都会回收,资源得到释放。

如果run方法执行中,出现一些为捕获的异常,这时就会导致java线程被终止,java线程终止以后,本地线程来决定jvm到底要不要终止(jvm要不要终止取决于当前线程是不是最后一个用户线程,如果剩下的都是守护线程,虚拟机就会退出了)

四,程序计数器-PC

JVM中的程序计数器,命名源于CPU的寄存器,寄存器存储相关的现场信息,CPU只有把数据装载到寄存器才能运行,这里,也不是指广义上的物理寄存器,或许将其翻译为PC计数器,会更贴切,并且也不容易引起一些不必要的误会,JVM中的PC寄存器是对物理PC寄存器的一种抽象。

程序计数器是用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。

运行速度最快的区域。线程私有的,生命周期与线程的生命周期一致。

任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,或者,如果在执行native方法,则是来指定值。

循环,分支跳转,异常处理,线程恢复都是依赖于程序计数器

字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

JVM规范中唯一一个没有内存溢出错误的区域

1.使用PC寄存器存储字节码指令地址有什么作用?

因为cpu需要不停地切换各个线程,这时候切换回来以后,就得知道接着从哪里继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

2.pc寄存器为什么要设置成线程私有的?

每一个线程都需要一个PC线程计数器来记录执行到了哪条字节码指令。

cpu时间片

CPU分配给各个程序的时间,每个线程被分配一个时间段,称作他的时间片。微观上,由于只有一个cpu,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

并行vs串行

并行:几个线程同时执行

串行:几个线程交替执行

并发:垃圾回收线程和用户线程同时执行

五,虚拟机栈

由于跨平台性的设计,java的指令集架构是基于栈的指令集架构。不同平台cpu架构不同,所以不能设计为基于寄存器的指令集架构。

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

1.内存中的栈与堆

栈是运行时单位,堆是存储的单位。

栈解决的是程序的运行问题,既程序如何执行,或者说如何处理数据,堆解决的是数据存储的问题,既数据怎么放,放在哪里。

2.基本内容

每个线程创建的时候都会创建一个java虚拟机栈,其内部保存一个个的栈帧,对应着一次次方法的调用(一个栈帧对应着一个java方法)。

栈顶的方法被称为当前方法。栈是线程私有的,生命周期和线程一致。

作用:主管java程序的运行,他保存方法的局部变量(8种数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。

3.特点

栈是一种快速有效的分配存储方式,访问速度仅仅次于程序计数器

jvm堆栈的直接操作只有两个 :方法执行,入栈;方法结束,出栈。

对于栈来说,不存在垃圾回收问题。

4.栈的常见异常和参数设置

jvm规范允许栈的大小是动态的或者是固定不变的。

如果采用固定大小的java栈,那每一个线程的虚拟机容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过java虚拟机允许的最大容量,jvm会抛出StackOverFlowError(递归没有出口)。

如果java虚拟机栈可以动态扩容,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程是没有足够的内存去创建对应的虚拟机栈,那ava虚拟机会抛出oom。

设置栈的大小

-Xss 设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达参数。

5.栈的存储结构和运行原理

每个线程都有自己的栈,栈中的数据都是以栈帧的形式存在。

在这个线程上正在执行的每个方法都各自对应一个栈帧。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中各种数据信息。

1.栈的运行原理

一个线程中,一个时间点上,只会有一个活动的栈帧,既只有当前正在执行的方法的栈帧是有效的,这个栈帧被称为当前栈帧。与这个栈帧对应的方法是当前方法,定义当前方法的类叫做当前类。

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

如果在该方法中调用了其他方法,对应的新的栈帧被创建出来,放在栈顶,成为新的当前栈帧。

不同线程中所包含的栈帧是不允许相互引用的,既不可能在一个栈帧之中引用另外一个线程的栈帧。

如果当前方法调用了其他方法,方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧成为当前栈帧。

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管使用哪种方式,栈帧都会弹出。

2.栈帧的内部结构

每个栈帧中存储着 局部变量表 操作数栈 动态链接(指向运行时常量池的方法引用) 方法返回地址 一些附加信息

3.局部变量表

本地变量表:定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各种基本数据类型,对象引用,以及返回地址类型。

由于局部变量表是建立在线程的栈上的,是线程私有数据,因此不存在数据安全问题。

局部变量表所需容量大小是在编译器确定下来的,并保存在方法的Code属性的maximun local variables数据项,方法运行期间变量表的大小是不会改变的。

参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。

局部变量表,最基本的存储单元是变量槽。

局部变量表中存放编译器可知的各种基本数据类型,引用类型,returnAddress类型。

方法嵌套的次数由栈的大小决定,局部变量表中的变量只在当前方法调用中有效,局部变量表随着栈帧销毁。

4.关于slot的理解

参数值的存放总是在局部变量表的index0开始,到数组长度-1的索引结束。

局部变量表,最基本的存储单元是变量槽 slot

局部变量表中存放着编译期可知的各种基本数据类型,引用类型,returnAddress的变量。

在局部变量表里,32位以内的类型只占用一个sort,64占用两个sort。

jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引就可以成功访问到局部变量表中指定的局部变量值。

当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。

如果需要访问一个局部变量表中64bit的局部变量值时,只需要使用一个索引即可。

如果当前帧是由构造方法或者实例方法(非static)创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

为什么静态方法不能使用this?

静态方法的局部变量表没有this

slot的重复利用:栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么再其作用域之后声明的新的局部变量就很有可能会复用过期的局部变量的槽位,从而达到节省资源的目的。

6.静态变量与局部变量的对比

变量按照数据类型:基本数据类型和一用数据类型

变量按照类中声明位置:成员变量(类变量 static ,局部变量) 局部变量

成员变量 在使用前都经过默认赋值,int显式赋值。

实例变量 随着对象创建会在堆空间中分配实例变量空间,并进行默认赋值。

局部变量显式赋值,否则没法使用。

在栈帧中,与性能调优最为密切的就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递。

在局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接引用或间接引用的对象都不会被回收。

7.操作数栈

栈的结构可以使数组或者单向链表,操作数栈时数组实现的。

操作数栈,在方法执行过程中,根据字节码指令,往占中写入数据或者提取数据。

某些字节码指令将数值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。

执行复制,操作,求和。

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

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

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

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

栈中的任何元素都是可以任意的java数据类型。

java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

涉及操作数栈的字节码指令执行分析

在这里插入图片描述

8.栈顶缓存技术

由于操作数是存储在内存中,因此频繁的执行内存读写必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。

9.动态链接

指向运行时常量池的方法引用。

每一个栈帧的内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。

在java源文件在编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

在这里插入图片描述

为什么需要常量池?为了提供一些符号和常量,便于指令识别。

10.方法的调用

在jvm中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关。

动态链接与静态链接

静态链接:当一个字节码文件被装载到JVM内部时,如果被调用的 目标方法在编译期可知 ,且运行期保持不变时,这种情况下调用方法的符号引用转换为直接引用的过程称之为静态链接。

动态链接:如果 被调用的方法在编译期无法被确定下来 ,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

对应方法的绑定机制

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

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

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

Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual 来显示定义)。

如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

4种方法调用指令区分非虚方法与虚方法

虚方法与非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本再运行时是不可变的。这样的方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。
(子类对象的多态性的使用前提:①类的继承关系 ②方法的重写)

动态类型语言和静态类型语言

1、动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
2、静态类型语言是判断变量自身的类型信息
3、动态类型语言是判断变量值的类型信息。变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

方法重写的本质与虚方法表的使用

方法重写的本质

找到操作数栈顶的第一个元素所执行的对象的实际类型,记做C。

如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过则返回java.lang.IllegalAccessError。

否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。

如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

java.lang.IllegalAccessError

程序视图访问或者修改一个属性或调用一个方法,这个属性或者方法,你没有权限访问,一般的,这个会引起编译器异常,这个错误 如果发生在运行时,就说明一个类发生了不兼容的改变。

11.方法返回地址

存放着调用该方法的pc寄存器的值

一个方法结束,有两种方式:

1.正常执行完成

2.出现未处理的异常,非正常退出

无论通过哪种方式退出,在方法退出后都要返回到该方法被调用的位置,方法正常退出时,调用者的PC寄存器的值作为返回地址;即调用该方法的指令的下一条指令的地址,而通过异常退出的,返回的地址需要通过异常表来确定,栈帧中一般不会保存这部分信息。

当一个方法开始执行后,只有两种方式可以退出方法

1.当执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常完成出口。
2.一个方法在正常执行完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际参数类型而定。
3.在字节码指令中,返回指令包含ireturn,lreturn,freturn,dreturn,另外还有一个return指令供声明为void的方法,实例初始化方法,类和接口初始化方法使用。
4.在方法执行过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。
5.方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码

总结

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

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

12.栈帧中的一些附加信息

栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。

13.虚拟机的五道面试题

1.栈溢出的情况?

递归没有出口,方法无限循环调用

主要从两个方面考虑,申请固定内存但是内存不足,动态扩容内存不足。

2.调整栈的大小,就能保证栈不溢出嘛?

不是,比如递归没有出口

3.分配的栈内存越大越好么?

不是

4.垃圾回收是否涉及到虚拟机栈?

不是,但是局部变量表的引用属于垃圾回收的根节点。

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

不一定,只有内部生内部死的变量才是线程安全的,不是内部产生的(方法参数),内部产生又返回到外面的(返回值),是线程不安全的。

六,本地方法接口和本地方法栈

在这里插入图片描述

什么是本地方法?

1、简单讲:一个Native Method 就是一个Java调用非Java代码的接口。一个 Native Method是这样的一个Java方法:该方法的实现由非Java语言实现,比如 C 。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,可以用 extern “C” 告知C++ 编译器去调用一个C 的函数。
2、“A native method is a Java method whose implementation is provided by non-java code.”
3、在定义一个native method 时,并不提供实现体(有些像定义一个Java interface ),因为其实现体是由非Java语言在外面实现的。
4、本地接口的作用是融合不同的编程语言为Java 所用,它的初衷是融合 C/C++程序。
5、标识符native可以与所有其他的Java标识符连用(除了abstract)。

为什么使用本地方法?

1.java虽然使用方便,但是有一些层次的任务用java实现并不容易,或者我们对程序的效率很在意时,问题会出现。
2.现状
目前该方法的使用越来越少了,除非是与硬件有关的应用,比如通过java程序驱动打印机或者java系统管理生产设备,在企业级应用中已经少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等。

本地方法栈的理解

java虚拟机栈用于管理java方法的使用,而本地方法栈用于管理本地方法的调用,本地方法栈也是线程私有的,它允许被实现成固定或者可动态扩展的内存大小(在内存溢出方面是相通的)。本地方法主要是使用c语言来实现的,它的具体做法是在本地方法栈中登记本地方法,在执行引擎执行的时候加载本地方法库。

本地方法栈,本地接口和本地方法库之间的联系

在这里插入图片描述

当某个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界,他和虚拟机拥有同样的权限,本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。他甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存。

并不是所有的jvm都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法,也可以无需实现本地方法栈。

在hotspot虚拟机中直接将本地方法栈和java虚拟机栈合二为一。

七,堆

1.堆的核心概念

一个jvm实例只存在一个堆空间,他是java内存管理的核心区域。他在jvm启动时创建,空间大小也就确定了,是jvm管理的最大一块内存空间,堆内存的大小是可以调节的,jvm规范规定,堆可以处于物理上内存不连续的空间,但是在逻辑上他应该被视为连续的。所有线程共享java堆,在这里还可以划分线程私有的缓冲区(TLAB)。

jvm规范中对java堆的描述是:几乎所有的对象实例以及数组都应当在运行时分配在堆上。数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。在方法结束后,堆中对象不会马上移除,仅仅在垃圾收集的时候才会被移除。堆是GC垃圾回收的重点区域。

在这里插入图片描述

2.堆的内存细分

在这里插入图片描述

现代垃圾收集器大部分基于分代收集理论,堆空间细分为:新生代(伊甸园区,幸存者0和幸存者1),老年代,永久代(jdk8改名叫元空间)

3.设置堆内存大小与OOM

堆空间的大小设置

java堆用于存储java实例对象,那么堆的大小在jvm启动的时候就已经设定好了,可以通过-Xms和-Xmx来设置。

 -Xms用于表示堆的起始内存
 -Xmx用于表示堆区的最大内存
 -X是jvm的运行参数
 ms是memory start

一旦堆区中的内存大小超过-Xms所指定的最大内存时,将会抛出OutOfMemoryError异常。通常将起始内存和最大内存设置相同的值,其目的是为了能够在垃圾回收机制清理完堆空间后不需要重新分隔计算堆空间的大小,从而提高性能。

默认情况下,初始内存:物理内存/64,最大内存大小物理内存/4,查看设置的参数JPS

//返回java虚拟机中的堆内存总量
long initMemory=Runtime.getRuntime().totalMemory()/1024/1024;
//返回java虚拟机试图使用的最大堆内存量
long maxMemory=Runtime.getRuntime().maxMemory()/1024/1024;
System.out.println("-Xms:"+initMemory);
System.out.println("-Xmx:"+maxMemory);

OutOfMemoryError

集合或者数组创建过大,导致堆空间超出内存。

3.年轻代和老年代

存储在jvm中的java对象可以被划分为两类

生命周期比较短的和生命周期比较长的。

java堆区进一步细分的话,可以划分为年轻代和老年代(1:2),其中年轻代又可以划分为Eden,幸存者0,幸存者1(8:1:1)。默认自适应,其实并不是8:1:1。

几乎所有的对象都是在Eden区被new出来的。绝大多数java对象的销毁都是在新生代进行的,80%的对象都是朝生夕死,可以使用-Xmn设置新生代最大内存,一般使用默认。-XX:NewRatio:设置新生代与老年代的比例,默认值是2.-XX:Survivoratio:设置新生代中Eden区与幸存者区的比例,默认值是8

4.对象分配过程

1.new的对象先放在伊甸园区,此区有大小限制。
2.当伊甸园的空间填满时,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区的不在被其他对象所引用的对象销毁,在加载新的对象放在伊甸园区。
3.然后将伊甸园区剩余的对象移动到幸存者0.
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,接着再去幸存者1区。
5.啥时候去养老区呢?可以设置次数,默认是15次。-XX:MaxTenuringThreshold=

(伊甸园区满了会触发ygc,将幸存者区域和伊甸园区都回收一下,但是幸存者区满了不会触发垃圾回收)
总结:针对幸存者S0,S1区的总结:复制之后有交换,谁空谁是to。关于垃圾回收,频繁在新生区收集,很少在养老区收集,几乎不在永久区或者元空间收集。
在这里插入图片描述

5.常用调优工具

1.JDK命令行
2.Jconsole
3.VisualVM
4.Jprofiler
5.Java Flight Recorder
6.GCViewer
7.GC Easy

6.Minor GC,Major GC,fULL GC

jvm在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分回收都是指新生代。
针对hotSpotVM的实现,它里面的GC按照回收区域分为两大类型:一种是部分收集,一种是FullGC
1.部分收集:不是完整收集整个Java堆的垃圾回收,又分为:1.新生代收集Minor GC:只是新生代的垃圾收集 2.老年代收集Major GC:只是老年代的垃圾收集

目前只有CMSGC拥有单独收集老年代的行为,很多时候Minor GC会和FullGC混淆使用,需要具体分辨老年代回收还是整堆回收。
3.混合收集:收集整个新生代以及部分老年代的垃圾

7.分代式GC策略触发条件

年轻代触发机制

1.当年轻代空间不足时,就会出发minorGC,这里的年轻代指的是Eden满,Survivor满不会触发GC

2.因为java对象大多数存活时间比较短暂,所以MinorGC非常频繁,一般回收速度也比较快。

3.MinorGC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。

在这里插入图片描述

老年代GC触发机制

1.指的是发生在老年代的GC,对象从老年代消失时,我们说的MajorGC和FullGC发生了。

2.出现了MajorGC,经常会伴随至少一次的MinorGC,也就是在老年代空间不足的时候,会先尝试触发minorGC,如果之后空间还是不足,则会触发MajorGC。

3.MajorGC的速度一般会比MinorGC慢10倍以上,STW时间更长。

4.如果MajorGC后,内存还是不足,就会OOM。

FullGC触发条件

1.System.gc() 不是一定执行的。

2.老年代空间不足

3.方法区空间不足

4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存。

5.Eden区,survivor0 区向survivor1区复制时,对象大小大于To Space可用区域,则把该对象转存到老年代,且老年代的可用内存小于该对象的内存就会触发GC。
Full GC是开发或者调优中尽量要避免的,这样暂停时间会短一些。

8.堆空间分代思想

研究表明,堆空间百分之七十以上的对象是临时对象,其实不分代完全可以,分代的唯一理由就是优化GC性能,如果没有分代,所有对象都在一块,GC的时候要找到哪些对象没用,这样就会造成整堆扫描,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一块区域,当GC时先把这块区域存储朝生夕死对象的区域进行回收,这样就会腾出很大空间。

9.对象分配原则

针对不同年龄段对象分配原则如下

优先分配到伊甸园区,大对象直接分配到老年代,尽量避免程序中出现过多的大对象,长期存活的对象分配到老年代,动态对象年龄判断,如果幸存者区中相同年龄的所有对象大小的总和大于幸存者空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到达到16.

空间分配担保: -XX:HandlePromotionFailure

10.TLAB

堆空间为每个线程分配的TLAB

堆空间是线程共享区域,任何线程都可以访问到堆区中的共享资源。由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

在这里插入图片描述

从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每一个线程分配了一个线程私有的缓存区域,它包含在伊甸园区内。

多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配的方式称为快速分配策略

尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选。

在程序中,可以通过-XX:UseTLAB设置是否开启TLAB空间。

默认情况下,TLAB空间的内存非常小,仅仅占整个Eden空间的百分之一,当然我们可以通过选项-XX:TLABWasteTargetPercent设置Tlab空间所占用Eden空间的百分比大小。

一旦对象在TLAB空间分配内存失败时,JVM会尝试通过加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

在这里插入图片描述

11.堆空间的参数设置

 -XX:+PrintFlagsInitial 查看所有的参数的默认值
 -XX:+PrintFlagsFinal 查看所有的参数的最终值
 -Xms: 初始化堆空间大小
 -Xmx: 最大堆空间内存
 -Xmn: 设置新生代的大小
 -XX:NewRatio 配置新生代与老年代在堆结构的占比
 -XX:SurvivorRatio:设置新生代中Eden和s0/s1空间的比例
 -XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
 -XX:+PrintGCDetails 输出详细的GC处理日志
 打印gc简要信息:XX:+PrintGC -verbose:gc
 -XX:HandlePromotionFailure 是否设置空间分配担保

在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次GC是安全的。如果小于,虚拟机会查看空间分配担保的策略参数是否为true,如果为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。如果大于,则尝试进行老年代GC,但是这次GC依然是有风险的;如果小于,则改为进行FullGC。如果空间分配担保的策略参数为false,直接FullGC。

12.从逃逸分析角度分析对象内存分配

1.堆是分配对象存储的唯一选择嘛?

在java虚拟机中,对象在java堆中分配内存的,这是一个普遍的常识。但是有一个特殊的情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能优化成栈上分配。这样就无需再堆上分配内存,也无须进行垃圾回收了。这是最常见的堆外存储技术。

2.逃逸分析

1.如果将堆上的对象分配到栈,需要使用逃逸分析手段。

2.这是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

3.通过逃逸分析,hotspot编译器能够分析出一个新的对象的引用的适用范围从而决定是否要将这个对象分配到堆上。

4.逃逸分析的基本行为就是分析对象动态作用域:

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

5.没有发生逃逸的对象,则可以分配到栈上,随着方法的执行结束,栈空间就被移除,栈空间指向堆空间对象的引用就没了,等到年轻代GC,对象就会被回收。

3.补充

其实主要就是解决了循环引用,没有逃逸分析,这个对象肯呢个一直不被回收,但是有了逃逸分析,栈帧销毁,这个对象就是垃圾了。

13.使用逃逸分析堆代码进行优化

1.栈上分配

将堆分配转换为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束后,栈空间被回收,局部变量对象也被回收。这样就无需进行垃圾回收了。

常见的栈上分配场景:在逃逸分析中,已经说明了。分别是给成员变量赋值,方法返回值,实例引用传递。

2.同步省略

如果一个对象被发现只能从一个线程被访问到,那么这个对象的操作可以不考虑同步。线程同步的代价是相当高的,同步的后果是降低并发和性能。在动态编译同步代码块的时候,jit编译器可以借助逃逸分析来判断同步块所使用的锁的对象是否只能被一个线程访问而没有被发布到其他线程,如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性能,这个取消的性能就叫做同步省略,也叫锁消除。

3.分离对象或标量替换

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分可以不存储在内存,而是存储在CPU寄存器中

1.标量是指一个无法在分解成更小的数据的数据,Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量,java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

2.标量替换的参数设置
参数-XX:+EliminateAllocations开启了标量替换,允许将对象打散分配在栈上。

八,方法区

1.栈,堆,方法区的交互关系

在这里插入图片描述

Person person=new Person();
Person这个运行时的大Class实例放在方法区
person这个在java虚拟机栈的某个栈帧的局部变量表
new Person();结构在堆中。

2.方法区的理解

1.尽管所有方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾回收或者压缩,对于hotspot而言,方法区还有一个别名叫非堆,目的就是要和堆分开。所以方法区可以看做是一块独立于java堆的内存空间。方法区与堆空间一样,是多线程共享的,方法区在jvm启动的时候被创建,并且他的实际物理内存空间中和java堆区一样都可以是物理上不连续的。方法区的大小和堆一样可以选择固定大小和扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法去溢出,虚拟机同样会OOM。关闭jvm就会释放这个区域的内存。

加载大量第三方jar

tomcat部署的工程太多

大量动态的生成反射类

关闭jvm就会释放这个区域的内存

3.方法区的演进过程

jdk7以前,习惯上把方法区成为永久代,jdk8开始,使用元空间取代了永久代。

方法区等同于永久代仅仅针对hotspot而言。

永久代更容易OOM

永久代使用的是jvm的内存,元空间使用的是本地内存

两者不光名字不同,内部结构也发生了变化

4.设置方法区大小的参数

方法区的大小不必是固定的,jvm可以根据应用需要动态调整。

1.元数据去大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
2.默认值依赖于平台,win下,元空间默认大小是21M,最大值是-1,代表没有限制。
3.与永久代不同,默认情况下,如果指定大小,虚拟机会耗尽所有的可用系统内存。如果元数据去发生溢出,虚拟机一样会OOM。
4.-XX:MetaspaceSize:设置初始元空间大小。这就是一个水平线,打到这个线就会出发FullGC,如果设置小了就会频繁GC,所以尽量设置大点,每次GC,会卸载没用的类(即对应的类加载器不在存活),然后这个水平线将会被重置。新的水位线取决于GC释放了多少空间。如果释放空间不足,那么在不超过最大值时,他会适当调高,如果释放空间过多,则适当降低该值。

5.如何解决OOM

1.要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具堆dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出。

2.如果是内存泄漏,可以进一步通过工具查看泄露对象到GC ROOTS的引用链,于是就能找到泄露对象到底是通过怎么样的路径与GC ROOTS相关联导致垃圾收集器无法自动回收他们的。掌握了泄露对象的类型信息,以及GC ROOTS引用链的信息,就可以比较准确的定位出泄露代码的位置。

3.如果不存在内存泄漏,换句话说就是内存中的对象却是还必须存活着,那就应当检查虚拟机堆参数,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

6.方法区的内部结构

在这里插入图片描述

在这里插入图片描述

他用于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。

1.类型信息

对每个加载的类型,jv必须在方法区存储一下类型信息。

全限定类名,直接父类的全限定类名,这个类型的修饰符,这个类型直接接口的一个有序列表

2.域信息

jvm必须在方法区中保存类型的所有域的信息以及域的声明顺序。

域的相关信息包括:域名称,域类型,域修饰符。

3.方法信息

jvm必须保存所有方法的以下信息,同域信息一样包括声明顺序

方法名称,返回类型,参数的数量和类型,顺序,方法的修饰符,方法的字节码,操作数栈,局部变量表以及大小,异常表

每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引。

/**
 * @author yinhuidong
 * @createTime 2020-08-20-12:09
 * 1.静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。
 * 2.类变量被类的所有实例共享,即使没有实例时你也可以放问他。
 * 3.全局常量 static final
 * 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
 *图:final-static
 * javap -v Order.class
 * javap -v  -p DemoE.class > DemoE.txt
 */
public class DemoD {
    public static void main(String[] args) {
        Order order=null;
        order.hello();
    }
}
class Order{
    public static int a=1;
    public static final int b=2;
    static {
        a=3;
    }
    public final static void hello(){
        System.out.println("a = " + a+"----"+"b = " + b);
        System.out.println("hello");
    }
}

在这里插入图片描述

4.class文件中常量池的理解

方法区内部包含了运行时常量池,字节码文件内部包含了常量池。

为什么需要常量池?

java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用,在动态链接的时候就会用到运行时常量池。

一个有效的字节码文件中除了包含类的版本信息,字段,方法以及接口等描述信息外,还包含一项信息那就是常量池表,包含各种字面量和对应类型,域和方法的符号引用。

总结:常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型。

5.运行时常量池

方法区的一部分,class文件的常量池被类加载器加载到运行时数据区就会在方法区生成对应的运行时常量池,JVM为每一个已经加载的类或接口都维护了一个常量池。池子中的数据就像数组一样,都是通过索引访问的,运行时常量池包括多种不同的变量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换位真实地址。

运行时常量池动态性,当创建类或者接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则会OOM。

7.图解常量池操作

在这里插入图片描述

public class DemoE {
    public static void main(String[] args) {
        int x=500;
        int y=100;
        int a=x/y;
        int b=50;
        System.out.println(a+b);
    }
}

8.方法区的演进

在这里插入图片描述

只有hotspot才有永久代。hotspot中方法区的变化:

jdk1.6 有永久代,静态变量存放在永久代
jdk1.7 有永久代,但已经逐步‘去永久带’,字符串常量池,静态变量移除,保存在堆
jdk1.8 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但是字符串常量池,静态变量仍在堆。

永久代为什么要被元空间替换?

为永久代设置空间大小很难确定,以前使用虚拟机内存,现在使用本地内存,对永久代调优困难

方法区的垃圾收集主要分两部分

常量池中废弃的常量和不再使用的类型

9.StringTable为什么调整位置

jdk7将StringTable放到了堆空间。因为永久代的垃圾回收效率低,在full gc的时候才会触发。而full gc是老年代空间不足,永久代不足的时候才会触发。这就导致了StringTable的回收效率不高。而我们在开发的时候会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

10.静态变量放在哪里

/**
 * @author yinhuidong
 * @createTime 2020-08-20-15:38
 * 分析:
 * 1.new出来的结构,也就是对象实例都在堆空间
 * 2.1.6:obj1随着DemoG的类型信息放在方法区;1.7:放在堆空间
 * 3.obj2是实例变量,在堆空间
 * 4.obj3在foo方法对应的栈帧的局部变量表
 */
public class DemoG {

    static class Test{
        static Order obj1=new Order();
        Order obj2=new Order();
        void foo(){
            Order obj3=new Order();
        }
    }
    static class Order{

    }
}

11.方法区的垃圾回收

1.一般来说,方法区的回收效果不好,特别是类型的卸载,但是有时候回收又是必要的

2.方法区的垃圾回收主要是两部分:废弃的常量和不再使用的类型

3.方法区常量池主要存放:字面量和符号引用

符号引用包括:1.类和接口的全限定类名2.字段的名称和描述符3.方法的名称和描述符

4.只要常量池中的常量没有被任何地方引用,就可以被回收

5.判断一个类是否可以被回收

1.该类所属的实例都已经被回收2.加载该类的类加载器已经被回收3.该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的对象

九,对象的实例化,内存布局与访问定位

1.对象的实例化方式

在这里插入图片描述

1.创建对象的方式

1.new

new(直接new ,工厂模式,构建者模式)

2.Class.forName().newInstance()

反射的方式,只能调用空参构造器,权限是public

3.Constructor.newInstance(xxx)

反射的方式,可以调用空参,带参的构造器,权限没要求

4.使用clone()

不调用任何构造器,当前需要实现Cloneable接口,实现clone();分为深克隆和浅克隆 对象嵌套

5.使用反序列化

从文件,网络中获取一个对象的二进制流

6.第三方库Objenesis

2.创建对象的步骤

1.首先判断对应的类是否已经加载,链接,初始化

2.为对象分配内存

内存规整-指针碰撞

所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是吧指针想空闲那边挪动一段与对象大小相等的距离罢了,如果垃圾收集器选择的是基于压缩算法的,虚拟机采用这种分配方式。一般使用带有整理过程的收集器时,使用指针碰撞。

内存不规整-空闲列表分配

如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。虚拟机维护一个列表,记录哪块内存可用,有多大,在分配的时候找到一块内存足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式称为空闲列表。选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

3.处理并发安全问题

采用cas配上失败重试保证更新的原子性,每个线程预先分配一块tlab

4.初始化分配到空间

所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用

5.设置对象头

6.执行init方法初始化

属性的显示初始化,代码块初始化,构造器初始化

3.对象访问定位

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?

定位,通过栈上reference访问。创建对象的目的是为了使用它。对象访问方式主要有两种:
1.句柄访问
在这里插入图片描述

2.直接指针(HotSpot采用)
在这里插入图片描述

十,直接内存

不是虚拟机运行时数据区的一部分,也不是jvm规范中定义的内存区域。直接内存是在java堆外,直接向系统申请的内存空间。来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存,通常,访问直接内存的速度会优于java堆,即读写性能高,因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存,java的NIO库允许java程序使用直接内存,用于数据缓冲区。

public class DemoH {
    private static Integer BUFFER=10*1024*1024;
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        ByteBuffer byteBuffer=ByteBuffer.allocate(BUFFER);
        sc.next();
        byteBuffer=null;
        System.gc();
    }
}

也可能导致OOM异常,由于直接内存在java堆外,因此它的大小不会受限于-Xmx指定的最大堆的大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

缺点:分配回收成本高,不受JVM内存回收管理,直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认与堆的最大值-Xmx参数值一致,简单理解java直接内存=java堆+本地内存。

十一,执行引擎

1.虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器,缓存,指令集和操作系统层面的,而虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约的定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
在这里插入图片描述

2.jvm的主要作用负责装在字节码到其内部,但是字节码不能直接运行在操作系统之上,执行引擎就是将字节码指令编译为对应平台上的本地机器指令。
在这里插入图片描述

3.外观上看:jvm的执行引擎输入输出都是一致的,输入的是字节码二进制流,处理过程是字节码解析执行的过程,输出的是执行结果。

java代码的编译和执行过程

在这里插入图片描述

大部分的程序代码转换为物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图的步骤。
为什么java称为半解释型半编译型语言?
因为jvm的执行引擎是解释器和jit即时编译器交互工作的。
什么是解释器?什么是JIT编译器?
解释器:将字节码指令逐行翻译成机器指令并执行
编译器:将源代码直接编译成对应的机器指令。

机器码,指令,汇编语言

在这里插入图片描述

1.机器码:二进制编码方式表示的机器指令

2.指令,指令集:将机器中特定的0和1简化成对应的指令。每个平台所支持的指令就叫做指令集

3.汇编语言:用助记符代替机器指令的操作码,所以汇编编写的程序还需要翻译成机器指令码

4.高级语言:需要把程序解释和编译成机器的指令码

5.字节码:一种中间状态的二进制文件,实现特定软件运行和软件环境,与硬件环境无关

解释器

在这里插入图片描述

解释器其实就是一个运行时翻译者,将字节码文件中的内容翻译为对应平台的本地机器指令执行。当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。

解释器分类

1.字节码解释器:纯软件代码模拟字节码的执行

2.模板解释器:将每一条字节码指令和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码

基于解释器执行已经沦落为低效的代名词,JIT编译的目的是为了避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行,只执行编译后的机器码即可。

JIT即时编译器

HotSpot采用解释器与即时编译器共存的架构。由JVM决定何时使用哪种方式执行。

解释器可以边解释边执行,这样程序启动时间就会变快,及时编译器是都编译好了在执行,程序启动时间慢。但是一旦jit编译器把越来越多的代码编译成本地代码,执行效率立马起飞。

在这里插入图片描述

在这里插入图片描述

热点代码以及探测方式

判断是否启动jit编译器将字节码直接编译为对应平台的本地机器指令,需要根据执行的频率而定。热点代码就是需要被编译为本地代码的字节码,jit在运行时会针对频繁调用的热点代码直接编译为对应平台的本地机器指令,提升性能。

OSR编译

一个方法被多次调用,或者一个方法体内部循环次数较多的循环体都可以被称为热点代码,因此都可以通过jit编译器编译为本地机器指令,也就是栈上替换.hotspot采用的热点探测方式是基于计数器的热点探测。

为每个方法建立2个不同类型的计数器,分别为方法调用计数器和回边计数器
1.方法调用计数器:统计方法调用次数,默认client模式下1500,server模式下100002.回边计数器:统计循环执行次数

当一个方法被调用时,先判断有没有jit编译过,有的话直接使用jit编译后的机器指令,没有的话计数器+1,然后判断两个计数器之和是否超过方法调用计数器的阈值。如果超过,就会向jit发出即时编译申请。

热度衰减是在虚拟机进行垃圾回收的时候顺便进行的,也就是在一段时间方法一直没有执行,计数器的计数就会减半。

可以自己手动设置虚拟机采用哪种编译模式

JDK9引入了AOT编译器

aot编译器在程序执行之前,就将字节码转换为机器码过程。

好处:可以直接运行,不必预热。

坏处:由于提前编译成了机器指令,无法实现java一次编译到处运行。

JDK10的Graal编译器

全新的即时编译器,实验阶段,需要手动开启,前景大好

十二,StringTable

String的基本特性

1.String字符串使用一对引号引起来表示
  String s1=“yhd”;
  String s2=new String(“yhd”);
  jdk8:private final char value[];
  jdk9:private final byte value[];
2.String 类声明为final,不可被继承
3.String 类实现了 Serializable接口,支持序列化 ,实现了Comparable接口,可以比较大小
4.String代表不可变的字符序列,简称不可变性。
 1.当对字符串重新赋值,需要重新指定内存区域赋值,不能使用原有的value赋值。
 2.当对现有的字符串进行连接操作时,也需要重新制定内存区域赋值,不能使用原有的value赋值。
 3.当调用String的replace方法修改指定的字符或者字符串时,也需要重新指定内存区域赋值
 一方面数组的长度一旦确定了,就不能再改变,一方面存储在字符串常量池的数据不可发生变化。
5.通过字面量的方式给一个字符串赋值,此时的字符串值声明在字符串常量池。
6.字符串常量池不会存储两个相同内容的字符串。
7.String的底层是一个固定大小的hashtable,默认长度是1009,如果放入StringPool的String特别多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了会影响String.intern的性能
8.使用-XX:StringTableSize设置StringTable的长度。
9.jdk6StringTable长度固定为1009,字符串多就会造成性能下降
10.jdk7 StringTable的长度默认值是60013,1009是可设置的最小值。
11data.intern();//如果字符串常量池中没有对应data的字符串的话,则在常量池中形成。

String的内存分配

常量池就类似一个Java系统级别提供的缓存。8中基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种
1.直接使用双引号声明出来的String对象会直接存储在常量池中。2.如果不是用双引号声明的String对象,可以使用String提供的intern()方法。

所有的字符串都保存在堆中,这样调优的时候仅仅需要调整堆的大小就可以了。

StringTable为什么要调整?
1.permSize默认比较小2.永久代垃圾回收频率低

String的基本操作

java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符串序列,并且必须是指向同一个String类实例。

字符串拼接

1.常量与常量的拼接结果在常量池,原理是编译期优化
2.常量池中不会存在相同内容的常量
3.只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
4.如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象的地址。
5.字符串拼接底层原理分析

    public static void main(String[] args) {
        String s1="a";
        String s2="b";
        String s3="ab";
        String s4=s1+s2;
        System.out.println(s3==s4);
        /**
         * 如下的s1+s2的执行细节:
         * ① StringBuilder  s= new StringBuilder();
         * ② s.append("a");
         * ③ s.append("b");
         * ④ s.toString(); //类似于new String("ab");
         */
        }

public class DemoA {
    /**
     * 1.字符串拼接操作不一定使用的是StringBuilder!
     * 如果拼接符号左右两边都是字符串常量或常量引用。则仍然使用编译器优化,即非
     * StringBuilder的方式。
     * 2.针对于final修饰类,方法,基本数据类型,引用数据类型的结构时,能使用上final
     * 的时候建议使用上。
     * @param args
     */
    public static void main(String[] args) {
        final String s1="a";
        final String s2="b";
        String s3="ab";
        String s4=s1+s2;
        System.out.println(s3==s4);//true
    }

6.拼接操作和append操作的效率对比
	StringBuilder  s= new StringBuilder();//每次直接编译器优化
	s.append("a");
	src+="a";//每次都会创建一个新的StringBuilder 而且还会new String()
	总结:实际开发,尽量少用空参的list和StringBuilder,防止不断扩容。

intern()的使用

intern()方法就是确保字符串在内存中只有一份,这样可以节约内存空间,加快字符串操作任务的执行速度,这个值会被存放在字符串内部池。

如何保证变量s指向的是字符串常量池的数据呢?

方式1:String s=“shkstart”;//字面量定义的方式

方式2:String s=new String(“shkstart”).intern();

String s=new StringBuilder(“shkstart”).toString().intern();

String a=new String(“a”);

底层创建了两个对象,一个在堆空间,一个在字符串常量池
new String(“a”)+new String(“b”);创建了几个对象
对象1:new StringBuilder();
对象2:new String(“a”);
对象3:常量池中的a
对象4:new String(“b”);
对象5:常量池中的b
对象6:new Sring(“ab”);
强调一下,toString()的调用,在字符串常量池中,没有生成"ab";

StringTable的垃圾回收

G1中的String去重操作

String s1=new String("a");
String s2=new String("a");

此时s1和s2在堆空间new了两个一模一样的对象,两个对象在同时指向字符串常量池的同一个字符串a,此时G1就会删除一个堆空间的对象,让s1和s2都指向同一个堆空间的对象。

public class DemoA {
    /**
     * 1.字符串拼接操作不一定使用的是StringBuilder!
     * 如果拼接符号左右两边都是字符串常量或常量引用。则仍然使用编译器优化,即非
     * StringBuilder的方式。
     * 2.针对于final修饰类,方法,基本数据类型,引用数据类型的结构时,能使用上final
     * 的时候建议使用上。
     * @param args
     */
    public static void main(String[] args) {
        final String s1="a";
        final String s2="b";
        String s3="ab";
        String s4=s1+s2;
        System.out.println(s3==s4);//true
    }

    
    
    6.拼接操作和append操作的效率对比
	StringBuilder  s= new StringBuilder();//每次直接编译器优化
	s.append("a");
	src+="a";//每次都会创建一个新的StringBuilder 而且还会new String()
	总结:实际开发,尽量少用空参的list和StringBuilder,防止不断扩容。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页