JVM——java virtual machine
java虚拟机就是二进制字节码运行的环境
特点:
- 一次编译,导出运行
- 自动内存管理
- 自动垃圾回收
文章目录
1. 内存与垃圾回收
1.1 java代码执行流程:
1.2 JVM架构模型
- 基于栈式架构
- 设计实现简单,适用于资源受限的系统
- 使用零地址指令方式分配,只是对栈顶 的操作
- 不需要硬件支持,可移植性好
- 基于寄存器架构
- 适用于二进制指令集,完全依赖硬件,可移植性差
- 性能优秀,执行高效
- 指令集以一地址指令、二地址指令、三地址指令为主
1.3 JVM的生命周期
- java虚拟机的启动是通过引导类加载器创建一个初始类来完成的
- 虚拟机的执行是执行java程序,本质是执行java虚拟机的进程
- 虚拟机的退出:
- 正常结束
- 遇到异常或错误终止
- 操作系统出现错误导致终止
- 某线程调用Runtime类或者System类exit方法
shotdomn.halt()
并且Java安全管理器允许本次操作 - 卸载虚拟机
1.4 各大公司的JVM
- classic VM
- Exact VM
- HotSpot VM(默认)
- JRokit
- J9
- Azul VM
- Apache Harmony->Android SDK
- Dalvik VM(应用与Android系统)
- Graal VM
1.5 类加载子系统
-
类加载器负责从文件系统或者网络中加载Class文件,class文件在开头有特定的文件标识(KA FE BA BE)
-
ClassLoader只负责class文件的加载,不负责运行。运行由Execution Engine决定
-
加载的类信息存放于在方法区的内存空间,除了类信息方法区还存放常量池信息,可能还包括字符串字面量和数字常量(常量池部分的内存映射)
class file加载到JVM中加载为DNA元数据模板,之后可以实例化多个实例调用构造器方法
类加载过程
加载
- 通过一个类的全限定名(包名+类型)获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
链接——验证
- 确保class文件的字节流包含信息符合当前虚拟机乔秋,保证被加载类的正确性,不会危害虚拟机自身的安全
- 四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
链接——准备
- 为变量分配内存并且设置该类变量的默认初始值即零值
- 不包含final修饰的static,因为在编译的时候就会分配,准备阶段会显式初始化(必须赋值)
- 不会为实例变量分配初始化,类变量会在方法区,而实例变量会随着对象一起分配到堆中
链接——解析
- 将常量池的符号引用(一组符号来描述所引用的目标)转换为直接引用(直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄)的过程
- 事实上,解析会在JVM执行完初始化后执行
- 解析主要正对类或接口、字段、类方法、接口方法、方法类型等
初始化
- 执行类构造器的过程,不是类的构造器
- 此方法不需要定义,而是类中所有类变量的赋值和静态代码块的语句合并而来,执行顺序按照语句顺序如果没有两者将不存在类构造器
- 若该类的具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
- 虚拟机必须保证一个雷的()在多线程下被同步加锁(方法只能调用一次)
静态代码块中的num、number已经在准备阶段被赋值为0,但是不能调用number,因为还没有声明变量
类加载器分类
这四种加载器是包含关系,不是上下层也不是子父类的继承关系
java核心类库都是引导类加载器加载并且只加载此
-
引导类加载器无父加载器,使用C++编写
-
扩展类加载派生于ClassLoader类,父加载器为启动类加载器,加载从java.ext.dirs系统属性所指定的目录中加载类库或从JDK的安装目录jre/lib/ext子目录加载类库
-
系统类加载器(AppClassLoader)派生于ClassLoader,父加载器为扩展类加载器,程序默认加载器负责加载classpath指定路径下类库
-
自定义加载器:用于隔离加载类、修改类加载方式、扩展加载源、防止源码泄露
ClassLoader类
获取ClassLoader的途径:
-
获取当前类的ClassLoader
clazz.getClassLoader()
clazz => Class.forName(“全限定名”)
-
获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader
-
获取系统的ClassLoader
ClassLoader.getSystemClassLoader
-
获取调用者的ClassLoader
DriverManger.getCallerClassLoader
双亲委派机制
JVM对class文件采用按需加载 的方式,当需要使用该类才会将class文件加载到内存中生成class对象,需要加载某个类的class文件时,JVM采用双亲委派机制,即把请求交由父类处理,是一种任务委派模式
优势
此模式避免累的重复加载,保护程序安全,防止核心API被篡改
工作原理:
- 如果一个类加载器收到类加载请求,他不会自己先去加载,而是把这个请求委托给父类加载器去执行
- 如果父类加载器还存在父类加载器,啧向上委托,到达最终的启动类加载器
- 如果父类加载器可以完成加载,就成功返回,如果父类无法完成,就由子加载器自己加载
沙箱安全机制
会将程序放置在一个独立空间内运行,这是对java核心代码的保护
例如自定义一个String类,加载自定义的String类的时候率先加载引导类加载器,先加载jdk自带文件(java.lang.String)
补充
在JVM中表示两个class对象为一个类的条件
- 类的完整类名、包名必须一致
- 类加载器也必须一致
对类加载器的应用
- 如果一个类是有用户类加载器加载的,那么JVM会将这个类加载器的一个应用作为类型信息的部分保存在方法区,所以JVM必须知道一个类型是由什么加载器加载的
- 当解析一个类型到另一个类型的应用的时候,JVM需要保证这两个类型的类加载器是相同的
类的主动与被动使用
1.6 运行时数据区
堆区和方法区(jdk1.8之后是元数据区 + 代码缓存->JIT编译产物)是多个线程共享(生命周期是进程),而程序计数器、本地方法栈、虚拟机栈是线程私有
每一个虚拟机实例对应一个Runtime实例(即运行时环境)
线程
- JVM允许一个应用有多个线程并行
- JVM每个线程斗鱼本地线程直接映射,当一个Java线程准备好执行,操作系统的本地线程也会创建,执行终止后,本地线程也会被回收
- 一旦本地线程出释怀成功后,就会调用Java线程中的run(),如果本地线程只剩下守护线程(专门为别的线程服务的,比如计时线程),虚拟机可以直接终止
JVM主要的线程
程序计数器(PC寄存器)
Program Counter Register 是对物理PC寄存器的一种抽象模拟,用来存储指向将要执行的指令代码,执行引擎根据地址读取下一条指令 => 将每行代码挂起来被执行引擎读取
- 很小的内存空间,运行速度最快
- 每个线程都有自己的程序计数器,生命周期与线程一致
- 任何时间一个线程只有一个方法执行,PC Register会存储当前方法的JVM指令地址,如果执行的本地方法,则是未指定值(因为java无法获取到C语言的值)
- 字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令
- 没有GC,同时也没有OOM异常(OutOtMemoryError)
使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址
- 因为CPU需要不断切换线程,需要知道线程应该从哪里继续执行,当线程切换回来字节码解释器就可以明确下调应该执行什么字节码指令
PC寄存器为什么被设为线程私有(每个线程一个)
- 为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的办法就是为每个线程分配一个寄存器,这样各个线程就会独立计算不会互相干扰
虚拟机栈
java指令根据栈来设计,优点:跨平台、指令集小,编译器容易实现,缺点:性能下降,实现同样的功能需要更多的指令
栈是运行时的单位(程序如何执行),堆是存储时的单位(数据放在哪里)
虚拟机栈:
一个线程对应一个虚拟机栈,一个栈帧对应一次方法调用,当前线程只有一个活动的栈帧,他保存方法的局部变量(基本数据类型、对象的地址引用)、部分结果,方法的调用和返回
成员变量与局部变量的区别
-
1.定义的位置不一样【重点】
- 局部变量:在方法的内部
- 成员变量:在方法的外部,直接写在类当中
2.作用范围不一样【重点】
- 局部变量:只有方法当中才可以使用,出了方法就不能再用了
- 成员变量:整个类都可以通用
3.默认值不一样【重点】
- 局部变量:没有默认值,如果要想使用,必须手动进行赋值
- 成员变量:如果没有赋值,会有默认值,规则和数组一样
4.内存的位置不一样(了解)
- 局部变量:位于栈内存
- 成员变量:位于堆内存
5.生命周期不一样(了解)
- 局部变量:随着方法进栈而诞生,随着方法出栈而消失
- 成员变量:随着对象的创建而诞生,随着对象被垃而消失
栈中可能出现的异常
设置栈内存
使用参数-Xss设置线层的最大栈空间,其大小决定函数调用最大可达深度
栈的存储单位
- 栈中数据以栈帧的格式存在,是一个内存区块
- 不同的线程中所包含的栈帧是不允许相互引用的
- 方法的两种退栈方式:return指令或者抛出异常(未处理的异常)
栈帧的内部结构
栈帧:局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息
局部变量表:
-
定义为一个数字数组,存储方法参数和定义方法体内的局部变量(包含对象实例引用);
-
由于局部变量表是建立在线程的栈上,是线程私有数据,不存在数据安全问题;
-
局部变量表所需容量是编译器确定下来的,运行时不会改变
-
方法嵌套调用的次数由栈的大小决定,线性正相关
-
局部变量表的变量制造当前方法调用中有效,方法执行时,JVM通过使用局部变量表完后曾参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会销毁
slot槽:
-
局部变量表的基本存储单位
-
32位以内的类型(byte、short、char转换为int,boolean转换为01,float)占用一个slot,64位类型(long、double)占用两位slot
-
参数值存放在局部变量数组的index0开始到数组长度-1的索引结束
JVM为局部变量表中的每一个slot分配一个访问索引,通过这个索引访问指定值
当一个方法被调用的时候他的方法参数和内部定义的局部变量会顺序复制到局部变量的solt上
当访问64位类型,只需要使用使用前一个索引
当前帧是构造方法或者实例方法(非静态方法),对象引用this会放在index0的slot处
slot是可以重复利用的,如果之前的变量过了作用域,之后的变量是可以占据过期变量的slot
操作数栈:
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据
- 用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- JVM执行引擎的工作区,方法开始执行的时候,方法的操作栈式空的
- 操作数栈并非采用访问索引的方式来进行数据访问的,而是出入栈操作
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并将更新PC寄存器中下一条需要执行的字节码指令
定义为int类型的数据,底层是按照数的大小定义类型的,超出这个类型范围,才会顺序转变类型
- 栈顶缓存技术:
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率(操作数是存储在内存中的)
动态链接:
每个栈帧内部包含的一个指向运行时常量池中该栈帧的所述方法的引用
所有的变量和方法引用都作为符号引用(#1)保存在class文件的常量池里
动态连接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法的调用:
JVM中符号引用转换为调用方法的直接引用与方法的绑定机制相关
- 静态链接-早期绑定:被调用的目标方法在编译器可知,且运行期保持不变
- 动态链接-晚期绑定:别嗲用的方法在编译器无法被确定下来,只能在程序运行时确定
java中的方法都特征具备虚函数的特征(晚期绑定)可以使用final去除这个
调用指令
invokestatic
:调用静态方法,解析阶段确定唯一方法invokespecial
:调用方法、私有及父类方法,解析阶段确定唯一方法invokevirtual
:调用所有虚方法以及final修饰的方法invokeinterface
:调用接口invokedynamic
:动态解析需要调用的方法,然后执行(java8-lambda表达式)
前四条指令固化在JVM,方法的调用认为不可干预,但最后一条有用户确定
静态类型语言就是判断自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息
方法重写的本质
虚方法表
因为频繁使用动态分配,每次动态分配的过程中都要重新在类方法元数据中搜索合适的目标的话影响执行效率,为提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找**(每个虚方法表都存放着方法的实际入口)**虚方法表会在链接-解析时创建并初始化
方法调用看你实现的是接口、父类还是自己的方法,再按照虚方法表找到对应的方法入口
方法返回地址、一些附加信息(帧数据区):
方法返回地址:存放调用该方法的pc寄存器的值
当A方法调用B方法时,在B方法的方法返回地址存放pc寄存器的值,记录调用B方法后的下一条指令,当执行引擎调用方法返回地址后,B方法栈帧出栈,A方法来到下一条指令
无论是正常执行完成还是出现未处理的异常在方法退出后都返回该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址;而通过异常退出的,返回地址要通过异常表来确定,栈帧不会保存这部分信息
一些附加信息:比如对程序调试提供支持的信息
面试题
举例栈溢出的情况
-
调用深度太深
-
递归没出口
调整栈溢出,能保证不会溢出吗
- 只是stackOverflowError延迟生效
垃圾回收会涉及虚拟机栈吗
- 不存在垃圾回收,但是存在异常,只有堆和方法区会出现GC和异常
栈内存越大越好吗
- 栈是线程私有的,如果设置的每一个栈的空间太大,就会导致最后可以分配的线程数量变少,甚至出现OOM
方法中定义的局部变量是否线程安全
- 具体问题具体分析(StringBuilder和StringBuffer的区别)
本地方法栈
JVM用于管理本地方法的调用
- 线程私有
- 允许被实现成固定或者可动态扩展的内存大小
- 具体为Native Method Stack中等级native方法
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,甚至可以直接使用本地处理器的寄存器,直接从本地内存的对中分配任意数量的内存
- 并不是所有的JVM都支持native method,本地方法栈可以虚拟机栈合二为一
堆
-
一个进程对应一个JVM实例,只存在一个堆内存,是线程共享的;在JVM启动时就被创建,空间大小确定但是可以调节
-
堆可以是物理上不连续的内存空间,但在逻辑上应该连续(在虚拟内存连续)
-
所有线程共享堆,但是可以划分线程私有的缓冲区TLAB,即在堆中每个线程都独有一分小空间
-
所有的对象实例以及数组都应该分配在堆上,数组和对象永远不会存储在栈上,只会保存引用
-
方法结束后,堆 中的对象不会立刻移除,当卡机回收的时候才会移除
-
堆是GC的重点区域
堆空间内存细分
JDK1.7分为新生区、养老区、永久区
JDK1.8分为新生区(Eden区 + Survivor区)、养老区、元空间
堆空间大小的设置
-Xms
=>-XX:InitialHeapSize 堆区起始内存
-Xmx
=>-XX:MaxHeapSize 堆区最大内存
一旦堆区内不错呢大小超过最大内存就会出现OutOfMemoryError
默认堆空间的大小:初始内存是物理内存的1/64,最大内存是物理内存的1/4
开发中初始和最大的内存最好设置为相同的值,避免频繁的扩容和释放,为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
查看所有进程:jps
查看堆空间内存大小:jstat -gc 进程
新生代和老年代
参数调整
jinfo -flag NewRatino 进程
查看占比
新生代Eden区与Survivor区默认应该是8:1:1,但是实际是6:1:1
几乎所有的对象都是Eden区被new出来的,绝大部分对象的销毁都在新生代中
-Xmn
:设置新生代最大内存大小
对象分配过程
对象分配
- new的对象先放在Eden区
- 当Eden区的空间填满时,再次创建对象时,JVM的垃圾回收器将对Eden区进行垃圾回收(YGC/Minor GC),将不再被其他对象所引用的对象销毁,再加载对象到Eden区
- 再将Eden区的幸存对象移动到from区(年龄计数器+1)
- 如果Eden区再次触发垃圾回收,此时from区对象也再次GC放到to区,Eden区幸存下来的也放在to区(age+1)
-
如果再次进行垃圾回收,此时所有幸存对象会重新放回from区,再次GC接着再去to区(age+1)
-
当age达到一定次数的时候,默认15次,会放到养老区
-XX:MaxTenuringThreshold=<N>
进行设置阈值
- 当养老区内存不足时,再次出发GC(Major GC),进行内存清理
- 若是养老区依然无法对象保存时,就会产生OOM
复制之后有交换,谁空谁是to区
GC频繁在新生区,很少在老年区,几乎不在永久区/元数据收集
特殊情况
如果存在超大对象,Eden区无法放下同时新生代也无法放下,直接放到老年区,如果老年区也放不下,执行FGC,还是放不下报OOM
如果新生代内存放不下,就会直接晋升老年代
Minor、Major、full GC
每次GC的时候不是都对三个内存区域都回收
- Minor GC:只是对新生代垃圾收集,Survivor区满不会触发,会触发STW暂停其他用户线程等垃圾回收结束,用户线程恢复
- Major GC:只是对老年代垃圾收集,比Minor GC慢十倍,STW时间长
- Mixed GC:收集整个新生代以及部分老年代的垃圾收集
- Full GC:收集整个java堆和方法区的垃圾收集
-XX:+PrintGCDetails
显示GC垃圾回收过程
堆空间的空间分代思想
- 分代作用:优化GC性能,避免对对象进行高频的垃圾收集
内存分配策略
对象提升规则
- 有限分配到Eden区
- 大对象直接分配到老年代
- 长期存活的对象分配到老年代
- 动态对象年龄判断:如果Survivor区中相同年龄的所有对象的总和大于Survivor空间的一半,年龄大于或等于的对象可以直接进入老年代,无序等到MaxTenuringTheshold中要求的年龄
- 空间分配担保:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平局大小就会进行Ninor GC,否则将会Full GC
为对象分配内存-TLAB
TLAB设置原因
- 堆区是线程共享区域,人和县城都可以访问堆区数据,由于对象实例的创建在JVM中使用频繁,因此并发环境下从堆区划分内存空间是线程不安全的,为避免多个线程操作同一个地址,需要使用加锁等机制,进而影响分配速度
TLAB:为每个相册个分配一个私有缓存区域
多线程同时分配时,可以避免非线程安全问题,同时哈能顾提升内存分配得吞吐量,这是快速分配策略
-XX:UseTLAB
开启TLAB
-XX:TLABWasteTargetPercent
设置TLAB空间所占用的Eden空间的百分比大小
一旦对象TLAB空间分配失败(对象比较大),JVM会使用加锁机制确保数据操作原子性,从而直接在Eden空间中分配内存
堆空间的参数设置
逃逸分析
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
判断new的对象是否被外部方法调用
开发中能使用局部变量的,就不要使用在方法外定义
代码优化
逃逸分析必须在server环境下
方法区
方法区看作是独立于java堆的内存空间
方法区的大小决定系统可以保存多少个类,如果定义太多类会导致方法区溢出(OOM),比如加载大量第三方jar包、Tomcat部署的工程过多、大量动态的生成反射类
JDK1.7以前方法区成为永久代,JDK1.8之后元空间取代了永久代
元空间不在虚拟机设置的内存中,而是使用本地内存
设置方法区大小
-XX:MetaspaceSize
设置方法区初始大小
-XX:MaxMetaspaceSize
设置最大大小
如何解决OOM
方法区的内部结构
方法区存放类型信息(类、接口、枚举、注解)、常量、编译后的代码缓存
类型信息
域信息(成员变量)
方法信息
运行时常量池
为什么需要常量池
一个java源文件中的类、接口,编译后产生一个字节码文件,而java字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,所以存到常量池里,这个字节码会包含常量池的引用
常量池存储的数据类型
🐬字节码中的常量池经过类加载之后,存储的到方法区的常量池就是运行时常量池
- JVM为每个加载类型都维护一个常量池,池中数据通过索引访问
- 运行时常量池具备动态性
方法区演进
只有HotSpot才有永久代
去除永久代的原因
- 永久代的空间大小很难确定
- 永久代的调优很难完成
String Table为什么调整
- 因为永久代回收效率太低,开发中会有大量字符串被创建,无法及时回收,就会导致永久代内存被占用
static Variable
- 静态引用对应的对象实体始终都存在堆空间,只是引用名也放在在堆中
方法区的垃圾回收
方法区的回收效果不好,尤其是类型的卸载
垃圾回收:常量池中废弃的常量和不再使用的类型
判定类型不再使用的条件
- 该类所有实例被回收(该类以及子类)
- 加载该类的类加载器被回收
- 该类对应的java.lang.Class对象没有被引用
大量使用动态代理、反射的场景下,需要使用方法区的回收
1.7 本地接口
本地方法
-
一个本地方法就是一个java调用非Java代码的接口
-
本地接口就是融合不同编程语言为java所用
-
定义一个native method时,并不提供实现体,使用native关键字修饰
-
不能和abstract一起用
使用native method的原因
- 与java环境外交互,如操作系统、某些硬件交换信息时
- 与操作系统交互
- sun的解释器用C语言编写的
1.8 对象的实例
对象的实例化
创建对象的方式
1. new 对象
- 对象的静态方法会存在调用构造器的方法(和new对象一样)
- xxxbuilder/xxxFactory的静态方法(工厂模式)
2. Class的newInstance
通过反射,只能调用空参的构造器,权限必须是public
3. Constructor的newInstance
通过反射,可以调用空参、带参的构造器,无权限要求
4. 使用clone()
不调用构造器,但必须要实现Cloneable接口
5. 使用反序列化
从文件、网络中获取一个对象的二进制流
创建对象的步骤
1. 判断对象对应的类是否加载、链接、初始化(加载类元信息)
虚拟机在接受到一条new指令,首先检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有加载,在双亲委派模式下,使用当前类加载器ClassLoader+包名+类名为key查找对应的.class文件。如果没有找到这个文件,抛出ClassNotFoundException异常,如果找到,进行类加载,生成对应的Class类对象。
2. 为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,四个字节大小(short、boolean、int、char、float都是四字节,lon、double都是八字节)
堆的规整取决于垃圾收集器是是否带有压缩整理功能
-
如果内存规整——指针碰撞
使用的内存在一边,空闲的内存在另一边,中间放着一个指针,分配内存时,就把指针向空闲那边移动与对象大小相等的距离;如果垃圾回收器选择的是Serial、ParNew这种压缩算法,虚拟机使用这种分配,一般使用conpact过程的收集器使用指针碰撞
-
如果内存不完整——空闲列表
使用内存和空闲内存交错,虚拟机后悔维护一个列表,记录纳西额内存可用于不可用,再分配一个足够大的空间划分给对象实例,并更新列表上的内容,例如CMS垃圾收集器
3. 处理并发问题
- 采用CAS失败重试、区域加锁保证更新的原子性
- 每个吸纳成预留一个TLAB
4. 初始化分配的空间
所有属性设置默认值,保证对象实例字段在不赋值时可以使用
给对象属性赋值方式
- 属性的默认初始化(√)
- 显示初始化/代码块中的初始化
- 构造器中的初始化
5. 设置对象的对象头
将对象的所属类、对象的HashCode和对象的GC信息、锁信息春初在对象的对象头中
6. 执行init方法进行初始化
正式开始初始化,初始化成员变量,执行实例化代码块,调用类的构造器,并把堆内对象的首地址赋值给引用变量,一般来说:new之后就是执行方法
对象的内存布局
对象头信息
运行时元数据
- 哈希值
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
类型指正
指向元数据的INstanceKlass,确定该对象所属的类型
如果是数组还需记录数组的长度
实例数据
对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(父类继承以及本身)
规则
- 相同宽度的字段总数被分配到一起
- 先加载父类变量在加载本身
- 如果ConpactFields参数为true,子类的窄变量可能插入到父类变量的空隙
对类填充
占位符作用
完整实例化过程
对象的访问定位
JVM通过栈上的reference访问,使得栈帧中的对象引用访问其内部的对象实例
句柄访问
优点:如果对象实例发生移动,只需要更改句柄池
直接指针
优点:不浪费空间
1.9 直接内存
直接内存是在java堆外的直接向系统申请的内存区别
来源于NIO,通过存在堆中的DirectByteBuffer操作的Native内存
应用访问物理磁盘的时候不在需要用户态和内核态,而是直接访问内存的映射
默认与 -Xmx参数值一致
-MaxDirectMemorySize
设置直接内存大小
1.10 执行引擎
虚拟机的执行引擎由软件自行实现,能够执行那些不被硬件支持的指令集格式,解释将字节码指令解释或编译为对应平台的本地机器码
执行引擎工作过程
- 执行引擎在致信国会曾中究竟需要执行什么样的字节码指令玩去哪以来与PC寄存器
- 每当执行万意向指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
- 当方法在执行过程中,执行引擎可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例以及通过对象头中元数据指针定位到目标对象的类型信息
输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果
java代码编译和执行过程
执行引擎
解释器:对字节码按采用装解释的方式执行
JIT编译器:虚拟机将源代码直接编译成和本地机器平台相关的机器语言
为什么java是半编译型半解释型语言
因为java执行引擎既编译既解释
机器码->指令->指令集->汇编语言->高级语言
解释器
字节码解释器和模板解释器(将每一条字节码和一个末班函数相关联)
- Interpreter模板:实现了解释器的核心功能
- Code模板:用于管理虚拟机运行时生成的本地机器指令
JIT编译器
为什么要保留解释器
当程序启动后解释器立马就可以发挥作用
一个被多次调用的方法或者和一个方法内部循环次数较多的循环体都是热点代码,将这些代码编译成本地机器指令就是栈上替换
采用热点探测方式使基于计数器(方法调用计数器C1500/S10000和回边计数器)的热点探测
JIT编译器内置两种-client
和-server
1.11 StringTable
关于String
String声明为final,不可被继承
String实现Serializable和Comparable接口,表示序列化和比较大小
在java1.9之后存储String数据由char[]改为了byte[]
String不可变的字符序列
- 当对字符串重新赋值的时候,需要重写指定内存区域赋值,不能使用原来的value赋值
- 当对字符串进行连接操作时,也需要重新制定内存区域
- 当调用String的replace方法修改字符时也需要冲洗指定内存区域
字符串常量池中不会存储相同内容的字符串
- String的String Pool是一个HashTable,默认值是1009,不会进行扩容;如果放进去String非常多的话,造成hash冲突,导致链表过长,调用String.intern性能下降
-XX:StringTableSize
设置StringTable长度- 在jdk1.7中,StringTable的长度默认是60013,jdk1.8后最小值是1009
String pool使用方式
- 字面量会直接存入数据池
- 调用String.intern()
String的拼接
- 常量与常量的拼接结果咋常量池中,编译期优化
- 常量池中不会存在相同内容的常量
- 只要字符串赋值时有一个变量,结果就在堆中(相当于new对象),底层是StringBuilder的运用
- 如果拼接调用intern,则会放入常量池进行比对,并返回对象地址
intern()的使用
newString(“”)创建的对象
会在堆中先new出对象,之后在常量池中再创建一个对象
new String(“”) + new String(“”)
注:new完两个对象后字符串常量池中并没有创建相应对象
对于存在大量重复的字符串,可以使用intern来减少内存消耗
String的去重
1.12 垃圾回收
关于垃圾回收
关于垃圾
在运行程序中没有任何指针指向的对象
为什么需要GC
- 内存会被耗光
- JVM需要整理内存分配新的对象
- 没有GC就不能保证应用程序的正常进行,需要不断进行优化
如果不及时堆内存中垃圾进行回收,垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,可能会导致内存溢出
System.gc() -> Runtime.getRuntime().gc()
- 显式触发Full GC,无法保证对GC的调用(提醒执行)
java垃圾回收机制
- 自动内存管理
- 自动内存管理机制
堆是GC的收集重点
内存溢出和内存泄漏
内存溢出
没有空闲内存,并且垃圾收集器也无法提供更多内存
- java虚拟机的堆内存设置不够
- 代码创建了大量大对象,并且长时间不能被垃圾收集器收集
在抛出OOM前,会进行一次GC处理
内存泄漏
对象不会被使用,但是GC不能回收
⭐️这里的存储空间不是指物理内存,而是虚拟内存,这个取决于磁盘交换区设定的大小
Stop The World
GC发生的时候,产生的应用程序的停顿
e.g:可达性分析算法中枚举GC Roots会导致吸纳成停顿
垃圾回收的并行与并发
并发
在一个时间段内有几个程序都是已启动和运行完毕之间,且几个程序都是同一个处理器上运行
并行
多个CPU执行个线程,DUO个线程互不抢占资源,也互不影响,就是在并发
安全点和安全区域
安全点:只有特殊位置才能停顿下来执行GC
选择一些执行时间较长的指令作为安全点,比如方法调用、循环跳转、异常跳转
如何在GC发生的时候,检查线程跑到最近的安全点停顿下来
主动式中断:设置一个中断标志,各个线程运行到萨芬Point的时候主动轮询这个标志,若为真,则中断挂起
安全区域:在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置来时GC都是安全的
执行过程
- 当线程运行到safe region的代码时,首先标识已经进入safe region,如果这段时间发生GC,JVM会忽略safe region状态的线程
- 当线程即将离开safe region时,会检查JVM是否完成GC,如果完成继续运行,否则线程必须等待,直到收到可以安全离开的信号为止
引用
为了存在内存空间足够时,对象存在内存,如果内存空间不够,就抛弃这些对象(缓存)
强饮用(默认引用)——不可回收
只要存在强引用关系,就永远不会被回收的对象
- 可以直接访问对象
- 任何情况都不会被系统回收
- 可能导致内存泄漏
软引用——内存不足就回收
在系统将要发生内存溢出之前,对这些对象二次回收
e.g:高速缓存的实现、mybatis的缓存
垃圾回收器在某个时刻决定回收软引用对象的时候,会清理软引用。并可选的把引用存放到一个引用队列
弱引用——发现就回收
只被弱引用关联的对象只能生存到下一次垃圾收集的时候
构造弱引用的时候可以指定一个引用队列,当弱引用对象被回收的时候就会加入引用队列,通过队列可以跟踪对象的回收情况
虚引用——对象回收跟踪
为一个对象设置虚引用唯一目的就是追踪垃圾回收过程,当垃圾回收时可以收到一个系统通知
e.g:将一些资源释放操作放置在虚引用中执行和记录
终结器引用
用于实现finalize()方法
内部配合队列使用
垃圾回收算法
- 先标记对象,判断是否为垃圾
- 再运行垃圾回收算法回收垃圾
标记:引用计数算法(java不使用)
对象存活判断
- 当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为死亡
⭐️对每个对象爆粗一个整型的引用计数器属性,用于记录对象被引用的情况
优缺点
python使用引用计数器和垃圾收集机制,依靠手动解除和使用弱引用weakref来解决循环引用
标记:可达性分析算法(根搜索/追踪信垃圾收集)
GC Roots必须是一组活跃的引用
基本思路
- 以根对象为起始点,按照从下到上的方式搜索被根对象集合所连接的目标对象是否可达
- 使用该算法中,内存中的存活对象都会被根对象集合直接或间接的相连,搜索走过的路径成为引用链
- 如果目标对象没有任何引用链,就位对象死亡
GC Roots包含的对象
- 虚拟机栈中引用对象(被调用方法的参数、局部变量)
- 本地方法栈内的本地方法引用的对象
- 方法区中静态属性引用的对象(引用类型静态变量)
- 方法区常量引用的对象(字符串常量池)
- 所有被同步锁synchronized持有的对象
- JVM内部引用(基本数据的Class对象)
- 反映JVM内部情况的JMXBean、JVMI中注册的回调、本地代码缓存
- 临时的回收:分代收集和局部回收,考虑内存区域会被其他区域的对象访问
必须在保障一致性的快照中标记,所以JVM会停下来进行标记
对象的finalization机制
对象终止机制提供对象被销毁前的自定义处理逻辑
垃圾回收对象之前会调用该对象的finalize(),用于在对象被回收时进行资源释放,比如关闭文件、套接字、数据库连接
由于finalize(),对象会处于三种状态
- 可触及的:从根节点开始,可以到达对象
- 可复活的:对象的所有引用都被释放,有可能在finalize复活
- 不可触及的:不可能被复活,finalize只会被调用一次
判断对象可回收的过程
清除:标记-清除算法
标记与清除
标记:Collector从引用根节点开始便利,标记所有被引用对象
清除:Collection对堆内存从头到尾开始线性遍历,如果发现某个对象在Header中没有标记为可达对象就回收
⭐️清除就是把需要清除的对象地址保存在空闲的地址列表里,下次新对象需要加载时,判断垃圾的位置空是否足够,如果够就存放
清除:标记-复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
一次可以回收70%~99%内存空间,存活对象少,垃圾对象多
清除:标记-压缩算法
标记与压缩
标记:从根节点标记所有被引用的对象,在清除未标记对象
压缩:将所有存活的对象压缩到内存的一端按顺序排放,清理边界外的所有空间
三种算法对比
分代收集算法
增量收集算法、分区算法
增量收集算法
如果一次性所有的垃圾进行处理,需要造成系统长时间的停顿,那么可以让垃圾收集线程和引用程序交替执行,垃圾收集线程只收集一小片区域的内存空间,接着切换到引用程序线程,直至垃圾收集完成
对线程间冲突的妥善处理,允许垃圾线程以分阶段的方式完成标记、清理或复制工作
会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法
将整个堆空间分成一块块小区间
每个小区间独立使用,独立回收
垃圾回收器
GC分类
- 按照线程分为并行和串行
- 按照工作模式分为并发式和独占式
- 按照碎片处理方式分为压缩式和非压缩式
- 按照工作的内存区间分为年轻代和老年代
性能指标
- 吞吐量:程序的运行时间占总运行时间的比例
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
- 垃圾收集开销:垃圾收集所用的时间占总运行时间的比例
- 内存占用:java堆区所占的内存大小
- 快速:一个对象从诞生到被回收所经历的时间
准则:在最大吞吐量有限的情况下,降低停顿时间
垃圾回收器的搭配使用
针对不同的场景下,使用不同组合的垃圾回收器,以此提高垃圾收集效率
查看默认的垃圾回收器
-XX:+PrintCommandLineFlags
查看命令行相关参数(包含垃圾回收器)
Serial回收器——串行回收
Serial收集器采用复制算法、串行回收、STW机制执行内存回收
Serial Old收集器采用标记-压缩算法、串行回收、STW机制,在Client模式下默认老年代GC,在Server模式下,与新生代Parallel Scavenge配合使用或者作为CMS后备垃圾收集器
适用于桌面应用场景,内存不大,可以在较短时间内完成垃圾收集
ParNew回收器——并行回收
与SerialGC相比只是 串行回收变成了并行回收
ParNew是运行在Server模式下的默认GC
一般不会使用,因为在1.9版本被移除了
Parallel回收器——吞吐量优先
Parallel Scavenge收集器采用复制算法、并行回收、STW机制
此收集器可以达到一个可控制的吞吐量,和自适应调节策略
高吞吐亮可以高效率低利用CPU时间,适合后台运算,比如鼻梁处理、订单处理、工资支付、科学计算等
Parallel Old采用标记-压缩算法
参数设置
-XX:+UseParallelGC
设置年轻代
-XX:+UseParallelOldGC
设置老年代
-XX:ParallelGCThreads
设置年轻代并行收集器线程数
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间
-XX:GCTimeRatio
垃圾收集时间占总时间的比例(=1/(N + 1)),默认99,不超过1%
-XX:+UseAdaptiveSizePolicy
:设置自适应调节策略,平衡吞吐量和垃圾回收停顿时间
CMS回收器——低延迟
采用标记-清除算法,尽可能缩短垃圾收集时用户线程的停顿时间
CMS过程
- 初始标记:仅仅只是标记出GC Roots能直接关联到的对象,需要STW
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,不需要停顿用户线程,与垃圾收集并发执行
- 重新标记:修正并发标记期间,因用户程序继续运作而导致标记变动的那一部分对象的标记记录,需要STW
- 并发清除:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间
因为在垃圾收集的时候用户线程没有切断,所以不能等到老年代快填满时才收集,应该留出足够的内存给线程;当堆内存使用率达到某一个阈值时,便开始进行回收。如果内存无法满足,则临时启用Serial Old收集器进行垃圾收集
为什么不用标记-压缩
要保证用户线程的执行,不能改变对象的地址,同时压缩算法需要STW,这样就无法达到低延迟特点
参数设置
-XX:UseConcMarkSweepGC
设置CMS GC(老年代用)
-XX:CMSInitiaingOccupanyFraction
设置堆内存使用率的阈值,默认92%,降低Full GC的次数
-XX:UseCMSCompactAtFullCollection
指定在执行完FullGC后对内存空间进行压缩整理
-XX:CMSFullGCBeforeCompaction
设置执行多少次FullGC后压缩整理
-XX:ParallelCMSThreads
设置CMS的线程数量(默认(ParallelGCTHreads + 3)/4)
G1回收器:区域化分代式
在延迟可控的情况下,尽可能获得高的吞吐量
G1垃圾回收
跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间以及回收所需时间),在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region
G1回收器特点
-
并行并发
并行性:G1在回收期间,可以有多个GC线程同时工作,多核处理
并发性:G1可以与应用程序交替执行
-
分代收集
堆空间分为若干区域,年轻代和老年代逻辑上连续,但内存空间上不连续
-
空间整合
每个Region之间使用复制算法,整体还可看做标记-压缩算法
-
可预测的停顿时间的模型
G1在大内存应用上优秀,在6~8G内存上
内存设置
-XX:+UseG1GC
设置G1
-XX:G1HeapRegionSize
设置每个Region大小,范围1~32MB,值2的幂,根据最小的堆大小划分2048个区域,默认堆内存1/2000
-XX:MaxGCPauseMillis
设置期望达到最大GC停顿时间,默认200ms
-XX:ParallelGCThread
设置STW时GC线程数
-XX:ConcGcThreads
设置并发标记的线程数
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的java堆占用率阈值,默认45
调优步骤
G1回收器的使用场景
- 面向服务端应用,正对大内存,多处理器
- 低延迟具有大堆的应用程序,比如堆6GB,可预测的暂停时间抵御0.5s
- 当超过50%的java堆被活动数据占用、对象分配频率或年代提升频率变化很大、GC停顿时间过长,G1比CMS好
- 当JVM的GC处理速度慢时,G1可以调用应用程序线程帮助加速垃圾回收过程
分区Region
所有Region大小相同,且在JVM生命周期内不会改变
一个Region属于一个角色->eden、survivor、old、humongous
humongous内存区域专门存储大对象,超过1.5个Region
如果一个H区存不下大对象,就可以寻找连续的H区存储,
Region内部使用指正碰撞存储对象,还可实现TLAB
G1回收器垃圾回收过程
-
Young GC
1.JVM启动的时候,会先准备好eden区,不断创建对象到eden
2.当年轻代的eden区用尽时开始年轻代回收,此时是并行独占式回收,G1创建回收集(被回收的内存分段集合包含eden和survivor)
3.之后从年轻代存活的对象到survivor区或old区,也有可能两个区都有
-
老年代并发标记过程
当堆内存使用达到一定值(45%)开始老年代并发标记过程
-
Mixed GC
混合回收,老年代存活对象移动到空闲内存,G1的老年代回收器不需要整个老年代别回收,一次只需要扫描一部分Region
-
Full GC
Remembered Set
G1收集器优化
总结
如何选择垃圾回收器
- 优先调整堆的大小让JVM自适应完成
- 如果内存大小小于100M,使用串行
- 如果是单核单机,使用没有停顿时间要求的串行
- 如果多CPU,需要高吞吐量,永续停顿超过1s选择并行,或者默认
- 如果多CPU,追求低停顿,快速响应,使用并发
GC日志分析