做网站遇到各种问题,网站建设实训计划书,Wordpress动静态分离,seo网络推广方法一、JVM概述#
1. JVM内部结构#
跨语言的平台#xff0c;只要遵循编译出来的字节码的规范#xff0c;都可以由JVM运行 虚拟机
系统虚拟机 VMvare
程序虚拟机 JVM JVM结构 HotSpot虚拟机 详细结构图 前端编译器是编译为字节码文件
执行引擎中的JIT Compiler编译器是把字节…一、JVM概述#
1. JVM内部结构#
跨语言的平台只要遵循编译出来的字节码的规范都可以由JVM运行 虚拟机
系统虚拟机 VMvare
程序虚拟机 JVM JVM结构 HotSpot虚拟机 详细结构图 前端编译器是编译为字节码文件
执行引擎中的JIT Compiler编译器是把字节码编译成机器码
Java编译器java c
词法分析语法分析-语法/抽象语法树语义分析-注解抽象语法树-字节码生成器- .class字节码文件 汇编知识 解释执行和编译执行返回执行的热点代码编译成机器指令缓存到方法区并行
2. JVM的指令集架构#
Java编译器输入的指令流是一种基于栈的指令集架构。
另一种指令集架构是基于寄存器的指令集架构
二者的区别
基于栈式架构
设计和实现更简单适用于资源受限的系统避开了寄存器的分配难题使用零地址指令方式分配操作树没有地址指令集更小编译器更容易实现不需要硬件的支持可移植性好。
基于寄存器的架构的特点
x86的二进制指令集pc android依赖硬件可移植性差性能优秀和执行更加高效花费更少的指令去完成一项操作指令集更大16位指令集往往以一地址、二地址、三地址指令为主
javap 执行反编译
基于栈指令集小指令多跨平台性执行性能比寄存器差
3. JVM的生命周期#
虚拟机启动 bootstrap class loader引导类加载器 创建一个初始类initial class这个类是由虚拟机不同的虚拟机不同的具体实现指定的。调用初始类中的main方法加载其他的类
虚拟机执行执行java程序程序结束的时候就停止。执行一个java程序实际上是执行一个叫做java虚拟机的进程
虚拟机退出执行结束程序执行异常终止操作系统出现错误。某些线程调用结束虚拟机进程的方法。
Runtime类运行时数据区中的halt() System中的exit()
JNI java native interface
4. JVM发展历程#
Classic#
Java1.0 1.4不再使用
只提供解释器没有即时编译器 二者是并行的。
解释器逐行解释字节码 效率比较低
JIT 编译成为指令 classic能外挂但只能二选一不能二者协同运作】
hotspot内置了该虚拟机
Exact VM#
JDK1.2
准确式内存管理可以知道内存中某个位置的数据具体是什么类型
编译器和解释器混合工作模式
热点探测知道哪些是热点代码
短暂使用被hotspot替代
Hotspot#
从服务器、桌面、移动端、嵌入式都有应用
JDK1.3 默认虚拟机
后面都主要是针对Hotspot虚拟机讲
名称热点代码探测技术
通过计数器找到最具编译价值的代码触发即时编译或栈上替换
通过编译器和解释器的协同工作在最优化的程序响应时间与最佳执行性能中取得平衡
JRocket#
商用 。BEA 已经被Oracle收购
专注于服务器端不太关注程序的启动速度。
不包含解释器实现全部代码都是编译器编译后执行
最快的JVM
MissionControl 服务套件可以监控分析程序运行中的状态 内存泄漏 、运行时分析器、管理控制台
J9#
IBM 三大有影响力的商用虚拟之一
从服务器、桌面、移动端、嵌入式都有应用
OpenJ9
其他#
KVM 和 CDC/CLDC Hotspot JavaME 现在已经较少使用
KVM : 智能控制器、传感器、老人机
Azul VM 和特定硬件平台绑定 Zing JVM
Liqud VM 不需要操作系统的支持可以直接进行 线程调度、文件系统、网络支持 项目停止了。
Apache Harmony IBM 和intel 没有大规模商用的案例被吸收到了Android SDK JCPJava Community Process成立于1998年是使有兴趣的各方参与定义Java的特征和未来版本的正式过程。 JCP使用JSRJava规范请求Java Specification Requests作为正式规范文档描述被提议加入到Java体系中的的规范和技术。 Microsoft JVM 当时为了支持 Java applets
TaobaoJVM 基于openJDK的深度定制的版本
Dalvik VM 安卓系统5.0 之前 之后使用ART VM替换 基于寄存器结构
JVM的内存结构取决于不同的实现。各个厂商的实现都有所不同。
Graal VM oracle 跨语言的全栈虚拟机。可以作为任何语言的运行平台使用。
二、类加载子系统# 详细图 类加载器子系统负责从文件系统从硬盘中的文件或者网络中加载Class文件根据class文件可以实例化出多个一模一样的实例class文件在文件开头有文件标识ClassLoader只负责文件的加载至于它是否可以运行则有ExecutionEngine决定
加载的类信息存放于一块成为方法区的内存空间方法区还会存放运行时常量池信息。 1. 类的加载过程#
1 加载 loading# 通过一个类的全类名获取定义此类的二进制字节流 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构 在内存中生成一个代表这个类的java.lang.Class对象作为方法区中这个类的各种数据的访问的入口 加载 .class 文件的方式 从本地文件系统 网络 web applet zip jar war 运行时生成 动态代理 其他文件生成 jsp 专有数据库中提取 .class文件 较少见 从加密文件中获取防止反编译的保护 2链接 linking# 验证 verify 确保class文件的字节流中包含的信息符合虚拟机要求保证加载类的正确性不会危害虚拟机自身安全CAFEBABY 开头的字节码文件。四种验证方式 文件格式、元数据、字节码、符号引用 准备 prepare为类变量static的分配内存并且设置该类变量的默认初始值即零值不包括final在编译的时候已经被分配了不会为实例变量分配初始值类变量分配在方法区中而实例变量是会随着对象一起分配到堆中 解析 resolve 在初始化之后进行符号引用 引用相关的结构将常量池中符号引用转换为直接引用的过程。符号引用就是一组符号描述所引用的目标符号引用的字面量形式明确定义在虚拟机规范的class文件格式中直接引用就是直接指向目标的指针相对偏移量或一个间接定位到目标的句柄handle
3初始化 initialization#
执行类构造器方法 clinit()的过程就是用于赋值的如果没有赋值的动作和静态代码块就不会执行clinit()类变量的属性值就会是prepare中指定的初始值实例变量则没有初始化值
不需要定义是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中的指令按语句在源文件中出现的顺序执行
clinit()不同于类的构造器类中的构造器在虚拟机视角是init()方法 每一个类都至少有一个构造器。
如果该类有父类会保证父类的clinit()已经先被执行
虚拟机必须保证一个类的clinit()方法在多线程下是同步加锁的因为类只会被初始化一次到方法区中是单例的 2. 类加载器的分类# 从概念上只有两种加载器 Bootstrap ClassLoader 和 User-defined ClassLoader
将派生于抽象类 ClassLoader的类加载器都划分为 User-defined ClassLoader
所以Extention ClassLoader 和 System ClassLoader 都视为自定义的类加载器
Bootstrap ClassLoader是用C和cpp实现的后面的都是用java实现的。
用户自定义的类的都是System ClassLoader加载的
String类等核心类库都是使用Bootstrap ClassLoader加载的
1Bootstrap ClassLoader#
启动类加载器嵌套在jvm内部 用来加载Java的核心类库JAVA_HOME/jre/lib/rt.jar、resources.jar 、sun.boot.class.path用于提供jvm自身需要的数据. 没有父类加载器加载扩展类和应用程序类加载器并制定为他们的父类加载器
在加载包名为java、javax 、sun等开头的类
嵌入式还是C系列语言的效率更高
不能直接在java中获取到 2ExtClassLoader#
java编写派生于ClassLoader类
父类加载器为启动类加载器
java.ext.dirs系统属性指定的目录下加载或者从jdk安装目录的jre/lib/ext子目录下加载类库如果用户创建的jar放在此目录也会又扩展类加载器加载
3AppClassLoader#
父类加载器为ExtClassLoader 负责加载环境变量或系统属性java.class.path指定路径下的类库
类加载是程序中默认的类加载器。java应用的类都是他来加载
4自定义加载器#
场景隔离加载类中间件不同的环境避免类的冲突实现隔离修改类加载的方式根据需要去加载非核心的类库扩展加载源从数据库等其他地方加载字节码防止源码泄漏对字节码文件进行加密在加载的时候解密
自定义的步骤继承ClassLoader 重写findClass() 读入二进制字节流
如果没有复杂的需求可以继承ClassLoader 子类URLClassLoader 不用编写findClass() 读入二进制字节流
5ClassLoader抽象类#
除了Bootstrap ClassLoader所有类加载器都是继承自ClassLoader
获取ClassLoader的途径 获取当前类的ClassLoader clazz.getClassLoader() 获取当前线程上下文的ClassLoader Thread.currentThread().getContextClassLoader() 获取系统的ClassLoader ClassLoader.getSystemClassLoader() 获取调用这的ClassLoader DriverManager.getCallerClassLoader()
3. 双亲委派机制#
面试
Java虚拟机对class文件进行按需加载当需要使用的时候才会把class文件加载到内存中生成class对象。而且加载某个类的class文件的时候Java虚拟机采用的双亲委派模式即把请求交由父类处理是一种任务委派模式 避免类的重复加载。
保护程序的安全防止核心的api被随意的窜改
沙箱安全机制对核心api的保护
两个class对象是同一个类的条件
包名、全类名一致classLoader也要一样
类的主动使用和被动使用会不会初始化 调用了clinit()与否
三、运行时数据区#
running data area
内存是CPU和硬盘的中间仓库和桥梁
内存的申请、分配、管理不同的JVM是有区别的JRocket就没有方法区
class loader subsystem -running data area - execute engine 运行时数据区有些会随着的虚拟机进程启动而创建随着虚拟机退出而销毁。另外一些则是与线程一一对应这些与县城对应的数据区会随着线程开始和结束而创建和销毁
每个线程独立包括程序计数器、栈、本地栈
一个虚拟机实例一个进程线程间共享堆、堆外内存永久代或元空间就是方法区的实现名称是同一个东西、代码缓存 Runtime实例 相当于running data area一个虚拟机实例就只有一份Runtime实例
虚拟机栈、方法区、堆 三大重点 本地方法栈和程序计数器次重要。
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行执行。在HotSpot里每个线程都与操作系统的本地线程直接映射当一个Java线程准备好执行以后此时一个操作系统的本地线程也同时创建Java线程执行终止后本地线程也回收。释放相应的资源。
操作系统负责所有线程的安排调度到任何一个可用的CPU上一旦本地线程初始化成功他会调用Java线程的run()方法
守护线程和普通线程。如果程序中最后一个非守护线程结束JVM的进行也就结束了。
jconsole
虚拟机线程JVM到大安全点才会出现。
周期任务线程 周期事件的体现比如终端一般用于周期性操作的调度执行。
GC线程 守护线程 垃圾收集
编译线程 将字节码编程成本地代码
信号调度线程 接收信号并发送给JVM在内部通过适当方法进行处理
1. 程序计数器#
PC 寄存器 programer counter register 源于CPU的寄存器寄存器存储指令相关的线程信息CPU只有把数据装载到寄存器才能够运行。
也称为程序钩子JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
用来存储指向下一条指令的地址即将要执行的指令代码由执行引擎读取下一条指令
是一块很小的内存空间。几乎可以忽略不计运行速度最快的存储区域。每个线程也有自己的程序计数器、线程私有的用来存储程序执行位置的信息生命周期与线程的生命周期保持一致。
任何时间一个线程都只有一个方法在执行也就是所谓的当前方法程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。或者如果是在执行Native方法则是未指定值undefined调用C语言的时候无法获取。 唯一一个没有OutOfMemoryError的区域。没有GC和OOM
StackArea没有GC。 Method Area 和Heap Area 都有GC。三者都有OOM
程序控制流的指示器分支、循环、跳转、异常处理、线程恢复等基础功能需要依赖计数器完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 1、使用PC寄存器存储字节码指令地址有什么用
因为CPU需要不停的在各个线程之间切换。切换回来时候需要知道从哪接着开始继续执行。
JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行怎么样的字节码命令。
CPU并发执行多个线程。
2、为什么PC计数器是线程私有的。
因为要知道某个线程执行的位置。当然不能共享PC计数器。每一个PC计数器需要单独记录执行到位置的指令地址值。
CPU时间片轮限制。任何一个确定的时刻一个处理器或者多核处理器的一个内核都只会执行某一个线程中的一条指令
这样必然导致经常中断或恢复。因此某个线程在创建之后都会产生自己的程序计数器和栈帧使得各个线程之间不会相互影响。
并发一个核快速切换并行串行同一个时间点多个线程同时执行。
2. 虚拟机栈#
1概念#
java virtual machine stack
栈解决程序的运行问题即程序如何执行或者说如何处理数据堆解决的是数据存储的问题数据放哪怎么放。
每一个线程创建都会创建一个虚拟机栈线程私有的内部保存一个栈帧 stack frame对应一次java方法调用
生命周期和线程一致。
保存方法的局部变量8种基本数据类型引用数据类型的地址、部分结果并参与方法的调用和返回 局部变量 vs 成员变量 基本数据变量 vs 引用类型变量
栈的访问速度仅次于 PC寄存器
每个方法执行伴随着进栈执行结束后出栈工作。不存在垃圾回收问题只有内存溢出问题OOM
开发中的遇到的异常有哪些
Java栈的大小可以是动态的或者固定不变的。
如果是固定大小的虚拟机栈每一个线程的虚拟机栈容量在线程创建的时候独立选定。如果线程请求分配的栈容量超过虚拟机栈允许的最大容量。虚拟机栈会抛出 StackOverFlowError如果是动态扩展的并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程的时候没有足够的内存区创建对应的虚拟机栈抛出OutOfMemoryError
在VM options中设置参数-Xss设置线程的最大栈空间可以请求的最大内存空间栈的大小直接决定了方法调用的深度
-Xss1m
-Xss1024k
-Xss10485762栈的存储单位 栈帧#
以栈帧为基本单位线程上正在执行的每个方法都对应一个栈帧 一一对应
栈帧是一个内存区块是一个数据集维系着方法执行过程中的各种数据信息。
类中的基本结构field method
一条活动的线程上一个时间点上只有一个活动栈帧。当前正在执行的栈帧称为当前栈帧对应的方法就是当前方法方法所属的类就是当前类。执行引擎运行的字节码指令只针对当前栈帧进行操作。PC计数器也是指向当前栈帧的地址。如果在该方法中调用了其他方法就会创建新的栈帧成为栈顶成为新的当前帧
不同线程的栈帧是不允许存在互相引用的不可能在一个栈帧中引用另一个线程的栈帧。因为栈是线程私有的
如果当前方法调用了其他方法方法返回之际当前栈帧后传回此方法的执行结果给前一个栈帧接着虚拟机会丢弃当前栈帧使得前一个栈帧重新成为当前栈帧。
返回函数有两种方式一种是正常的函数返回使用return指令另外一种是抛出异常。不管使用哪种方式都会导致栈帧被弹出。
调用方法进栈。执行结束弹栈
3栈帧的内部结构#
局部变量表 local variables
操作数栈 operand stack 表达式栈
动态连接 dynamic linking 指向运行时常量池的方法引用
方法返回信息 return address 方法正常退出或者异常退出的定义
一些附加信息
栈帧的大小由其内部结构决定。
① 局部变量表 local variables
又称局部变量数组本地变量表
数字数组用于存储方法参数和定义在方法体内部的局部变量。这些数据类型包括基本数据类型最终都会转换为int存储、对应引用refrence 以及return address的类型异常、还是成功
线程私有的不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的。并保存在code属性中的maxium local variables 数据项中在方法运行期间是不会改变局部变量表的大小的。在编译的时候确定 参数和局部变量越多局部变量表膨胀栈帧越大能嵌套调用的方法次数的减少。
局部变量表只在当前方法调用中有效。在方法执行时虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后随着方法栈帧的销毁局部变量表也会随之销毁。 参数值的存放 在局部变量数组的index0开始到数组长度-1的索引结束
局部变量表最基本的存储单元是slot变量槽
局部变量表中存放编译器可知的各种基本数据类型8种引用类型referenceretrun address类型的变量
在局部变量表里32位以内的类型只占用一个slot包括returnAddress64位的类型long double 8bytes占用两个slot 构造方法和实例方法非静态的方法有this的局部变量而静态方法是没有的所以在静态方法中是不能使用this的。构造器也是有this局部变量的可以调用当前对象的其他构造器。 this 关键字
this refers to the current object.
Each non-static method runs in the context of an object. So if you have a class like this:
public class MyThisTest {private int a;public MyThisTest() {this(42); // calls the other constructor // 调用其他的构造器}public MyThisTest(int a) {this.a a; // assigns the value of the parameter a to the field of the same name}public void frobnicate() {int a 1;System.out.println(a); // refers to the local variable aSystem.out.println(this.a); // refers to the field aSystem.out.println(this); // refers to this entire object}public String toString() {return MyThisTest a a; // refers to the field a}
}Then calling frobnicate() on new MyThisTest() will print
1
42
MyThisTest a42So effectively you use it for multiple things:
clarify that you are talking about a field, when theres also something else with the same name as a fieldrefer to the current object as a wholeinvoke other constructors of the current class in your constructor Slot是可以重复利用的。如果一个局部变量过了其作用域在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位从而达到节省资源的目的。
前面的局部变量已经销毁了后面定义的局部变量可以使用前面的索引的slot 静态变量和局部变量的对比 基本数据类型 和 引用数据类型 在类中声明的位置 成员变量在使用前都经历过默认初始化赋值。 类变量静态变量 prepare中进行默认赋值 - initial阶段给类变量显式赋值或者静态代码块赋值实例变量非静态变量随着对象的创建会在堆空间中分配实例变量空间并进行默认赋值局部变量在使用前必须要进行显式赋值否则都无法编译。参数是通过调用方法进行赋值的 栈帧中与性能调优关系最为密切的就是局部变量表在方法执行的时候虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点只要局部变量表中直接或间接引用的对象都不会被回收。
② 操作数栈 operand stack
栈可以使用数组或者链表来实现。操作数栈使用数组存储顺序表或者是链表
在方法执行的过程中根据字节码指令往栈中写入数据或提取数据即入栈push、出栈pop
某些字节码指令将值压入操作数栈其余的字节码指令操作数取出使用它们后再把结果压入栈。
比如执行复制、交换、求和 operant(被运算的对象)
把前两个pop出来执行iadd后再push回去。如果被调用的方法带有返回值的话其返回值将会被压入当前栈帧的操作数栈中并更新PC寄存器中下一条需要执行的字节码指令。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。这有编译器在编译期间进行验证同时在类的加载过程中的类检验阶段的数据流分析阶段要再次验证。
我们所说的Java虚拟机的执行引擎是基于栈的执行引擎其中的栈指的就是操作数栈 用于保存计算过程中的中间结果同时作为计算过程中变量临时存储空间
操作数栈就是JVM执行引擎的一个工作区当一个方法开始执行的时候一个新的栈帧也会随之被创建出来此时操作数栈是空的。
因为操作数栈是一个数组每一个操作数栈都会拥有一个明确的栈深度用于存储数值其所需的最大深度在编译期就定好了怎么在编译期间就确定的保存在方法的code属性中为max_stack的值
栈中的任何一个元素都可以是任意的java数据类型
32bit占用一个栈单位深度
64bit占用两个栈单位深度
public void sum(){byte i 15;int j 8;int k i j;
}public void getsum(){int i sum();int j 10;
}代码追踪 在getSum对应的栈帧当中
静态方法的局部变量表的索引为0 的地方放的this 局部变量
左边是字节码文件bytecode中对应的指令。通过PC寄存器来控制执行的顺序
bipushb表示是byte根据数据的大小而变化 b s i会先把局部变量i的值push 到操作数栈中在istore_1 把 i 的值放到局部变量表中索引为1的位置然后操作数栈就pop了。
然后又bipush j的值 8 到操作数栈中索引为零的位置只有一个数栈顶接着又存到局部变量表
iload_1,iload_2 就是取出局部变量表中索引为1和2的数据。放到了操作数栈当中此时栈中就有815两个数据接下来执行 iadd 这些指令都是执行引擎做的把字节码指令翻译成机器指令把815相加后的值23压入操作数栈当中
在istore_3中再讲23存在局部变量k当中并且从操作数栈中弹出。
方法没有返回值。return直接就结束了
操作数栈的深度stack为2locals 局部变量表的变量数量为4。 在testGetSum()对应栈帧中
先aload_0加载当前类的对象 this
然后invokevirtual 调用当前对象中的getSum()方法。istore存到局部变量表中
在操作数栈中push10存放到局部变量表中的索引位置2
return结束
面试题 i 和 i的区别
i 是先自增后赋值
i 是先赋值后自增public void add(){//第1类问题int i1 10;i1;System.out.println(i1);//11int i2 10;i2;System.out.println(i2);//11//第2类问题int i3 10;int i4 i3;System.out.println(i4);//11int i5 10;int i6 i5;System.out.println(i6); //10//第3类问题int i7 10;i7 i7;System.out.println(i7); //10int i8 10;i8 i8;System.out.println(i8); //11//第4类问题int i9 10;int i10 i9 i9;System.out.println(i10); //22}iinc 2 by 1 就是把局部变量表中索引为2的变量值加上1。此处需要重点注意的是iinc自增指令直接对局部变量表的元素进行累加而不是在栈中。
iinc指令它是int类型的局部变量自增指令将后者数据加到前者下标的int类型局部变量中)
第一类问题二者完全是一致的
//第1类问题int i1 10;i1;int i2 10;i2;第二类问题因为iinc是直接在局部变量表中进行操作的因此前者就是store了load在操作数栈中未加一的10而第二种情况则是load把局部变量表中的值又重新load了一遍因此就是加了1的值 int i3 10;int i4 i3;int i5 10;int i6 i5;第三类问题和第二类情况是一致的。 //第3类问题int i7 10;i7 i7;int i8 10;i8 i8;第四类问题 10 12 前面先load了10 i9 自增1 这时候局部变量表中已经是11了i9又加一这时候就是12了执行加法的时候就是12了。 //第4类问题int i9 10;int i10 i9 i9; // 10 12System.out.println(i10); //22栈顶缓存技术
零地址指令。更加紧凑指令集小完成一个操作需要使用更多的指令内存读写的次数多因此效率也不如基于寄存器的架构。因此为了提高效率出现了栈顶缓存技术 top-of-stack caching将栈顶元素全部缓存到物理CPU的寄存器中以此降低对内存的读写次数提升执行引擎的执行效率。
③ 动态链接 dynamic linking
方法返回地址、动态链接其他附加信息也会被称为帧数据区。
每一个栈帧内部都包含着一个指向运行时常量池中该栈帧所属的方法的引用。包含这个引用的目的就是为了支持当前方法的代码可以实现动态连接。比如invokedynamic指令
在java源文件被编译到字节码文件中时所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如描述一个调用了其他方法时就是通过常量池中指向方法的符号引用来表示的。动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。 在栈帧中的动态链接引用到方法区常量池运行起来就放到了方法区 执行引擎运行方法的时候字节码中的常量池会被加载到运行时常量池。
为什么需要常量池常量池在字节码文件中运行起来在方法区中的就是运行时常量池
为了提供一些符号和常量便于指令的识别字节码文件比较小(?)
方法的调用
在JVM中把Java方法调用的符号引用转换为直接引用和方法的绑定机制直接相关。
静态链接当一个字节码文件被将在进JVM内部时如果被调用的目标方法在编译期可知且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程成为静态链接。
动态链接
如果被调用的方法在编译器无法被确定下来。也就是说只能在程序运行期将调用方法的符号引用转换为直接引用由于这种引用转换过程具备动态性因此被称为动态链接。
方法的绑定机制绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程这仅发生一次
早期绑定早期绑定就是指被调用的目标方法如果在编译器可知且运行期保持不变时即可将这个方法与所属类型进行绑定这样一来由于明确了被调用的目标方法究竟是哪个因此也就可以使用静态链接的方法将符号引用转换为直接引用。
晚期绑定被调用的方法在编译期无法被确定下来只能在程序运行期根据实际的类型绑定相关的方法这种绑定方式成为晚期绑定。多态性就需要晚期绑定
Java中的任何一个方法都具有虚函数的特征如果不希望其具有虚函数的特征则需要使用final来标记这个方法。 final 关键字 final类不能被继承没有子类final类中的方法默认是final的。final方法不能被子类的方法覆盖但可以被继承。final成员变量表示常量只能被赋值一次赋值后值不再改变。final不能用于修饰构造方法。 虚方法vs非虚方法
非虚方法静态方法、私有方法、final方法、实例构造器、父类方法。 在编译器就确定了具体的调用版本这个版本在运行时是不可变的就成为非虚方法。方法都是不能被重写的。
其他的方法都是虚方法。对应着多态性。调用父类的方法的时候可能实际上调用的是子类中的方法所以在编译期间是无法确定的实际上调用的谁因此是虚方法需要晚期绑定和动态链接 子类对象的多态性的使用前提 类的继承关系。 方法的重写。调用的是父类的方法实际运行的是重写的方法接口 调用方法的指令
普通调用指令
invokestatic 调用静态方法解析阶段确定唯一方法版本。invokespecial 调用init方法 私有及父类方法不包括父类的静态方法解析阶段确定唯一方法版本invokevirtual 调用所有的虚方法如果是final修饰的虽然使用invokevirtual 指令调用但是实际上是非虚方法invokeinterface 调用接口中的方法虚方法。在调用的时候不知道实现类中具体是怎么实现的
动态调用指令
invokedynamic 动态解析出需要调用的方法然后执行
前四条指令固化在虚拟机内部。方法的调用执行不可人为干预而invokedynamic指令则指出用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法其余的非final修饰的成为虚方法。
invokedynamic 动态类型语言。知道Java8的lambda表达式invokedynamic 指令在java中才有了直接的生成方式。 动态类型语言 运行期间 动态类型语言是判断变量值的类型信息变量没有类型信息变量值才有。JavaScript、Python 静态类型语言 编译期间 静态是陈判断变量自身的类型信息。 Java 区别在于对于类型的检查是在编译期间还是在运行期间进行的。 增加动态语言类型本质是对java虚拟机规范的修改而不是对java语法规则的修改。最直接的受益者是运行在java平台上的动态语言使用jvm的其他动态编程语言的编译器。
方法重写的本质。先去找子类重写的方法如果有进行权限的验证如果通过则返回这个方法的直接引用。如果没有去找父类。
虚方法表为了提高效率如果调用过就会在虚方法表中建立对应关系。不用每次都去找这个方法是在哪个类子类 父类 的继承树上中实现的虚方法表是在类加载的时候Linking步骤中的resolve阶段创建的。
④ 方法返回地址 return address
存放的是该方法的PC寄存器的值把值给返回给执行引擎让其继续执行。
调用的方法结束正常结束异常退出回到该方法的调用位置正常退出的时候调用者的PC寄存器的值作为返回地址即调用该方法的指令的下一条指令的地址而异常退出的返回地址是要通过异常表不再有方法返回地址来确定栈帧中一般不会保存这部分信息。
方法在正常调用完成之后使用哪个返回指令还需要根据方法返回值的实际数据类型而定。
ireturn(boolean byte char short int)
lreturn long
freturn float
dretrun double
areturn 引用
return void返回的方法 实例初始化方法 类和接口的初始化方法 静态代码块
异常退出如果进行了catch的则会在本方法的异常处理表中去匹配异常处理器如果没有且没有catch的在本方法的异常表中没有搜索到匹配的异常处理器就会导致方法退出。
方法的退出实际上是当前栈帧出栈的过程此时需要恢复上层方法的局部变量表、操作数栈、将返回值不是方法返回地址是方法调用产生的返回值压入调用者栈帧的操作数栈设置PC寄存器的值等让调用者方法继续执行下去。
异常退出的不会给他的上层调用者产生任何的返回值
⑤ 附加信息
携带虚拟机实现相关的信息如对程序调试提供支持的相关信息。 虚拟机栈的相关面试题
1、栈溢出的情况 StackOverFlowError 调整栈的大小也不能保证不溢出
通过-Xss设置栈的大小。OOM out of memory
2、垃圾回收是否会涉及到虚拟机栈吗不会虚拟机栈及本地方法栈只有OOM不会垃圾回收。就pop出栈不会有显式的垃圾回收。程序计数器既没有OOM也不存在GC。
方法区和堆二者都有。
Out of Memory ErrorGarbage ColletionPC registernononative method stackyesnoJava vm stackyesnoheapyesyesmethod areayesyes
3、分配的栈内存不是越大越好。
4、方法中定义的局部变量是否是线程安全的不一定
如果只有一个线程在操作此数据。则是线程安全的。如果传入的参数在其他的线程中也在被操作。就会有线程不安全的问题。
主要看这个局部变量是否在方法方法内部消亡。如果不是内部产生的或者内部产生再返回给外部都有可能出现线程不安全。
3. 本地方法栈#
native method stack
用于管理本地方法的调用。
线程私有的。
允许被实现为固定或者是可动态扩展的内存大小。
在本地方法栈去登记本地方法。在execution engine执行时加载本地方法库。
当某一个线程调用一个方法时它就进入一个全新的不受虚拟机限制的世界和虚拟机拥有同样的权限。
本地方法可以通过本地方法接口访问虚拟机内部的运行时数据区。
设置可以直接使用本地处理器中的寄存器直接从本地内存的堆中分配任意数量的内存。
并不是所有的JVM都支持本地方法。JVM规范并没有规定语言、实现方式、数据结构
在HOTSPOT JVM中直接将本地方法栈和虚拟机栈合二为一。 本地方法接口 Native Method Interface(JNI)
JNI Java Native InterfaceJava本地接口是一种编程框架使得Java虚拟机中的Java程序可以调用本地应用/或库也可以被其他程序调用。 本地程序一般是用其它语言C、C或汇编语言等编写的并且被编译为基于本机硬件和操作系统的程序。
Java调用非Java代码一个Native Method该方法的实现不是由Java语言实现的而是使用C或C实现的。这个特征不是Java所特有的。很多其他的编程语言也有这一机制。比如在C中可以使用extern C 告知编译器去调用C的函数。
融合其他语言。
Java底层系统交互不如C语言。Native和public、static、synchronized等修饰符都是可以共同使用的和abstract不能共存。 native 关键字可以应用于方法以指示该方法是用Java以外的语言实现的方法对应的实现不是在当前文件而是在用其他语言如C和C实现的文件中。。 Java不是完美的Java的不足除了体现在运行速度上要比传统的C慢许多之外Java无法直接访问到操作系统底层如系统硬件等)为此Java使用native方法来扩展Java程序的功能。 可以将native方法比作Java程序同程序的接口其实现步骤 、在Java中声明native()方法然后编译 、用javah产生一个.h文件 、写一个.cpp文件实现native导出方法其中需要包含第二步产生的.h文件注意其中又包含了JDK带的jni.h文件 、将第三步的.cpp文件编译成动态链接库文件 、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件这个native()方法就可以在Java中被访问了。 JAVA本地方法适用的情况 1、为了使用底层的主机平台的某个特性而这个特性不能通过JAVA API访问 2、为了访问一个老的系统或者使用一个已有的库而这个系统或这个库不是用JAVA编写的 3、为了加快程序的性能而将一段时间敏感的代码作为本地方法实现。 原因
1、需要与Java外面的环境交互。与底层系统操作系统、硬件交换信息。本地方法正是这是一种交流机制提供了一个非常简洁的接口无需去了解Java应用之外细节的实现。效率更高
2、操作系统都是c like的语言写的。JVM的一些部分都是C写的。
3、Sun的解释器是用C实现的
4. 堆 heap#
1堆空间的结构#
方法区和堆是一个进程对应一个JVM实例中唯一的。
在JVM启动的时候就被创建同时其空间也是确定的。是JVM管理的最大的一块内存空间也是最重要的一块空间。
堆内存的大小是可以调节的。
堆可以处于物理上不连续的空间但是在逻辑上是连续的。
所有的线程共享java的堆但是在这里可以划分线程私有的缓存区不会有线程安全问题。thread local allocation buffer TLAB
几乎所有的对象实例及数组都应该在运行时分配在堆上。
数组和对象可能永远不会存储在栈上因为栈帧中只保存引用这个引用指向对象、数组在堆中位置。
逃逸分析栈上分配。标量替换
在方法结束后堆中对象不会被马上移除仅仅在垃圾收集的时候才会被移除
堆是GC执行垃圾回收的重点区域。 方法区中是类及方法的结构。
堆空间不足。GC来判断堆中的实例是否有来自栈中的指针。在一定时间后才回收。防止过高的GC频率。不是栈把局部变量pop出去就开始回收的。
new的时候在堆空间中开辟内存空间初始化实例变量。 元空间是方法区的一部分。
-Xms 用来设置堆空间初始内存大小只包括新生代和老年代
-X JVM运行参数 memory start
-Xmx 堆空间最大的内存 memory max
默认的堆空间的大小。
初始物理电脑内存/64
最大物理内存/4
运行时数据区是单例的。
初始的内存大小和最大的内存大小一般设置为一致的。因为GC的时候不用重新划分空间效率更高。减少GC频繁的扩容和释放。
s0from和s1(to)区中总有一片空间的是空的垃圾回收需要
OOM内存溢出
2年轻代与老年代#
Java中的对象有生命周期的长短。有些对象和JVM生命周期一致。在老年代中
有些迅速的消亡的对象就在年轻代。 old/young 的比例是 NewRatio
-XX:SurvivorRatio 8
-XX:-UseAdaptiveSizePolicy 自适应内存分配策略。-是关闭 是开启。
实际上是611 不是811
-Xmn 设置新生代的空间的大小。优先于NewRatio 但是一般不设置
几乎所有的Java对象都是在Eden区中new出来的。
新生代中的80%的对象都是声明周期极短的。 3内存分配#
内存如何分配和在哪里分配。以及垃圾回收都是密切相关的。
对象先分配到Eden区当满了的时候触发YGC (YoungGC) 判断哪些是垃圾,如果还有被占用的就会被放到S0
年龄计数器每个对象都有在进行一次YGC的时候就加一。 to区就空的。from区是非空的survivor区。Eden中还被占用的对象会被放到空的S1区S0中的对象也会进行判断。如果还在被使用的也是要移到S1。这时候AGE就又增加1了。 当年龄计数器达到thresholdpromoted to Tenured YGC只会在Eden区满的时候才会触发。
YGC在触发的时候会同时回收Eden和Survivor。
如果一些特殊情况。Survivor满了会在没有达到threshold的后就被promote to tenure
-XX:MaxTenuringThrethold 15 默认值
复制算法。谁空谁是TO
GC频繁在YoungGen 中进行。很少在Tenured中进行 几乎不在MetaSpace中进行。 FGC full GC
JVisualVM 常用调优工具。 JProfiler等。
4GC#
YGC和Minor GC是完全一样的。
Full GC 和 Major GC
GC线程会导致用户线程的暂停。调优的目的就是减少GC的频率
Full GC 和 Major GC的暂停时间是YGC的10倍 MinorGC只有在Eden区满的时候才会触发。Survivor区是被动的。不会主动触发MinorGC
MinorGC频率很高。
会导致STWstop to World 暂停其他用户线程。等待垃圾回收结束。用户线程才会恢复。
Major GC 通常会伴随MinorGC 时间慢10倍以上。如果Major GC后还不行就OOM
Full GC 整堆回收。 调用System.gc() 建议执行 不是必然执行 老年代空间不足 方法区空间不足 通过Minor GC 进入老年代的平均大小大于老年代的可用内存 Eden区、from space 到 to space 。大于to space 。转存到老年代。老年代空间也放不下
full gc是开发中尽可能避免的。暂停时间长。
ps process status 进程状态
分代的目的就是为了优化GC的性能。
内存分配策略
优先分配到Eden
大对象直接分配到老年代。尽量避免过多的大对象
长期存活的对象分配到老年代
动态对象年龄判断如果Survivor区中相同年龄的所有对象大小的总和大于等于Survivor空间的一半年龄大于该年龄的对象可以直接进入老年代。不用达到阈值。减少反复的复制。
空间分配担保 -XX:HandlePromotionFailure。老年代为survivor提供空间的担保。
5TLAB#
thread local allocation buffer
堆区是线程共享的。由于对象的实例的创建非常频繁。在并发的环境下会出现线程不安全。
为避免多个线程操作同一个地址。使用加锁机制会影响的效率。
在Eden区为每个Thread分配一个私有的缓存区域。
快速分配策略。
只占Eden空间的1%
设置参数 -XX:UseTLAB 默认情况是开启的。 6参数设置#
-XX:PrintFlagsInitial 查看所有的参数的初始默认值
-XX:PrintFlagsFinal 查看参数的最终值
-Xms 初始堆空间
-Xmx 最大堆空间
-Xmn 新生代
-XX: NewRatio 老年代空间/新生代
-XX:SurvivorRatio eden/s0
-XX:MaxTenuringThrethold 新生代垃圾的最大年龄
-XX:PrintGCDetails GC日志
-XX:HandlerPromotionFailure 设置空间分配担保 如果老年代的连续空间大于新生代对象总大小或者历次晋升的对象的总大小。则进行MinorGC 否则就FullGC 堆是分配对象的唯一选择吗
JIT编译器的发展。逃逸分析技术发现一个对象并没有逃逸出方法那么就可能被优化成栈上分配
栈上分配。标量替换优化技术在栈上分配就不用进行垃圾回收。
逃逸分析。能使用局部变量的就不要在方法外定义。没有发生逃逸的可以使用一下优化的策略
栈上分配
标量替换
同步省略。 -XX:EliminateAllocation 开启标量替换
-XX:DoEscapeAnalysis 开启逃逸分析
目前还不是特别成熟。
5. 方法区 method area# 虚拟机启动的时候会创建堆、栈、方法区。
逻辑上方法区也被视为堆的一部分但一些简单的实现是不会进行垃圾回收和压缩
物理上可以是不连续的内存逻辑上的是连续的
Hotspot方法区还有一个别名叫做Non-heap所以可以看做是独立于堆的内存空间
各个线程共享的区域。方法区的大小和堆一样可以选择固定大小和可扩展
方法区的大小决定了系统可以保存多少个类。如果系统定义了太多的类大量的第三方jar包、tomcat部署的工程过多动态的生成反射类。导致方法区溢出OOM metaspace
虚拟机关闭的时候内存被释放
方法区可以视作接口。元空间和永久代可以视作实现。以后都是叫metaspace了。
方法区和永久代不是等价的仅对Hotspot二者可以视为等价的。
是在虚拟机的内存中使用更容易导致OOM错误。Jdk8以后 在本地内存中实现Metaspace 永久代不再使用虚拟机设置的内存。
元空间的内部结构也做了一些调整。
JDK7
-XX:PermSize
-XX:MaxPermSize
JDK8
-XX:MetaspaceSize 依赖于平台。21M windows平台下。
-XX:MaxMetaspaceSize -1 无限制
一旦触及这个水位线。FullGC将会触发卸载没用的类即这些类对应的类加载器不再存活然后这个高水位线会被重置。新的高水位线取决于GC后释放了多少元空间。如果释放的空间不足那么在不超过Max的情况下会适当提高该水平线。如果释放的空间过多则会适当降低。
为了减少FullGC通常设置为一个较高的值。
OOM异常或者heap space 。dump出堆转储快照进行分析。重点是确认内存中的对象是否必要。要区分清楚是内存泄漏还是内存溢出
方法区存储的信息类型信息域信息、方法信息、常量、静态变量、即时编译器JIT编译后的代码缓存、类的加载器信息、运行时常量池
类型信息class interface enum annotation JVM必须在方法区中存储一下类型信息
类型完整有效名称 全名 包名.类名类型直接父类完整有效名除了interface和java.lang.Object类型的修饰符public abstract final类型直接接口的一个有效列表
域信息Field成员变量
field nameclass修饰符 public 等
方法信息 Method
方法名称方法参数的数量和类型方法的修饰符方法的字节码、操作数栈、局部变量表的大小 abstract 、native除外返回值类型异常表 abstract 、native除外。每个异常处理的开始位置、结束位置、代码处理在程序计数器的位置。被捕获的异常类的常量池索引
常量 static final 每个全局常量在编译的时候就会被分配其值。不同于静态变量是在类加载的时候加载的静态变量在prepare、initialization阶段初始化。
静态变量 类变量随着类的加载而加载被所有的类的实例共享。即使没有类的实例也能访问它。
JIT编译后的代码
都会从字节码文件中加载到方法区当中。
运行时常量池。字节码文件中有常量池Constant Pool 加载到方法区中。就城变成了运行时常量池。
classfile
magic cafebaby 为什么需要常量池。在动态链接的时候会用到运行时常量池。类似于C里的动态链接库。这些是需要反复复用的东西。加载在常量池的时候所有的类在加载的时候就指向常量池。解耦、模块化的一种。
常量池中存储的数据类型
数量值 不是在栈中吗字符串值类引用字段引用方法引用
常量池可以视作一张表。虚拟机指令根据表找到要执行的类名、方法名、参数类型、字面量等数据
运行时常量池是方法区的一部分。常量池表是字节码文件的一部分这部分内容加载后放到方法区的运行时常量池。
JVM为每个已加载的类型类或接口都维护一个常量池池中的数据像数组项一样通过索引访问。
运行时常量池包括编译期就已经明确的数值字面量、也包括运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了这里转换为真实地址。相当于传统语言的Symbol table只不过存储的数据更丰富。 类加载之后常量池的内容会进入运行时常量池这时候里面的数据也许还保持着符号引用。 因为解析的时机由JVM自己设定 如果在虚拟机栈的 栈帧中我准备调用 main() 函数那么会通过栈帧中持有的动态连接找到运行时常量池 然后找到main函数的常量 比如 #2 如果这个常量没有被解析过那么就通过这个常量进行解析过程 其中包括通过常量 找到 类名 和 nameAndType通过 nameAndType 找到方法名和返回值。 这时候 我手里有 类名/方法名/方法返回值下一步我通过类名和方法名通过JVM记录的方法列表找到对应的方法体。 而这个方法体实际上是一段内存地址那么这时候我就把这段内存地址复制给 #2并且给 #2设定一个已经解析的 flag。 这样就完成了 符号引用到直接引用的过程。 虚拟机栈是一个大的栈里面存放的单位是栈帧。操作数栈是虚拟机栈里栈帧的一部分是另外一个栈存放操作数。
方法区的演进细节 为什么使用要使用元空间替换永久代
永久代设置空间大小是很难确定的。如果动态加载的类太多容易产生OOM
元空间并在不在虚拟机中而是使用本地内存因此默认情况下元空间大小仅受本地内存限制。元空间的最大内存没有进行限制默认情况下
对永久代进行调优是很困难的
为什么要把静态变量和StringTable放到堆老年代里呢
因为永久代的GC效率比较低。因为fullGC只有在老年代空间不足的时候才进行回收。String比较少回收。
new的对象始终是放在堆空间的。但是静态变量是放在老年代中。
非静态的成员变量是随着类的实例方法在堆空间中方法中的局部变量是放在栈帧中的局部变量表里。
而静态变量是存放在堆空间的老年代里JDK6及以前是放在方法区的
虚拟机规范中对方法区的约束比较松散一些简单的实现可以不用实现垃圾回收和压缩GC的碎片压缩
hotspot有方法区的GC主要涉及一下两部分内容 废弃的常量。包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。如果没有引用指向常量池中的常量就可以被回收。 不再使用的类型类型的卸载条件比较苛刻 类所有的实例被回收了以及其所有的子类的实例 加载类的类的加载器也被回收了。 该类对应的java.lang.Class对象没有任何引用。 只有满足这些才有可能被卸载。
运行时常量池不包括字符串常量池和类型信息类、方法、域、JIT编译后的代码、静态变量
常量池主要存放两大类的常量字面量final常量值、文本字符串等和符号引用编译原理方面的概念 一个程序运行的流程 编译成字节码。类加载子系统加载进入虚拟机 加载的过程在一个方法里new 对象局部变量在栈中引用指向堆里的实例。堆中的分区。进行垃圾回收方法区中加载了类的信息、运行时常量池 四、对象的实例化、内存布局与访问定位#
1. 对象的实例化#
对象创建的方式
new() 直接new单例模式的获取实例的静态方法XxxBuilder/FactoryClass的newInstance()。只能调用空参的构造器。权限必须是Public的Constructor的newInstance() 也是反射。可以调用空参的或者是带参的构造器对权限也没有要求。jdk8以后替代上面的clone() 不调用任何构造器。但是要实现Cloneable接口实现Clone() 浅拷贝使用反序列化。从文件、网络获取一个对象的二进制流第三方库Objenesis
创建对象的步骤
判断对象对应的类是否加载、链接、初始化 new的指令去常量池中定位到到一个类的符号引用。并且检查这个类对应的符号引用是否已经被加载、链接初始化。如果已经有了直接加载类如果没有双亲委派机制模式下使用类加载器去找class文件进行加载如果没有就是ClassNotFoundException为对象分配内存 计算对象所需要的内存大小引用变量是4个字节是一个地址值。 如果内存规整 指针碰撞在空闲的内存和被占用的内存中有一个指针向空闲那边挪动如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的虚拟机采用这种分配方式一般使用带有Compact过程的收集器时使用指针碰撞如果不规整 虚拟机 需要维护一个列表空闲列表分配 CMS标记清除算法处理并发安全问题 堆空间是线程共享的可能存在创建对象抢空间的问题 CAS失败重试区域加锁每个线程预先分配一块TLAB初始化分配到的空间 所有属性设置默认值。保证对象字段在不赋值的情况下可以直接使用设置对象的对象头 对象所属的类、HashCode 、GC信息、锁信息等数据执行init方法。 调用类的构造方法、并把堆内对象的首地址值赋值给引用变量对象显式初始化
2. 对象的内存布局#
Java对象内存布局 3. 对象的访问定位#
栈帧中的局部变量存了堆空间中的对象的地址值。通过栈上的refrence访问到
访问的方法主要有两种 句柄访问 handler pool 里面存放了到对象的指针以及到类型的指针 空间浪费、效率低。但是栈上的引用时很稳定的。一旦对象进行了移动。指针就会修改。 直接指针Hotspot使用的。直接指向对象对象实体中的对象头指向类型。 直接从栈就能访问到对象的实体。当对象的地址改变之后需要修改
五、直接内存#
元空间使用的就是本地内存。Java堆外的直接向系统申请的内存区间
来源于NIO通过DirectByteBuffer直接操作本地内存。 直接内存效率更高直接从物理内存中进行物理层磁盘的读取。不需要从用户态切换到内核态。
直接内存访问速度高于Java堆。
分配回收成本较高
不受JVM内存回收管理
仍然还是会有OOM。系统内存的限制
六、执行引擎#
执行引擎概述
Java代码编译、执行过程
机器码、指令、汇编语言
解释器
JIT编译器 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面的。而虚拟机的执行引擎则是由软件自行实现的。虚拟机不受物理条件的限制能够执行那些不被物理层面支持的指令
执行引擎的任务是将字节码指令解释/编译为对应平台上的本地机器指令才可以。
执行引擎在执行的过程中需要执行什么样的字节码指令由程序计数器决定。去地址中执行指令。 方法压入栈帧之后解释执行。编译成汇编代码CPU和内存分配资源给进程。
下面的就是编译上面的是解释执行。 解释器对字节码采用逐行解释的方式执行。
JIT just in time compiler 将虚拟机源代码直接编译成和本地机器平台相关的机器语言。
为什么要二者都使用呢半编译半解释型语言。
JIT编译缓存在方法区当中。
机器码
指令
指令集 x86 ARM指令集
汇编语言。助记符mnemonics代替机器指令的操作码
汇编语言还需要翻译成机器指令码。
高级语言
解释器模板解释器把每一条字节码和一个模板函数相关联模板函数中直接产生这条字节码执行时的机器码
解释器和JIT并存。解释器响应速度快。JIT执行快。
JIT的热点代码探测功能翻译成本地机器指令存到方法区的缓存中。
AOT Ahead of time compiler
前端编译器源码到字节码
JIT Hotspot 的C1 C2 client 使用C1server使用C2。64位默认是server
C1 方法内联、去虚拟化、冗余消除
C2 标量替换 栈上分配 同步消除
AOT编译器 GNU compiler for the java /excelsior JET
热点代码及其探测方式。根据代码执行的频率。在方法的执行过程中进行栈上替换。on stack replacement OSR
方法调用计数器统计调用次数、回边计数器。
-Xint 仅适用解释器
-Xcomp 仅适用编译器
-Xmixed 混合模式 默认 JVM server client Mode
This is really linked to HotSpot and the default option values (Java HotSpot VM Options) which differ between client and server configuration.
From Chapter 2 of the whitepaper (The Java HotSpot Performance Engine Architecture): The JDK includes two flavors of the VM -- a client-side offering, and a VM tuned for server applications. These two solutions share the Java HotSpot runtime environment code base, but use different compilers that are suited to the distinctly unique performance characteristics of clients and servers. These differences include the compilation inlining policy and heap defaults. Although the Server and the Client VMs are similar, the Server VM has been specially tuned to maximize peak operating speed. It is intended for executing long-running server applications, which need the fastest possible operating speed more than a fast start-up time or smaller runtime memory footprint. The Client VM compiler serves as an upgrade for both the Classic VM and the just-in-time (JIT) compilers used by previous versions of the JDK. The Client VM offers improved run time performance for applications and applets. The Java HotSpot Client VM has been specially tuned to reduce application start-up time and memory footprint, making it particularly well suited for client environments. In general, the client system is better for GUIs. So the real difference is also on the compiler level: The Client VM compiler does not try to execute many of the more complex optimizations performed by the compiler in the Server VM, but in exchange, it requires less time to analyze and compile a piece of code. This means the Client VM can start up faster and requires a smaller memory footprint. The Server VM contains an advanced adaptive compiler that supports many of the same types of optimizations performed by optimizing C compilers, as well as some optimizations that cannot be done by traditional compilers, such as aggressive inlining across virtual method invocations. This is a competitive and performance advantage over static compilers. Adaptive optimization technology is very flexible in its approach, and typically outperforms even advanced static analysis and compilation techniques. Note: The release of jdk6 update 10 (see Update Release Notes:Changes in 1.6.0_10) tried to improve startup time, but for a different reason than the hotspot options, being packaged differently with a much smaller kernel.
七、String Table#
String的基本特性
内存分配
基本操作
字符串拼接
intern()的使用
StringTable的垃圾回收
G1中的String去重操作
八、垃圾回收#
1. 概要#
内存动态分配。垃圾收集技术
垃圾收集。Lisp语言就有了
三大问题
哪些内存需要回收什么时候回收如何回收
垃圾是运行程序中没有任何指针指向的对象。这个对象就是需要被回收的垃圾。
引用类型的才需要考虑回收。如果不进行垃圾回收会一直保留到应用程序结束空间会被占用。可能会导致内存溢出问题。
内存溢出vs内存泄漏
内存会被消耗完。
清理内存中的记录碎片。将整理出的内存分配给新的对象腾出连续的空间
没有GC就不能保证应用的程序的正常运行因为应用程序所应付的业务越来越大。
早期的垃圾回收。C/C需有手动申请释放如果忘记回收就会导致内存泄漏
自动化的内存分配和垃圾回收技术是现代高级语言发展的趋势。
Java的垃圾回收机制。
降低内存泄漏和内存溢出的风险。
更专注于业务开发。
弱化Java开发人员在程序出现问题时的问题定位和解决问题的能力。
进行必要的监控和调节。方便排查和定位解决问题。
只针对堆和方法区。
2. 垃圾回收算法#
哪些是垃圾怎么回收
标记阶段 - 清除阶段
1垃圾标记引用计数算法#
垃圾标记阶段。对象存活判断
有些JVM实现是不回收方法区的。
主要是针对堆中的对象。对象死亡没有任何指针引用对象就死亡了
Refrence Counting
为每个对象保存一个整型的引用计数器属性用于记录对象被引用的情况
优点实现简单垃圾对象便于辨识判断效率高回收没有延迟性怎么知道被引用或者不再被引用
缺点
需要单独的字段存储计数器增加了存储空间的开销每次赋值都需要更新计数器需要加减法增加了时间的开销严重的问题无法处理循环引用的情况。Java的GC没有使用该类算法 python就使用了引用计数算法。
手动解除使用弱引用weakref python标准库就可以解决循环引用
2垃圾标记可达性分析算法#
又称作根搜索算法、追踪性垃圾收集
可以解决循环引用。同样具备实现简单执行高效的优点。
Java C#都采用这种算法
GC Roots根集合就是一组必须活跃的引用。
以GC Roots为起点从上至下搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析算法后内存中的存活对象都会被根队形集合直接或间接连接着搜索所走过的路径称为引用链
如果目标对象没有任何引用链相连则是不可达的。就意味着该对象已经死亡可以标记为垃圾。
GC Roots包括以下几类元素
虚拟机栈中引用的对象 如各个线程被调用的方法中使用的从的参数局部变量本地方法栈中引用的对象方法区中类静态属性引用的对象 Java类的引用类型静态变量方法区中常量引用的对象 字符串常量池的引用所有被同步锁synchronized持有的对象Java虚拟机内部的引用基本数据类型对象的Class对象一些常驻的异常对象系统类加载器反应java虚拟机内部请清的JMXBean JVMTI 中注册的回调、本地代码缓存等
一个指针指向了堆内存里面的对象但是自己又不存放在堆内存里那它就是一个Root
临时性的对象可以加入Roots。如分代收集和局部回收。
分析工作必须在一个能保障一致性的快照中进行。所以需要STW来保持一致性。
即使是号称几乎不会发生停顿的CMS收集器枚举根节点时也是必须要停顿的。 对象的Finalization机制
允许开发人员提供对象被销毁之前的自定义处理逻辑
在垃圾回收器执行之前就会调用对象的finalize()方法在Object类中。可以重写该方法
一般用于在对象被回收时进行资源的释放比如关闭文件套接字和数据库连接。
不要主动去调用应该交给垃圾回收机制去调用
finalize()可能导致对象复活方法的执行时间没有保障完全由GC线程决定如果不GC该方法不会执行一个糟糕的finalize()会严重影响GC的性能
由于finalize()的存在。虚拟机中的对象一般又三种状态
可触及的 根节点可以到达的对象可复活的 对象的所有引用都被释放、但是可能在finalize()中复活不可触及的 对象的finalize()被调用并且没有复活那么就会进入不可触及状态finalize()只会被调用一次
一个对象是否可回收需要经历两次标记过程
没有GC Roots引用链进行一次标记判断此对象是否有必要执行finalize() 使用MAT和JProfiler进行GC Roots溯源
Eclipse Memory Analyzer Tool dump出堆快照在MAT中的分析
3垃圾清除标记清除算法 mark-sweep#
当堆中的内存空间耗尽STW 然后进行两项工作 第一项是标记第二项是清除
标记Collector从引用节点开始遍历标记所有被引用的对象。一般在对象的Header中记录为可达对象
清除发现某个对象在其Head中没有标记为可达对象则将其回收。 优点
简单
缺点
效率不算高。标记的时候递归遍历清除的时候遍历整个堆在进行GC的时候需要停止整个应用程序清理出来的空闲内存是不连续的会产生内存的碎片需要维护一个空闲列表
何为清除
并不是真的置空而是把需要清除的对象地址保存在空闲地址列表下次有对象需要加载的时候判断垃圾的位置空间是否够如果空间够就覆盖原有的数据。
4垃圾清除复制算法 copying#
Lisp 语言
将活着的内存空间分为两块每次使用其中一块在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中之后清除正在使用的内存块中的所有对象交换两个内存的角色最后完成垃圾回收。 就是Survivor区的设计。
优点
没有标记和清除过程实现简单运行高效不会有内存的碎片保证空间的连续性。使用指针碰撞为对象分配内存。
缺点
需要两倍的空间对于G1这种分拆成大量region的GC复制而不是移动。意味着GC需要维护region之间对象引用关系因为内存地址的变化从引用到对象的关系需要进行大量的调整。不管是内存占用还是时间开销都不小
另外如果系统中的垃圾对象很多复制算法需要复制的存活对象数量并不会太大。但是如果系统中都是存活的对象垃圾回收的效率就很低。
就像Eden区会有大量的生命周期很短的对象就很适合使用复制算法来进行垃圾回收。
5垃圾清除标记压缩算法 mark-compact#
也称为标记整理算法。
复制算法在老年代中就不太适合因为老年代中很多的对象的存活周期长垃圾少。
标记清除算法也可以使用在老年代中但是会导致内存碎片而老年代通常需要大的连续空间因此使用标记清除算法效率比较低。所以大部分现代的垃圾回收器使用的是标记压缩算法或其改进版本 在标记之后直接就把存活的对象压缩到内存的一段按顺序排放清理边界之外的所有空间。直接移动而不是复制。
等同于在标记清除算法的基础上加了一次内存整理。
标记清除算法是非移动式的维护一个空闲空间表。
而标记压缩算法是移动式的。
移动需要修改引用维护引用关系但是整理了空间不需要维护空闲列表。
优点
清理了内存碎片只保存一个内存的起始地址就可以不用复制算法的双倍空间
缺点
效率不如复制和标记清除算法移动对象还是需要调整引用的地址移动过程中需要STWSTW时长会稍微长一些
三个算法的对比 6分代收集算法#
没有一种完美的普适的算法
不同的对象的生命周期不同不同的生命周期使用不同的垃圾回收算法
目前几乎所有的垃圾回收算法都是使用分代收集算法的。
年轻代使用复制算法使用Survivor区域缓解了空间利用率的问题。
老年代使用标记清除和标记整理算法的混合实现
mark阶段的开销与存活对象的数量成正比sweep阶段的开销与所管理区域的大小成正相关compact阶段的开销与存活对象的数据成正比
Hotspot中的CMS回收器老年代是基于Mark-Sweep实现的对于对象的回收效率很高而针对碎片问题CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施当内存回收不佳碎片导致的ConcurentModeFailure将采用SerialOld执行Full GC以达到老年代内存的的整理。
分代的思想被现有的虚拟机广泛的使用几乎所有垃圾回收器都会区分新生代和老年代。
7增量收集算法#
为了解决STW的问题一次性把垃圾收进行处理需要造成系统长时间的停顿。那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间接着切换到应用程序线程依次反复直到垃圾收集完成。
基础仍然是传统的标记-清除和赋值算法增量收集算法通过对线程间冲突的妥善出里。允许垃圾收集线程以分阶段的方式完成标记、清理、复制工作
缺点使用这种方式由于在垃圾回收过程中间断性地还执行了应用程序代码所以能减少系统的停顿时间但是因为线程切换和上下文转换的消耗会使得垃圾回收的总体成本上升造成系统吞吐量的下降
低延迟和吞吐量是相互矛盾的两个标准。更关注低延迟
8分区算法#
堆空间越大GC的时间越长为了更好地控制GC产生的停顿时间将一块大的内存区域分割成多个小块根据目标的停顿时间每次合理地回收若干个小区间而不是整个堆空间从而减少一次GC所产生的停顿
分区算法将按照对象的生命周期长短划分成两个部分分区算法将整个堆空间分成连续的小区间region.
每个小区间独立使用独立回收可以控制一次回收的区间的个数。可以控制目标停顿时间。 实际上垃圾收集器都是复合的算法并行并发兼备
3. 垃圾回收相关概念#
1System.gc()#
会显式触发Full GC但其调用附带一个免责声明无法保证对垃圾收集器的调用不保证什么时候执行
Runtime.getRuntime.gc() 底层调用的
2内存溢出和内存泄漏#
内存溢出会导致程序崩溃。没有内存空闲并且垃圾收集器也无法提供更多的内存。由于GC的发展其实不太容易出现OOM错误。
原因
堆内存设置的不够创建了大量的大对象并且长时间不能被垃圾收集器手机存在被引用
内存泄漏 memory leak
严格的说只有对象不会再被程序用到了但是GC不能回收他们的情况才称为内存泄漏
但实际情况中由于一些不好的实践导致对象的生命周期变得很长甚至导致OOM也可以称为宽泛意义的内存溢出。
尽管内存泄漏不会立刻引起程序崩溃但是一旦发生内存泄漏程序中的可用内存就会被逐步蚕食。直至耗尽所有的内存最终可能会导致OOM
虚拟内存。取决于磁盘交换区的大小。 Java没有使用引用计数算法所以其实不会出现循环引用的情况。
举例
单例模式单例的生命周期和应用程序一样长如果持有对外部对象的引用的话那么这个外部对象是不能被回收的则会导致内存泄漏的产生。
一些需要关闭的close()的资源为关闭。如数据库网络连接io连接必须要手动关闭否则会内存泄漏
3STW#
Stop the world 应用程序的线程会被停止。
需要确保一致性的快照中进行GC Roots的分析。如果分析过程中对象的引用关系还在不断的变化则分析结果的准确性无法保证。
被STW中断的应用程序线程会导致卡顿。
所有的垃圾回收器都会有STW不能完全避免。只能提高回收的效率尽可能缩短暂停的时间。
STW是JVM在后台自动发起和完成的。在用户不可见的情况下把用户正常的工作线程全部停掉。
开发中的不要使用System.gc() 会进行Full GC导致STW的发生。
4垃圾回收的并行与并发#
并发concurrent。在一个时间段中有几个程序都处于已启动运行到运行完毕之间且这几个程序都是在一个处理器上运行的。并发并不是真正意义上的同时进行只是CPU把一个时间段划分成几个时间片然后在几个时间区间来回切换由于CPU的运行速度非常快。只要时间间隔处理得到就会感觉是多个应用程序同时进行。 并行多核cpu各个核执行各自的进程不存在抢占资源。适合科学计算、后台处理弱交互操作GPU? 并发是在一个时间段同时发生
并行是同一个时间点同时发生
并发的任务互相抢占资源
并行的多个任务之间是不互相抢占资源的
只有在多核CPU中才有并行。
垃圾回收中并发与并行 5安全点与安全区域#
安全点程序并不是在任何时间点都可以停顿下来开始GC的这些位置称为安全点
Safe Point的选择很重要如果太少可能导致GC等待的时间太长如果太频繁可能导致运行时的性能问题大部分指令的执行时间都非常短暂通常会根据是否具有让程序长时间执行的特征选择一些执行时间较长的指令作为safe point 如方法调用、循环跳转、异常跳出
在GC的时候如果检查线程是否到达安全点
抢先式中断 目前没有虚拟机采用
先中断所有线程如果发现有线程不在安全点就恢复线程让线程跑到安全点
主动式中断
设置一个中断标志各个线程运行到Safe Point 的时候主动轮询这个标志如果中断标志为真则将自己进行中断挂起。
安全区域如果有些线程sleep或者blocked了。安全区域是指在一段代码片段中对象的引用关系不会发生变化在这个区域的任何位置开始GC都是安全的可以视作是安全点的扩展。
当线程运行到SafeRegion的时候首先标识已经进入了SafeRegion。如果这段时间进行GCJVM会自动忽略标识为safe region状态的线程。
当线程即将离开Safe region的时候会检查JVM是否已经完成GC 如果完成了就继续执行线程否则就必须等到GC完成可以安全离开的信号为止。
6强引用、软引用、弱引用、虚引用、终接器引用#
当内存空间够的时候则能够保留在内存中如果内存空间在进行GC后还是很紧张就可以抛弃这些对象。
偏门且高频的面试题强引用、软引用、弱引用、虚引用区别及使用场景
Strong Soft Weak Phontom Refrence 4种引用强度依次逐渐减弱。
强引用new 对象如果强引用还存在就不会被GC掉 不回收。
软引用在系统将要发生内存溢出之前将会把这些对象列入回收范围进行第二次回收。如果这次回收之后还是没有足够的内存则抛出OOM。内存不足就回收。
弱引用被弱引用关联的对象只能生成到下一次垃圾收集前当垃圾收集器工作时无论内存空间是否足够弱引用都会被回收掉。发现就回收。
虚引用一个对象是否有虚引用的存在完全不会对其生存的时间构成影响也无法通过虚引用来获得一个对象的实例为一个对象设置虚引用关联唯一的目的就是能在这个对象被收集器收回时收到一个通知。跟踪对象回收。
强引用都是可触及的不会被回收。强引用是造成内存泄漏的主要原因
软可触及、弱可触及、虚可触及
软引用在第一次回收之后还是空间不足就会把软引用列入第二次回收的范围。用来实现内存敏感的缓存比如告诉缓存就有用到软引用。如Mybatis中使用到了。当内存不足的时候就清除缓存。尽量会让软引用存活的时间长一些迫不得已才回收。
Object obj new Object();
SoftReferenceObject sr new SoftReferenceObject(obj)
obj null 销毁强引用。弱引用来存储可有可无的缓存数据。当弱引用对象被回收时会被进入指定的引用队列通过这个队列可以跟踪对象的回收情况。弱引用更简单、发现就GC而软引用还需要使用算法判断。
WeakHashMap
虚引用不能单独使用也无法通过虚引用获取被引用的对象当试图通过虚引用的get()方法区取得对象时总是Null
PhantomRefrence 。对象回收跟踪。
总结器引用FinalRefrence 实现对象的finalize() 无需手动编码在GC的时候终接器入队由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()第二次GC时才能回收被引用对象。
4. 垃圾回收器#
GC分类与性能指标
不同的垃圾回收器概述
Serial 回收器 串行回收
ParNew回收器 并行回收
Parallel回收器 吞吐量优先
CMS 回收器 低延迟
G1回收器 区域化分代式
垃圾回收器总结
GC日志分析
垃圾回收器新发展
1分类与指标#
Java不同版本的新特性
语法 Lambda 表达式API层面 Stream API 时间日期底层优化 JVM的优化、GC的变化
分类 按线程分串行和并行多个GC线程 工作模式并发式和应用程序线程同时进行和独占式 按碎片处理方式压缩式、非压缩式 按工作的内存区间年轻代GC 老年代GC
指标
吞吐量 运行用户代码的时间占总运行时间的比例 总运行时间 程序的运行时间GC的时间垃圾收集开销吞吐量的补数 1-吞吐量暂停时间 执行垃圾收集时程序的工作线程被暂停的时间 这个指标是日益凸显的因为内存便宜了收集频率 相对于应用程式的执行收集操作发生的频率。收集频率高就会暂停时间短但是吞吐量低收集频率低就会暂停时间长吞吐量大。内存占用 Java堆区所占的内存大小快速 一个对象从诞生到被回收所经历的时间
吞吐量 throughput 低延迟和高吞吐量是不可兼得的。
现在的标准在最大吞吐量优先的情况下降低停顿时间可控的时间范围
2不同垃圾回收器概述# 7款经典的垃圾收集器
串行回收器Serial、 Serial Old
并行回收器ParNew 、Paraller Scavnege、Parallel Old
并发回收器CMS G1 来的方向
ZGC、 Shenandoah
不同的垃圾回收器适用的场景不同。
JDK8 默认使用ParallelGC(在新生代)
JDK9使用G1 GC
3Serial GC 串行回收#
JDK1.3之前回收新生代的唯一选择Hotspot Client模式下的默认新生代垃圾收集器
Serial 收集器 采用复制算法串行回收和STW机制的方式执行内存回收
Serial Old 也采用了串行回收和STW机制只不过内存回收算法使用的标记-压缩算法是运行在Client模式下默认的老年代垃圾回收器
Serial Old与新生代的Parallel Scavenge配合使用作为老年代CMS收集器的后备垃圾收集方案。 优势简单高效相较于其他收集器的单线程对于限定单个CPU的环境没有线程交互的开销
对运行在Client模式下的JVM是不错的选择
在用户的桌面应用场景可用内存不大一两百MB可以在较短时间完成垃圾收集100多ms只要不频繁发生使用串行回收是可以接受的。
-XX:UseSerialGC 新生代和老年代都使用Serial
基本不使用只在单核的CPU场景使用对于交互较强的应用不会采用这种GC
4ParNew GC并行回收#
是Serial GC的多线程版本在新生代中收集 Parallel New
采用了并行回收其他和Serial GC几乎没有区别共享了很多底层代码。同样采用了复制算法、STW机制
ParNew是很多JVM运行在Server模式下默认的新生代GC
配合老年代的SerialOld GC(JDK9 后不支持配合使用) 和CMS GCJDK14后移除了CSM GC
被Deprecated了较少使用了。 最新的JDK14只有三种组合 Serial GC Serial Old 应付低性能的场景 Parallel Scanvenge Parallel Old 应付性能高的场景 多个GC线程并行 G1 新生代和老年代都使用G1 多核CPU使用ParNew效率更高可以充分使用硬件资源可以提升吞吐量
如果是单核的还是Serial效率高一点
-XX:UseParNewGC
-XX:ParallelGCThreads 设置GC可以使用的线程
5Parallel Scavenge GC: throughput first#
同样使用了复制算法、并行回收、STW机制
Parallel Scavenge为了达到一个可控制的吞吐量。和ParNew使用的底层GC框架不同。
自适应调节策略根据性能监控的调整最优的内存回收策略。
高吞吐量适合在后台运算的而不需要太多的交互的任务如执行批处理订单处理、工资支付、科学计算等应用
JDK1.6之后提供了Parallel Old用来替代Serial Old在老年代的作用
在Server模式下能发挥良好的性能是JDK8默认使用的组合JDK9就开始使用G1了。
-XX:UseParallelGC
-XX:UseParallelOldGC
二者会互相激活。使用其中一个会默认的使用另一个。
-XX:ParallelGCThreads 设置GC可以使用的线程。最好和CPU的核相同小于8的时候。不会有线程切换的效率损失。
大于8的时候
-XX:MaxGCPauseMillis 停顿时间。低延迟。调整堆的大小或者其他一些参数。该参数谨慎使用
-XX:GCTimeRatio 垃圾收集时间占总时间的比例。吞吐量和MaxGCPauseMillis参数矛盾。
-XX:UseAdaptiveSizePolicy自适应策略
6CMS GC: low latency#
Concurrent Mark Sweep 第一款真正意义上的并发收集器第一次实现类垃圾收集线程与用户线程同时工作。
停顿时间短适合和用户交互的程序良好的响应速度提升用户体验
Java应用集中在互联网网站或者B/S系统的服务端上重视响应速度。
使用标记清除算法只能配合Serial GC 和ParNew GC
在G1出现之前CMS使用广泛。 仍然需要STW只是时间更短。
初始标记 initial markSTW 标记出GC Roots直接关联到的对象时间非常快
并发标记 concurrent mark: 从直接关联到的对象开始遍历这个对象图耗时较长但是不需要停止用户线程并发运行
重新标记 remark 对并发标记的修正因为用户线程执行会出现变化STW但是时间短比初始标记时间长
并发清理 Concurrent sweep 清理删除掉标记阶段判断已经开死亡的对象释放内存空间不需要移动对象维护一个空闲列表。
重置线程
当堆内存使用率达到一定阈值的时候就开始CMS因为并行的原因。如果CMS期间预留的内存无法满足需要就会出现Concurrent Mode Failure 启动Serial Old 预案。
会产生内存碎片。只能使用空闲列表的算法分配对象的内存空间
为什么不适用标记压缩算法因为要保证用户线程的正常进行不能修改内存的地址。
弊端
会产生内存碎片。会提前触发Full GCCMS收集器对CPU资源非常敏感不会造成用户停顿但吞吐量会下降无法处理浮动垃圾。在并发标记阶段产生的新的垃圾CMS无法对这些对象进行标记不会被回收。重新标记只是再次确认上一个阶段怀疑时垃圾的
CMS默认搭配的是ParNew
-XX:UseConcMarkSweepGC
-XX:CMSInitiatingOccupanyFraction 设置内存使用率的阈值一旦达到阈值就开始回收。JDK6以后是92% 用户线程增长快的时候要降低阈值
-XX:UseCMSCompacyAtFullConllection 在执行完Full GC 进行内存空间的整理
-XX:CMSFullGCsBeforeCompaction 每多少次Full GC整理一次
-XX:ParallelCMSThreads
最小化内存使用和并行开销 Serial GC
最大化应用程序的吞吐量 Parallel GC
最小化GC的中断和停顿时间 CMS(9 deprecated 、14删除)
7G1 GC 区域化分代式#
JDK9开始默认使用
原因业务越来越庞大复杂用户越来越多。
为了适应现在不断扩大的内存和不断增加的处理器的数量进一步降低pause time同时兼顾良好的吞吐量
目标是在延迟可控的情况下获得尽可能高的吞吐量所以才担起全功能收集器的重任与期望。
把堆内存分割成不相关的Region(物理上不连续)使用不同的Regison来表示Eden S0 S1 老年代等
有计划的避免在整个Java堆中进行安全区域的垃圾收集G1跟踪各个Region里面的垃圾堆积的价值大小回收所获得空间大小以及回收所需时间的经验值在后台维护一个优先列表每次根据允许的收集时间优先回收价值最大的Region
面向服务端针对多核CPU和大容量内存的机器。
-XX: UseG1GC
JDK9以后取代Parellel组合和CMS
并行G1在回收期间可以有多个GC线程同时工作有效利用多核计算能力此时用户线程STW
并发G1拥有与应用程序交替执行的能力部分工作可以与应用程序同时执行不会在整个回收阶段发生完全阻塞应用程序的情况
分代收集。G1仍然属于分代型垃圾回收器他会区分年轻代和老年代但不需要各个区是连续的。不再需要固定大小和固定数量。
同时兼顾年轻代和老年代
空间整合Region之间是复制算法但整体上世纪可看做标记压缩算法。可以避免内存碎片程序的连续运行当堆大的时候优势明显
可预测的停顿时间模型 soft real-time软实时可以明确指定在一个长度的时间片段内进行垃圾收集的时间G1在Region进行区域回收。缩小了回收的范围。维护一个优先列表根据回收价值进行区域回收。保证了G1收集器在有限的时间内获取尽可能高的收集效率
相比于CMS GC G1未必能CMS的最好情况下的延时停顿但是最差情况要好很多
G1无论是为了垃圾收集产生的内存占用footprint还是程序运行时的额外执行负载Overload都要比CMS高 开启垃圾回收器
设置堆的最大内存
设置最大的停顿时间
G1提供了3种模式Young GC Mixed GC Full GC
G1回收器的适用场景。
服务端、大内存、多处理器。 6GB或者更大的堆
每次只清理一部分而不是全部的Region的增量式清理
可以使用应用线程帮组加速垃圾回收过程
Region分区。
将整个Java堆划分成2048个大小相同的独立Region每个Region块大小根据堆空间的实际大小而定整体控制在1-32MB之间。通过-XX:G1HeapReguinSize设定。所有的Region大小相同且在JVM生命周期内才不会改变
新生代和老年代不再是物理隔离而是Region的集合通过Region的动态分配实现逻辑上的连续。 GC后的Region会被记录在空闲列表中下一次可以分配给其他功能区。
在每一个Region中
bump-the-pointer 指针碰撞TLAB
三个环节
Young GC老年代并发标记过程Mixed GC如果需要单线程、独占式、高强度的Full GC 还是继续存在。失败保护机制强力回收。 记忆集 Remembered Set 互联网项目都是使用G1
8GC日志分析# ZGC
Shenandauh GC