概念
虚拟机:指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统﹐是物理机的软件实现。Java Virtual Machine (Java虚拟机,简称JVM)
运行在操作系统之上,没有与硬件直接交互
生命周期及体系结构
虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的退出有如下的几种情况:
·某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止·
由于操作系统出现错误而导致Java虚拟机进程终止
类的加载
是什么?Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
优缺点
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成 的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销, 但是却为Java应用提供了极高的扩展性和灵活性
类的加载过程
1.加载
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数组?
对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接在 内存中动态构造出来的。
因为数组类的元素类型最终还是要靠类加载器来完成加载,
2.链接
2.1验证
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
2.2准备
·为类变量分配内存并且设置该类变量的默认初始值
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
2.3解析
将常量池内的符号引用转换为直接引用的过程。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可
直接引用
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机 的内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
3.初始化
初始化阶段就是执行类构造器方法<clinit>()的过程。
·<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的 语句合并产生的,
编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量在前面的静态语句块可以赋值,但是不能访问,
若该类具有父类,JVM会保证子类的<clinit>()执行前父类的<clinit>()已经执行完
1.使用new关键字实例化对象的时候。2.读取或设置一个类型的静态字段.3.调用一个类型的静态方法的时候。
属于生命周期不属于加载过程
4.使用
5.卸载
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按 照这种顺序按部就班地开始
而解析阶段则不一定:
它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
按部就班地“开始”,
而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都 是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
类加载器分类
通过一个类的全限定名来获取描述该类的二进制字节 流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。
虚拟机自带的加载器
启动类加载器Bootstrp,C++
只加载包名为java、javax、sun等开头的类
拓展类加载器Extension,Java
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用类加载器AppClassLoader
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
自己写的类由它加载
自定义类加载器
为什么需要?
·隔离加载类,避免类冲突
修改类加载的方式,根据实际情况在某个时间点按需动态加载
·扩展加载源:网络、数据库、机顶盒
·防止源码泄漏
双亲委派机制
工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
避免类的重复加载、保护程序安全,防止核心API被随意篡改
沙箱安全机制
沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,沙箱主要限制系统资源访问
那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
保证对代码的有效隔离,防止对本地系统造成破坏。
对象创建过程
1.判断该类是否加载、连接、初始化
⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
2.分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。
分配⽅式有 “指针碰撞” 和 “空闲列表” 两种
指针碰撞:按顺序放置
空闲列表:找到一块空闲足够放得下的空间放置
选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
⽽ Java 堆内存是否规整,取决于 GC收集器的算法是"标记-清除",还是"标记-整理"
3.处理并发安全问题
采用CAS
TLAB: 为每⼀个线程预先在Eden区分配⼀块⼉内存,JVM在给线程中的对象分配内存时,⾸先在TLAB分配,当对象⼤于TLAB中的剩余内存或TLAB的内存已⽤尽时,再采⽤上述的CAS进⾏内存分配
4.初始化分配到的内存空间
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
5.设置对象头
初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。
另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
6.执行init方法初始化
内存结构-运行时数据区
程序计数器
程序计数器是一块较小的内存空间(指针),它可以看作是当前线程所执行的字节码的行号指示器。(用来存储指向下一-条指令的地址)
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地 址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯 一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域
每一个线程都有一个程序计数器,是线程私有的,就是一个指针
使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
为什么使用PC寄存器记录当前线程的执行地址呢?
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设定为线程私有?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个Pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰
虚拟机栈
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放只要线程结束该栈就Over,生命周期和线程一致,是线程私有的。
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
对于栈来说不存在垃圾回收问题
参数-Xss 选项来设置线程的最大栈空间
存储哪些东东?
栈帧,一个栈帧对应一个方法、是一块内存、是一个数据集
局部变量表
定义为一个数字数组
主要用于存储方法参数和定义在方法体内的局部变量
这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型。
局部变量表中的变量只在当前方法调用中有效。
但是只要被局部变量表中直接或间接引用的对象都在栈帧销毁时不会被回收。GC时才会收
操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
执行引擎的一个工作区,当一个方法刚开始执行的时候一个新的栈帧的操作数栈是空的。
调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
栈顶缓存技术
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
动态链接
栈帧内部一个指向运行时常量池中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
存放调用该方法的pc寄存器的值
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。
堆(Heap)
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
几乎:逃逸分析栈上分配
jdk8字符串常量池、静态变量也在堆里面
空间占比
Young占1/3
伊甸:From:To = 8:1:1
新生代
伊甸园区
幸存者S0区(From)
幸存者S1区(To)
一XX:SurvivorRatio”调整这个空间比例。比如-Xx : SurvivorRatio=8
Old占2/3
上面占比是默认值
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
元空间:jdk8------永久代.jdk7
逻辑上在堆中,事实上不在虚拟机中而是使用本机物理内存.jdk8是在本地内存
TLAB空间
每个线程分配了一个私有缓存区域,它包含在Eden空间内。
解决线程安全
OOM(内存溢出)异常
(1) Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来 调整。
堆参数调优
-Xmx
最大分配内存,默认为物理内存的“1 /4'
-Xms
设置初始分配大小,默认为物理内存的“1 /64"
-XX: MaxTenuringThreshold --设置对象在新生代中存活的次数(默认15)
特殊情况下:幸存者区要是满了直接放到Old区了
配置第一点:两个设置一样大,初始值也设置为最大值,原因避免频繁的收集
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
方法区
存储那些东西?
存储了每一个类的结构信息
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
例如:字段、方法数据权限参数等、构造方法、普通方法的字节码内容。父类、接口、修饰符、全限定名等等
(jdk版本有所不同)运行时常量池字符串常量池
jdk7
字符串常量池、静态变量在永久代(方法区)
jdk8
字符串常量池、静态变量也在堆里面 (元空间)
静态变量:jdk7以后都放在了堆中
是所有线程共享的内存区域
方法区是规范,在不同虚拟机里头实现是不一样的, 最典型的就是永久代(PermGen space) 和元空间(Metaspace)。
元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize参数设置大小。
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspacesize的值是-1,即没有限制。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
虚拟机规范没有强制要求有垃圾回收
八字真言:栈管运行,堆管存储
垃圾回收
垃圾标记
什么是垃圾?
简单的说就是内存中已经不再被使用到的空间就是垃圾
为什么要用?
不及时对内存中的垃圾进行清理,垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
怎么找到垃圾?
垃圾标记阶段算法
1.引用计数法
每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
有一个地方引用时计数器+1,有一地方的引用失效时计数器-1,计数器引用为0时当垃圾收集
优点:
实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗 ;较难处理循环引用
2.可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
哪些对象可以为GC Roots?
虚拟机栈中引用的对象。
方法区中的类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(Native方法)引用的对象
所有被同步锁synchronized持有的对象
相对而言,解决对象循环引用引起内存溢出问题
GC四大算法
GC算法总体概述
普通GC (minor GC),一种是全局GC (major GC or Full GC)
区别
普通GC (minor GC) :只针对新生代区域的GC,
指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
全局GC (major GC or Full GC)
指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC (但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上
垃圾回收
标记-清除算法
what
算法分成标记和清除两个阶段
1.标记出所有需要回 收的对象,统一回收掉所有被标记的对象,
2标记存活的对象,统一回收所有未被标记的对象。
标记录在对象头中
标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候GC线程就会被触发并将程序暂停,随后将要回收的对象标记一 遍,最终统一 回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。
优势:简单,解决复制算法的浪费空间问题
缺点:1.内存碎片。2.需要停顿、扫两次耗时严重效率不高
清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。
下次有新对象需要加载时,判断垃圾的位置空间是否够如果够,就存放。
标记-复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题
思想
复制算法的基本思想就是将内存分为两块,每次只用其中一块, 当这一块内存用完,就将还活着的对象复制到另外一块上面。
优点:不会产生内存碎片,效率高
缺点:耗空间、如果对象存活率高的话就要全部复制
.标记-整理(压缩)算法
标记-清除-压缩
优势:解决标记清除的碎片问题、解决复制算法内存减半
缺点:需要移动对象的成本、不仅要标记对象还要整理存活对象的引用地址,耗时最长,效率低于复制算法,慢工出细活
小总结
内存效率:复制算法 > 标记清除 > 标记整理
(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐:复制算法==标记整理算法 > 标记清除
内存利用率:标记整理算法=标记清除算法>复制算法。
清除阶段的开销与所管理区域的大小形正相关
标记阶段的开销与存活对像的数量成正比
整理阶段开销与存活对象数量成正比
分代收集
年轻代:区域小生命周期短,Minor GC采用的是复制算法
老年代:区域大生命周期长、一般是由标记清除或者是标记清除与标记整理的混合实现
GC原则
频繁收集Young区
较少频繁收集Old区
基本不动元空间
垃圾回收器
垃圾收集器就是GC四算法的理念的落地实现
GC指标
吞吐量:运行用户代码的时间占总运行时间的比例
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
内存占用:Java堆区所占的内存大小。
现在标准:在最大吞吐量优先的情况下,降低停顿时间。
Serial 收集器
串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃圾收集工作,GC 线程工作时,其它所有线程都将停止工作。
新生代
使用复制算法收集新生代垃圾
优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率,是 Client 场景下的默认新生代收集器。
Serial Old-老年代
标记-整理算法收集老年代垃圾。
ParNew 收集器(新生代)
复制算法-新生代垃圾
Serial 收集器的多线程版本
单核环境下不如 Serial ,多核的条件下才有优势
Parallel Scavenge 收集器
(新生代
使用复制算法收集新生代垃圾
JDK8默认
目标是提高吞吐量
高吞吐量则可以高效率地利用 CPU 时间,适合在后台运算而不需要太多交互的任务
Parallel Old-老年代
使用标记-整理算法收集老年代垃圾
在注重吞吐量的场景下,可以采用 Parallel Scavenge + Parallel Old 的组合
CMS 收集器(老年代)
Concurrent Mark Sweep并发标记清除,划时代的意义就在于垃圾回收线程几乎能做到与用户线程同时工作。
是一种以获取最短回收停顿时间为目标的收集器。使用时年轻代收集器使用ParNew收集器
关注点是尽可能缩短垃圾收集时用户线程的停顿时间
适用于用户交互的程序
标记-清除 算法收集老年代垃圾
工作流程主要有如下 4 个步骤
1.初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,用户线程需要停顿
2.并发标记: 进行 GC Roots 跟踪的过程,主要标记过程,标记全部对象。它在整个回收过程中耗时最长,不需要停顿
3.重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿
4.并发清除: 清理垃圾,不需要停顿
优势:在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿
劣势:1.吞吐量低.2.无法处理浮动垃圾3.标记 - 清除算法带来的内存空间碎片问题
在并发标记阶段如果产生新的垃圾对象CMS无法标记,会导致这些新产生的垃圾对象没有被及时回收
G1:区域化分代式
G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)
侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First) 。
目标是在延迟可控的情况下获得尽可能高的吞吐量,是一款主要面向服务端应用的垃圾收集器
对要针对多核CPU和大容量的机器
工作流程:
子主题 1
组合方案
年轻代-老年代
Parallel Scavenge + parallel old
ParNew + CMS +(备用Serial Old)
Serial + Serial Old
G1
引用类型
强引用(默认支持模式)
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。因此强引用是造成Java内存泄漏的主要原因之一
只要还有强引用指向一个对象,就能表明对象还“活着”
软引用
软引用是一种相对强引用弱化了一 -些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
当系统内存充足时 不会 被回收,当系统内存不足时它 会 被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短
只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
谈一谈WeakHashMap?
Reference_WeakHashMap.java
内部类Entry继承WeakRefrence
虚引用
虚引用需要java.lang.ref. PhantomReference类来实现。
虚引用的主要作用是跟踪对象被垃圾回收的状态
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
作用:为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程
引用队列
当GC释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一一种通知机制。
当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。
执行引擎
负责解释命令,提交操作系统执行
为什么Java是半编译半解释语言?
现在VM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
问题:什么是解释器( Interpreter),什么是JIT编译器?
解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT (Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
谈一谈OOM
内存溢出:对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
拓展
内存泄漏:也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是Gc又不能回收他们的情况,才叫内存泄漏。
java.lang.StackOverflowError
栈溢出错误
java.lang.OutOfMemoryError:java heap space
堆空间溢出错误
java.lang.OutOfMemoryError: GC overhead limit exceeded
大量的时间用于垃圾回收,98%的时间用来做GC,并且回收了不到2%的堆内存
java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: Metaspace
元空间溢出
性能调优
JVM参数类型
标配参数
-version
-hlep
java -showversion
X参数(了解)
-Xint,解释执行
-Xcomp,第一次使用就编译成为本地代码
-Xmixed,混合模式
XX参数
Boolean类型
公式
-XX:+或者-某个属性
+表示开启 、-表示关闭
KV设置类型
公式
-XX:属性Key=属性Value
查看JVM初始家底
Java -XX:+PrintFlagsInitial