这里写自定义目录标题
1、JVM
1、 JVM是什么
JVM 是 Java 能够跨平台的核心
JVM(Java Virtual Machine)是 Java 虚拟机,用于运行 Java 编译后的二进制字节码,最后生成机器指令。
三种JVM:
Sun公司:HotSpot 用的最多
BEA:JRockit
IBM:J9VM
我们学习都是:HotSpot
2、JDK,JRE,JVM三者关系
JDK :(Java Development Kit),Java 开发工具包。 JDK 是整个 Java 开发的核心,集成了 JRE 和javac.exe,java.exe,jar.exe 等工具。
JRE :(Java Runtime Environment),Java 运行时环境。 主要包含两个部分,JVM 的标准实现和 Java 的一些基本类库。它相对于 JVM 来说,多出来的是一部分的 Java 类库。
三者的关系是:一层层的嵌套关系。JDK>JRE>JVM
3、JVM 与操作系统之间的关系
JVM 上承开发语言,下接操作系统,它的中间接口就是字节码
2、JVM体系结构
Java虚拟机定义了若千种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
- 每个线程独立。
- 线程私有(指令区):程序计数器(PC)、栈(VMS)、本地方法栈(NMS)。
- 线程间共享(数据区):堆(Heap)、方法区
2.1、类加载器
2.1.1、类加载器分类
引导类加载器(Bootstrap ClassLoader)和自定义加载器(User-Defined ClassLoader)
概念上,将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
在程序中我们常见的3类加载器:
- 启动类加载器(引导类加载器)(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序加载器(系统类加载器)(Application ClassLoader)
应用程序加载器(系统类加载器)(Application ClassLoader)
Java语言编写,由sun.misc.Launcher$AppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过ClassLoader#getSystemClassLoader()方法可以后去到该类加载器
扩展类加载器(Extension ClassLoader)
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
派生于ClassLoader类
父类加载器为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
启动类加载器(引导类加载器)(Bootstrap ClassLoader)
使用C/C++语言实现的,嵌套在JVM内部
用来加载Java核心类库,(JAVA_HOME/jre/lib/rt.jar,resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
并不继承java.lang.ClassLoader,没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
- 隔离加载类(例如使中间件的Jar包与应用程序Jar包不冲突);
- 修改类加载的方式(启动类加载器必须使用,其他可以根据需要自定义加载);
- 扩展加载源;
- 防止源码泄漏(对字节码进行加密,自定义类加载器实现解密)
2.1.2、加载(Loading)
1、通过一个类的全限定名获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类加载器的作用
类加载子系统负责从文件系统或者网络中加载Class文件,Class文件开头有特定的文件标识。加载的类信息存放于一块称为方法区的内存空间。
作用:加载Class文件~ 如果new Student();(具体实例在堆里,引用变量名放栈里)
查找顺序
- application classLoader 应用程序加载器
- extension classLoader 扩展类加载器
- bootStrap classLoader 启动类(根)加载器
- 虚拟机自带的加载器
获取类加载器代码示例
双亲委派机制
Java虚拟机对Class文件采用的是按需加载,而且加载class文件时,Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
优势
避免类的重复加载
保护程序安全,防止核心API被篡改
作用
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。 举例:java.lang.String
工作原理:
当一个Hello.class这样的文件要被加载时
- 不考虑我们自定义类加载器,首先会在AppClassLoader(应用程序加载器)中检查是否加载过,如果有那就无需再加载了
- 如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
- 如果父类的加载器可以完成类加载任务,就成功返回。
父类加载器无法完成加载任务,子加载器才会尝试自己去加载,一直到最底层。
如果没有任何加载器能加载,就会抛出ClassNotFoundException。这就是双亲委派模式
- 存取控制器(access controller): 存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- 安全管理器(security manager): 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- 安全软件包(security package): java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名 keytools https(需要证书)
- 加密
- 鉴别
沙箱安全机制
什么是沙箱?
沙箱是一个限制程序运行的环境(沙箱主要限制系统资源的访问,如cpu,内存等等。不同级别的沙箱对这些资源的访问限制也不一样)
什么是沙箱机制?
就是将java代码限定在虚拟机(jvm) 特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对系统造成破坏。
作用
限制系统资源访问,保证对Java核心源代码的保护
jdk1.6安全模型(当前最新安全机制)
沙箱的基本组件
- 字节码校验器(bytecode verifier): 确保Java类文件.Class遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
- 类装载器(class loader): 其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
Java程序对类的使用方式分为:主动使用和被动使用。
主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName ( “com.atguigu. Test”) )
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK 7 开始提供的动态语言支持: java . lang.invoke.MethodHandle实例的解析结果 REF getStatic、REF putstatic、REF_invokestatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用, 都不会导致类的初始化。
2.1.3、连接(Linking)
验证(Verify)
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
-
文件格式验证
- CA FE BA BE(魔数,Java虚拟机识别)
- 主次版本号
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
-
元数据验证
- 对字节码描述的信息进行语义分析,保证描述符合Java规范
- 类是否有父类,除了Object之外,所有的类都应该有父类
- 类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是 抽象类,是否实现了其父类或接口中要求实现的所有方法。
- 类的字段,方法是否与父类的产生矛盾。例如方法参数都一样,返回值不同
-
字节码验证
- 通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。
- 对类的方法体,进行校验分析,保证在运行时不会做出危害虚拟机的行为
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现类似于在操作数栈放了一个int类型的数据,使用时却按照long类型加载到本地变量表中的情况。
- 保障任何跳转指令都不会跳转到方法体之外的字节码指令上。
-
符号引用验证
- 通过字符串描述的全限定名是否能找到对应的类
- 符号引用中的类、字段、方法的可访问性是否可被当前类访问
准备(Prepare)
为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析(Resolve)
将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class info、CONSTANT_Fieldref info、CONSTANT_Methodref_info等。
2.1.4、初始化(Initialization)
初始化阶段就是执行类构造器方法()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令按语句在源文件中出现的顺序执行。
()不同于类的构造器。(关联:构造器是虚拟机视角下的 () )
若该类具有父类,JVM会保证子类的()执行前,父类的 ()已经执行完毕。
虚拟机必须保证一个类的 ()方法在多线程下被同步加锁。
补充说明
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。
解析阶段不一定,在某些情况下可以在初始化阶段之后再开始,为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)
Java虚拟机规范严格规定了,有且只有六种情况,必须立即对类进行初始化
- 遇到new,getstatic,putstatic或invokestatic这四条字节码指令时。
-
使用new关键字实例化对象
-
读取或设置一个类型的静态字段(final修饰已在编译期将结果放入常量池的静态字段除外)
-
调用一个类型的静态方法的时候
-
对类型进行反射调用,如果类型没有经过初始化,则需要触发初始化
-
初始化类的时候,发现父类没有初始化,则先触发父类初始化
-
虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类
-
只用JDK7中新加入的动态语言支持,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法对应的类没有进行初始化,则先触发其初始化。
-
当一个接口中定了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,要先将接口进行初始化
除了以上几种情况,其他使用类的方式被看做是对类的被动使用,都不会导致类的初始化
2.2、运行时数据区
2.2.1、PC寄存器(程序计数器)
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码 ( 用来存储指向下一条指令的地址, 也即将要执行的指令代码 ), 在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
-
JVM中的程序计数寄存器(Program counter Register)是对物理PC寄存器的一种抽象模拟。
-
它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
-
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。线程之间互不影响。
-
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。 程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned) 。
-
运行时数据区中唯一不会出现OOM的区域,没有垃圾回收
-
当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的位置
作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
2.2.2、虚拟机栈
Java虚拟机栈(Java virtual Machine stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,是线程私有的,其内部保存一个个的栈帧(stack Frame) ,对应着一次次的Java方法调用。
- 栈是运行时的单位,而堆是存储的单位。
- 栈解决程序如何执行,如何处理数据。
- 堆解决的是数据存储问题,即数据怎么放,放在哪里。
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
生命周期和线程同步,一旦线程结束,栈就Over!
栈不存在垃圾回收,但是存在OOM
优点
快速有效的存储方式,访问速度仅次于程序计数器
JVM直接对JAVA栈的操作只有两个
-
每个方法执行,伴随着进栈(入栈,压栈)
-
执行结束的出栈
栈存放
8大基本类型+对象引用+实例的方法
栈的存储单位 - 栈帧
每个线程都有自己的栈,栈中的数据以栈帧(stack Frame)格式存储
线程上正在执行的每个方法都各自对应一个栈帧(stack Frame)
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息
- 栈帧 (Stack Frame)是用来支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
- 栈帧 (Stack Frame)存储了方法的局部变量表、操作数栈、动态连接、和方法返回地址、额外的附加信息。
- 每个方法在执行的同时,都会创建一个栈帧 (Stack Frame)。
- 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
- 栈帧中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的
- 在栈帧中,与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
栈运行原理
-
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”(FILO)/“后进先出”(LIFO)原则。
-
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(current Frame),与当前栈帧相对应的方法就是当前方法(CurrentMethod),定义这个方法的类就是当前类(current class) 。
-
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
-
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
-
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
-
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
-
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
局部变量表(Local variables)
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。(fy:所以这就是变量的生命周期?)
slot
- 局部变量表,最基本的存储单元是slot(变量槽)
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot(包括 returnAddress类型),64位的类型(long和double)占用两个slot。
- byte 、 short 、 char在存储前被转换为int,boolean也被转换为int,0表示false ,非0表示true。
- long和double 则占据两个slot。
变量的分类
按照数据类型分: 基本数据类型、引用数据类型
按照声明的位置:
- 成员变量: 在使用前经历过初始化过程
- 类变量: 链接的准备阶段给类变量默认赋值,初始化阶段显示赋值,即静态代码块赋值
- 实例变量: 随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
- 局部变量: 在使用前,必须进显式赋值,否则编译不通过
操作数栈(operand stack)(或表达式栈)
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-out)的操作数栈,也可以称之为表达式栈(Expression stack) 。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop) .
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
- 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新Pc寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
比如:执行复制、交换、求和等操作
栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
栈顶缓存技术
由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读/写次数,提升执行引擎的执行效率
动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
常量池在字节码文件中,运行时常量池,在运行时的方法区中
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。
比如: invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里。
比如: 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
存放调用该方法的pc寄存器的值。
一个方法的结束,只有两种方式:正常执行完成(正常完成出口)。出现未处理的异常,非正常退出(异常完成出口)
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
-
正常完成出口:
- 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
- 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
- 在字节码指令中,返回指令包含ireturn (当返回值是boolean、byte、char、short和int类型时使用)、lreturn、 freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
- 调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
-
异常完成出口
- 在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。
- 方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
- 返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。
此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
区别: 通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
一些附加信息
允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。不确定有,可选情况
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
链接
- 静态链接: 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
- 动态链接: 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
绑定机制
Java具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定: 指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
- 晚期绑定: 如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
虚方法和非虚方法
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
- 非虚方法: 如果方法在编译期就确定了具体的调用版本,则这个版本在运行时是不可变的。私有方法,final方法,实例构造器,父类方法都是非虚方法
- 虚方法: 其他方法称为虚方法
虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派 的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。
因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
那么虚方法表什么时候被创建? 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
静态语言和动态语言
- 静态类型语言: 是编译期检查类型,判断变量自身的类型信息;Java是静态类型语言,动态调用指令增加了动态语言的特性
- 动态类型语言: 是运行期检查类型,判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
2.2.3、本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
凡是带了native 关键字的,说明java的作用范围达不到了,得回去调用底层C语言的库!
凡是带了native 关键字的方法会进入本地方法栈,其它的是java栈
本地方法栈:
- 本地方法是使用c语言实现的。也是线程私有的。
- 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
JNI:Java Native Interface(本地方法接口)
调用本地方法接口(JNI)作用:
- 扩展java的使用,融合不同的编程语言为java所用
- java诞生的初衷是融合C/C++程序,C、C++横行,想要立足,必须要有调用C、C++的程序~
- 它在内存区城中专门开辟了块标记区城: Native Method Stack
它的具体做法是Native Method stack中登记natilve方法,
在执行引擎(Execution Engine)执行时通过JNI 加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存。
在企业级应用中少见,与硬件有关应用:java程序驱动打印机,系统管理生产设备等,掌握即可
2.2.4 堆
概述
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB)。
- “几乎”所有的对象实例都在这里分配内存
- 类加载器读取了类文件后,一般会把什么东西放到堆中?
- 类,方法,常量,变量~,保存所有引用类型的真实对象
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的位置
- 方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆是GC(Garbage Collection ,垃圾回收器)执行垃圾回收的重点区域
堆空间细分
堆内存逻辑上分为三部分
- jdk1.6之前:新生代+老年代+永久代,常量池在方法区
- jdk1.7:新生代+老年代+永久代,但是慢慢退化了(去永久代)常量池在堆中
- jdk1.8之后:新生代+老年代+元空间,常量池在元空间
一句话:运行时常量池一直在方法区,JDK1.7之后字符串常量池保存到了堆中。
- 物理上、堆内存分为新生代和老年代,可以调节新生代和老年代空间大小。
- 新生代与老年代空间默认比例1:2,
- 新生代占堆空间三分之一,老年代占堆空间三分之二,如下图:
元空间逻辑上存在,物理上并不存在。
新生区、老年区、永久区
新生区(伊甸园+幸存者区*2)
- 类诞生和成长甚至死亡的地方;
- 伊甸园,所有对象都是在伊甸园区new出来的!
- 幸存者区(0, 1),轻GC定期清理伊甸园,活下来的放入幸存者区,幸存者区满了之后重GC清理 伊甸园+幸存者区,活下来的放入养老区。都满了就报OOM。
真理: 经过研究,99%的对象都是临时对象!直接被清理了
老年区:
新生区剩下来的,轻GC杀不死了。
永久区:
这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,该区域不存在垃圾回收GC。关闭虚拟机就会释放这个内存。
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM。
新生区、老年区、永久区运行过程
- new的对象先放伊甸园区。此区有大小限制。大对象直接分配到老年代(尽量避免程序中出现过多的大对象。长期存活的对象分配到老年代)
- 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC=轻GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
- 然后将伊甸园中的剩余对象移动到幸存者0区。
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
注: 当伊甸园的空间填满时进行Minor GC,将伊甸园区和幸存者区一起垃圾回收,幸存者区属于被动回收,但幸存者区满时不会促发Minor GC,这不代表它没有垃圾回收。- 啥时候能去养老区呢?对象在 幸存者区中每熬过一次MinorGc ,年龄就增加1岁,默认为15 岁,就会被晋升到老年代中。
可以设置参数: -XX :MaxTenuringThreshold=进行设置默认值。
注: 如果幸存者区中相同年龄的所有对象大小的总和大于幸存者空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。- 在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC = 重GC,进行养老区的内存清理。
- 若养老区执行了Major GC = 重GC之后发现依然无法进行对象的保存,就会产生OOM异常 java. lang. outOfMemoryError: Java heap space
TLAB( Thread Local Allocation Buffer )-为对象分配内存
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分存空间是线程不安全的
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
尽管不是所有的对象实例都能够在TAB中成功分配内存,但JVM确实是TLAB作为内存分配的首选。
在程序中,开发人员可以通过选项“一xx:UseTLAB”设置是否开启TLAB空间。默认开启。默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项“一XX:TLABwasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。
一旦对象在TAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
堆空间大小设置
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项”-Xmx"和-Xms"来进行设置。
-
"-xms"用于表示堆区的起始内存,等价于-XX : InitialHeapsize
-
"-xmx"则用于表示堆区的最大内存,等价于-XX: MaxHeapsize
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出 OutOfMemoryError异常。
通常会将-Xms和—Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下,初始内存占物理电脑内存大小 1 / 64,最大内存占物理电脑内存大小1/4
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory(); //字节 1024*1024
//返回jvm初始化的总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max="+max+"字节\t"+(max/(double)1024/1024+"MB"));
System.out.println("total="+total+"字节\t"+(total/(double)1024/1024+"MB"));
/* 运行后:
max=1866465280字节 1780.0MB
total=126877696字节 121.0MB
*/
}
扩大内存方法:
Edit Configration>add VM option>输入:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
新生区+养老区:305664K+699392K=1005056K = 981.5M
说明元空间物理并不存在。只是逻辑存在
//-Xms8m -Xmx8m -XX:+PrintGCDetails
public static void main(String[] args) {
String str = "kuangshensayjava";
while (true){
str += str + new Random().nextInt(888888888)+ new Random().nextInt(21_0000_0000);
}
}
Jprofiler
在一个项目中,突然出现了OOM故障,该如何排除,研究为什么出错~
- 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
- Debug,一行行分析代码!
MAT,Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏;
- 获得堆中的数据
- 获得大的对象(大厂面试)
…
逃逸分析(Escape Analysis)
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。
但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。
这样就无需在堆上分配内存,也无须进行垃圾回收了。
这也是最常见的堆外存储技术。
如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。开发中能使用局部变量,就不要在方法外定义
逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有 发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃 逸。例如作为调用参数传递到其他地方中。
代码优化
- 栈上分配: 在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。
- 同步省略: 线程同步的代价是相当高的,同步的后果是降低并发性和性能。JIT编译器在编译这个同步块的时候,会借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程,如果没有就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
- 分离对象或标量替换: 标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate) ,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
垃圾回收
GC两种:轻GC,重GC (Full GC,全局GC)
垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在元空间收集。
Full GC触发机制
触发Full GC执行的情况有如下五种:
- 调用system.gc()时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0 (From Space)区向survivor space1 (To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
关于GC面试题:
- JVM的内存模型和分区~详细到每个分区放什么?
- 堆里面的分区有哪些?Eden, from, to, 老年区,说说它们的特点!
- GC算法有哪些?怎么用的?标记清除法,标记整理,复制算法,分代收集法。引用计数法。
轻GC与重GC分别在什么时候发生? 新生区、老年区、永久区运行过程
引用计数法
一般JVM不用,大型项目对象太多了
复制算法
- -XX:MaxTenuringThreshold=15 设置进入老年代的存活次数条件
- 好处:没有内存的碎片,内存效率高
- 坏处:浪费了内存空间(一个幸存区永远是空的);假设对象100%存活,复制成本很高。
复制算法最佳使用场景:对象存活度较低的时候,新生区~。
标记清除算法
- 优点:不需要额外空间,优化了复制算法。
- 缺点:两次扫描,严重浪费时间,会产生内存碎片。
标记压缩清除算法
- 三部曲:标记–清除–压缩
- 每标记清除几次就压缩一次,或者内存碎片积累到一定程度就压缩。
算法总结
-
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
-
内存整齐度:复制算法=标记压缩算法>标记清除算法
-
内存利用率:标记压缩算法=标记清除算法>复制算法
难道没有最优算法吗?
答案:无,没有最好的算法,只有合适的算法(GC也被称为分代收集算法)。
- 年轻代:存活率低,用复制算法。
- 老年代:存活率高,区域大,用标记-清除-压缩。
参考和研究:《深入理解Java虚拟机》
2.2.5 方法区
-
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会去进行垃圾收集或者进行压缩。”但对于HotSpot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
-
方法区是JVM概念上的一个区,不会随着JDK版本的变化而变化,方法区的具体实现,却会随着JDK版本的变化而变化的。JDK7以前,方法区的实现是在“永久代”里;到了JDK8以后,则在Metadata空间里。
-
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.outofMemoryErroPermGen space或者java.lang.outofMemoryError: Metaspace
-
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于: 元空间不在虚拟机设置的内存中,而是使用本地内存。
-
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
-
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
如:static,final,,Class(类模板), 常量池
设置内存大小
-
jdk7及以前
- 通过-xx: Permsize来设置永久代初始分配空间。默认值是20.75M
- 一XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
- 当JVM加载的类信息容量超过了这个值,会报异常outOfMemoryError: PermGenspace 。
-
jdk8及以后
-
元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
-
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MazMetaspacesize的值是-1,即没有限制。
-
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。
如果元数据区发生溢出,虚拟机一样会抛出异常outOfMemoryError: Metaspace
- -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说.其默认的-XX:MetaspaceSize值为21MB。这就是初始的高水位线,
- 一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。
- 新的高水位线的值取决于GC后释放了多少元空间。
- 如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
- 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将- XX:Metaspacesize设置为一个相对较高的值。
如何解决OOM?
要先分清楚到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory overflow) 。
内存泄漏(MemoryLeak):
如果是内存泄漏,可进一步通过工具查看泄漏对象到Gc Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
内存溢出(Memory overflow):
不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。