jvm概述
虚拟机
- 虚拟机是指通过软件模拟的具有完整硬件系统功能的,运行在一个完全隔离环境中的计算机系统
jvm
- jvm是通过软件来模拟java字节码的指令集,是java程序的运行环境
jvm的主要功能
- 通过ClassLoader寻找和装载class文件
- 解释字节码成为指令并执行,提供class的运行环境
- 进行运行期间的内存分配和垃圾回收
- 提供与硬件交互的平台
jvm定义的主要内容
- 字节码指令集(相当于中央处理器CPU)
- Class文件的格式
- 数据类型和值
- 运行时数据区
- 栈帧
- 特殊方法
- <init> 实例初始化方法,通过jvm的invokespecial指令调用
- <clinit> 类或接口的初始化方法,不包含参数,返回void
- 类库
- 异常的处理
- 虚拟机的启动,加载,链接和初始化
class文件
- class文件是jvm的输入,java虚拟机规范中定义了class文件的结构,class文件是jvm实现平台无关性的基础
asm
- asm是一个java字节码操纵框架,他能被用来动态生成类或者增强既有类的功能
- asm可以直接产生二进制class文件,也可以在类被加载入虚拟机之前动态改变类行为,asm从类文件中读取信息后,能够改变类行为,分析类信息,甚至根据要求产生新类
- 目前许多框架如cglib,hibernate,spring都直接或间接的使用asm操作字节码
asm编程模型
- Core API:提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存,但这种编程方式难度较大
- 功能主要基于ClassVisitor接口,这个接口中的每一个方法对应了class文件中的每一项
- asm提供了三个基于ClassVisitor接口的实现类来实现class文件的生成和转换
- ClassReader:解析一个类的class字节码
- ClassAdapter:实现要变化的功能
- ClassWriter:用来输出变化后的字节码
- 辅助开发工具ASMifier
# 使用辅助开发工具可以查看字节码的方法调用,方便使用ClassVisitor的实现类来从代码层面完成对字节码的修改
# 工具包括asm和asm-util(org.objectweb下的jar包,7.3.1和9.0均可用)
# 参考步骤:
# 1,对修改之前的类进行编译,进入到编译的包文件中(如class的包为com/mostall/jvm,则进入到com的上一级即可),执行以下命令,查看字节码反汇编代码
java -cp .:../lib/asm-9.0.jar:../lib/asm-util-9.0.jar org.objectweb.asm.util.ASMifier com.mostall.asm.CC
# 2,对类进行修改并编译,同样适用以上命令查看反汇编代码
# 3,对比两次反汇编代码的不同之处,将差异的代码通过使用asm和asm-util类进行实现
# 4,将类还原到修改前的状态,执行asm和asm-util的字节码操作方法
# 5,对比直接编码后的类和通过asm和asm-util操作后的字节码,可以发现实现了相同功能
- Tree API:提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存当中,所以这种方式需要更多的内存,这种编程方式较简单
类加载,连接和初始化
类的生命周期
- 类从被加载到jvm开始到卸载出内存的整个生命周期
-
生命周期详解
-
加载:查找并加载类文件的二进制数据
-
连接:就是将已经读入内存的类的二进制数据合并到jvm运行时环境中去,包含如下几个步骤
- 验证:确保被加载的类或接口的二进制表示在结构上是正确的
- 准备:为类的静态变量分配内存并初始化他们(初始化时按照普通变量和引用变量的初始默认值进行初始化)
- 解析:把常量池中的符号引用转换为直接引用
-
初始化:为类的静态变量赋初始值
-
类加载和类加载器
-
类加载要完成的功能
- 通过类的全限定名来获取该类的二进制字节流
- 把二进制的字节流转换为方法区的运行时数据结构
- 在堆上创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并向外提供了访问方法区内数据结构的接口
-
加载类的方式
- 最常见的方式:本地文件系统中加载,从jar等归档文件中加载
- 动态方式:将java源文件动态编译成class
-
类加载器
- java虚拟机自带的加载器包括如下几种
- 启动类加载器(BootstrapClassLoader)
- 平台类加载器(PlatformClassLoader,存在于jdk9及以后,替换jdk8的扩展类加载器)
- 扩展类加载器(ExtensionClassLoader,jdk8)
- 应用程序类加载器(AppClassLoader-通常默认用到的类加载器)
- 自定义加载器,是java.lang.ClassLoader的子类,用户可以定制类的加载方式;只不过自定义的类加载器其加载的顺序是在所有系统类加载器的最后
- java虚拟机自带的加载器包括如下几种
-
类加载器的关系
-
类加载器说明
- 启动类加载器:用于加载启动的基础模块类,比如:java.base,java.management,java.xml等,对于启动类加载器,通过getClass().getClassLoader()方法获取的类加载器将是null
- 平台类加载器:用于加载一些平台相关的模块,比如:java.scripting,java.compiler*,java.corba*等
- 应用程序类加载器:用于加载应用级别的模块,比如jdk.compile,jdk.jartool,jdk.jshell等;还加载classpath路径中的所有类库
-
jdk8类加载器说明
- 启动类加载器:负责将<JAVA_HOME>/lib,或者-Xbootclasspath参数指定的路径中的,且是虚拟机识别的类库加载到内存中(按照名字识别,比如rt.jar,对于不能识别的文件不预加载)
- 扩展类加载器:负责加载<JAVA_HOME>/lib/ext或者java.ext.dirs系统变量所指定路径中的所有类库
- 应用程序类加载器:负责加载classpath路径中的所有类库
-
类加载器其他说明
- java程序不能直接引用启动类加载器,直接设置classloader为null,默认就使用启动类加载器
- 类加载器并不需要等到某个类”首次主动使用“的时候才加载他,jvm规范允许类加载器在预料到某个类将要被使用的时候就预先加载他
- 如果在加载的时候.class文件缺失,会在该类首次主动使用时报告LinkageError错误,如果一直没有被使用,就不会报错
-
双亲委派模型
-
双亲委派的意思是如果一个类加载器需要加载类,那么首先他会把这个类加载请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这请求时,子类才会尝试去加载。
-
jvm中的ClassLoader通常采用双亲委派模型,要求除了启动类加载器之外,其余的类加载器都应该有自己的父级加载器。这里的父子关系是组合而不是继承,其工作流程如下
- 一个类加载器接收到类加载请求后,首先搜索他的类加载器定义的所有"具名模块"(模块化开发中定义的模块中包含模块中引入的类,模块通常需要指定名称,此即为具名模块)
- 如果找到了合适的模块定义,将会使用该加载器来加载
- 如果class没有在这些加载器中定义的具名模块中找到,那么将会委托给父级加载器,直到启动类加载器
- 如果父级加载器反馈它不能完成加载请求,比如在它的搜索路径下找不到这个类,那子的类加载器才自己来加载
- 在类路径下找到的类将成为这些加载器的无名模块
# 双亲委派机制实际上是双向的,从第三点和第四点可以完全体现出来,即当子类在对应的加载器定义的具名模块中找不到指定的class时,将委托给父级加载器来加载,返回来,如果父级加载器反馈它不能完成加载请求时,将又交回给子类加载类加载,在子类的classpath去寻找类 # 总体来说加载类有两种方式: # 1,从具名模块中加载 # 2,从classpath中加载,加载到的类将成为无名模块 # 注意,在jdk8中,没有模块化开发的概念,当类加载器收到类加载请求后,会直接委派给父级类加载器进行加载,没有从自己具名模块加载的过程
-
-
双亲委派模型说明
- 双亲委派模型对于保证java程序的稳定运行的重要性
- 公用且具有一致性,同样的类,尤其是一些公用的类都只会被加载一次,同时,越是公共的类,越是由父级加载器来加载,个性化的类由自己的加载器来完成,形成一颗稳定的树
- 同包同名的类不能被多次加载,保证了系统的类不会被恶意修改和覆盖
- 实现双亲委派的代码在java.class.ClassLoader的loaderClass方法中,如果自定义类加载器的话,推荐覆盖实现findClass方法
- 如果有一个类加载器能加载某个类,则称此加载器为定义类加载器,所有能成功返回该类的Class的类加载器,都被成为初始类加载器
- 每个类加载器都有自己的命名空间,命名空间由该加载器及其所有父加载器所加载的类构成,不同的命名空间,可以出现类的全路径名相同的情况
- 运行时包由同一个类加载器的类构成,决定两个类是否属于同一个运行时包,不仅要看全路径名是否一样,还要看定义类加载器是否相同。只有属于同一个运行时包的类才能实现相互包内可见
- 双亲委派模型对于保证java程序的稳定运行的重要性
-
破坏双亲委派模型
- 双亲委派模型存在的问题:父加载器无法向下识别子加载器加载的资源,但通常需要这样的机制,例如java.sql.DriverManager是由平台加载器加载的,而其实现类com.mysql.cj.jdbc.Driver是有应用类加载器加载的
- 解决的方式:引入线程上下文类加载器,可以通过Thread的setContextClassLoader()进行加载器的传输,使得父级加载器可以直接使用子级类加载器(实际装载类的加载器)
类的连接
- 验证,验证阶段需要验证的内容
- 类文件结构检查,按照jvm规范规定的类文件结构进行
- 元数据验证:对字节码描述的信息进行语义分析,保证其符合java语言规范要求,如是否有父类,是否实现了父类的方法等
- 字节码验证:通过对数据流和控制流(if-else,for...)进行分析,确保程序语义是合法和符合逻辑的。这里主要是对方法体进行校验
- 符号引用验证:对类自身以外的信息,也就是常量池中各种符号引用,进行匹配校验,如能否访问到其他类的方法等
- 准备,给变量分配内存空间,对于常量直接赋值为指定的值,对于静态变量赋值为默认值(基本类型为0或false,引用类型为null)
- 解析,所谓解析就是把常量池中的符号引用转换为直接引用的过程
- 符号引用:在编译阶段,还不知道所引用的对象的实际地址,使用一个符号来代替
- 直接引用:直接指向目标对象的指针,即真正引用该对象
- 主要针对:类,接口,字段,类方法,接口方法,方法类型,方法句柄,调用点限定符
类的初始化
-
类的初始化就是为类的静态变量赋初始值,或者说是执行类构造器<clinit>方法(非实例构造器方法,由jvm调用,专门处理类初始化的工作)的过程,是一个类或者接口在使用前的最后一项工作,<clinit>方法由静态属性显式赋值语句和静态代码块组成,其执行顺序为从上到下顺序执行且只会执行一次,jvm会保证执行clinit是线程安全的
- 如果类还没有加载和连接,就先加载和连接
- 如果类存在父类,且父类没有初始化,就先初始化父类
- 如果类中存在初始化语句,就依次执行这些初始化语句
- 如果是接口的话
- 初始化一个类的时候,并不会初始化它实现的接口
- 初始化一个接口时,并不会初始化它的父接口
- 只有当程序首次使用接口里面的变量(接口中的变量也就是常量)或者调用接口方法的时候,才会导致接口初始化
- 调用ClassLoader类的loadClass方法类装载一个类,并不会初始化这个类,因为不是对类的主动使用
-
类初始化时机
- java程序对类的使用方式分成主动使用和被动使用,jvm必须在每个类或接口"首次主动使用"时,才初始化他们;被动使用类不会导致类的初始化
- 主动使用的常见情形
- 创建类实例
- 访问某个类的静态变量
- 调用类的静态方法
- 反射某个类(Class.forName("类完全限定名"))
- 初始化某个类的子类,而此时该类还没有初始化时
- jvm启动时运行的主类
- 定义了default方法的接口,当接口实现类初始化时
- 被动使用的常见情形
- 通过子类引用父类的静态字段,此时子类即为被动使用的情形,不会导致子类初始化
- 通过数组定义类引用类,不会导致类初始化,例如
MyChild[] mcs = new MyChild[2];
不会导致MyChild被初始化 - 访问常量不会导致类初始化
-
注意
-
以上提到关于常量访问会不会导致类初始化时有分歧,原因是常量分为两种类型
- 编译时常量,包括基础数据类型和String类型的字面量(常量池中只能引用到基本类型和String类型的字面量),这种类型的常量在编译时就会被放入常量池中,对这种类型的常量进行引用时,不会导致类的初始化
- 运行时常量,包括其他的引用类型的常量,这种类型的常量在运行时才会被放入常量池中,对这种类型的常量进行引用时,会导致类的初始化
-
并不是所有类都有<clinit>方法,满足以下条件之一的类不会拥有<clinit>方法
- 该类既没有声明任何类型的变量,也没有静态初始化语句(静态块)
- 该类声明了静态变量,但没有明确使用静态初始化语句或静态初始化语句块
- 该类仅包含常量,且常量为基本数据类型或者String类型
-
实例对象初始化(顺带介绍)
- 实例对象初始化实际是执行<init>方法的过程,其执行的时期为对象的初始化阶段,一个类有几个构造器就有几个<init>方法,每次创建实例对象,都会调用对应的<init>方法,该方法同样由jvm进行调用,<init>方法由非静态实例属性显示赋值语句和非静态代码块以及对应的构造函数组成,其执行顺序也是自上而下顺序执行,但构造器的语句最后执行
- 实例化对象的四种途径
- 调用new操作符
- 调用Class或java.lang.reflect.Constructor对象的newInstance方法
- 调用任何现有对象的clone()方法
- 通过java.io.ObjectInputStream类的getObject方法反序列化
类的卸载
- 当代表一个类的Class对象不再被引用,那么Class对象的声明周期就结束了,对应的在方法区中的数据也会被卸载,此过程有虚拟机自动判断并执行
- jvm自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的
java内存分配
jvm简化架构图
- 线程共享的区域:堆、方法区
- 线程隔离的区域:虚拟机栈、本地方法栈、程序计数器
运行时数据区
-
PC(Program Counter Register,程序计数器)寄存器
- 每个线程拥有一个PC寄存器,是线程私有的,用来存储指向下一条指令的地址
- 在创建线程时,创建相应的PC寄存器
- 执行本地方法时,PC寄存器的值为undefined
- 它是一块较小的内存空间,是唯一一个在jvm虚拟机规范中没有规定outofmemoryerror的内存区域
-
java栈
- 栈是由一系列帧(Frame)组成(因此java栈也叫帧栈),是线程私有的
- 栈分为虚拟机栈和本地方法栈,虚拟机栈管理java方法,本地方法栈管理native方法
- 帧是用来保存一个方法的局部变量表,操作数栈(存放临时数据),常量池指针,动态链接,方法返回值等
- 每一次方法调用都会创建一个栈帧,并压入栈,方法返回时则对应出栈操作
- 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个slot存放32位数据,long,double占2个槽位
栈的优缺点
优点:
存取速度比堆快,仅次于寄存器
缺点:
存在栈中的数据大小,生存周期是在编译器决定的,缺乏灵活性 -
java堆
- 用来存放应用系统创建的对象和数组,所有线程共享java堆
- GC主要就是管理堆空间,对分代GC来说,堆也是分代的
堆的优缺点
优点:运行期动态分配内存大小,自动进行垃圾回收
缺点:效率相对较慢 -
方法区
- 方法区不是物理区域的划分,而是一个逻辑上的划分,它是线程共享的,通常用来保存装载的类的结构信息(运行时的常量池等)
- 方法区包括类信息、静态变量、字符串常量池、运行时常量
方法区与堆存在交叉区域,静态变量和字符串常量池是存放在堆中的,类信息和运行时常量池是存放在元空间的
- 通常和元空间(java8以前叫永久代)关联在一起,但具体跟虚拟机的实现和版本有关
- jvm规范把方法区描述为堆的一个逻辑部分,但它有个别名为Non-heap(非堆),是为了与java堆区分开
-
运行时常量池(其实是属于方法区,由于方法区一部分在元空间,一部分在堆中,所以此处单独列出)
- 是Class文件中每个类或接口的常量池表在运行期间的表示形式,通常包括:类的版本,字段,方法,接口等信息
- 在方法区中分配
- 通常在加载类和接口到jvm后,就创建相应的运行时常量池
-
本地方法栈
- 用于在jvm中支持native方法执行的栈
方法区中的常量池
- 静态常量池
静态常量池也叫class文件常量池,主要存放字面量(如文本字符串、final修饰的常量)、符号引用(例如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)
- 运行时常量池
当类加载的内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中;运行时常量池里存储的主要是编译期间生成的字面量、符号引用等
- 字符串常量池
字符串常量池也可以理解成运行时常量池分出来的一部分,类加载到内存的时候,字符串会存到字符串常量池里面
栈、堆、方法区交互关系
- 堆的内部结构
- 结构图
-
堆的结构
- 新生代用来存放新分配的对象;新生代中经过垃圾回收,没有回收掉的对象,被赋值到老年代
- 老年代存储对象通常比新生代存储对象的年龄大得多
- 老年代存储一些大对象(此时年龄不一定很大)
- 整个堆的大小 = 新生代 + 老年代
- 在cms垃圾回收器的日志中,分别用par new generation和concurrent mark-sweep generation表示新生代和老年代
- 新生代 = 伊甸区 + 存活区
- 在cms垃圾回收器的日志中,分别用eden和from或者to表示伊甸区和存活区
- 以前的持久代用来存储Class,Method等原信息的区域,从jdk8开始,取而代之的时元空间(MetaSpace),元空间并不在虚拟机里面,而是直接使用本地内存
-
对象的内存布局
- 对象在内存中存储的布局(以HotSpot虚拟机为例来说明),分为:对象头,实例数据和对齐填充
- 对象头包含两个部分
- Mark Word:存储对象自身的运行数据,如HashCode,GC分代年龄,锁状态标识等
- 类型指针,对象指向它的类元数据的指针
- 实例数据,真正存放对象实例数据的地方
- 对齐填充,这部分不一定存在,也没有什么特别含义,仅仅是占位符,因为HotSpot要求对象起始地址都是8字节的整数倍,如果不是,就进行对齐填充
-
对象的访问定位
-
在jvm规范中只规定了reference类型是一个指向对象的引用,但没有规定这个引用具体如何去定位以及访问堆中对象的具体位置,因此对象的访问方式取决于jvm的实现,目前主流的包括:使用句柄和使用指针两种方式
- 使用句柄,java堆中会划分出一块内存来作为句柄池,reference中存储句柄的地址,句柄中存储对象的实例数据和类元数据的地址
- 使用指针(HotSpot使用方式),java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址
-
java内存参数配置
- trace跟踪参数
- 打印GC的简要信息:-Xlog:gc
- 打印GC的详细信息:-Xlog:gc*
- 指定GC log的位置,以文件输出:-Xlog:gc:garbage-collection.log
- 每一次GC后,都打印堆信息:-Xlog:gc+heap=debug,debug为日志级别
- GC日志格式
- GC发生的时间,也就是jvm从启动以来经过的秒数
- 日志级别信息和日志类型标记
- GC识别号
- GC的类型和说明GC的原因
- 容量:GC前容量->GC后容量(该区域总容量)
- GC持续时间,单位秒,有的收集器会有更详细的秒数,比如:user表示应用程序消耗的时间,sys表示系统内核消耗的时间,real表示操作从开始到结束的时间
- java堆内存参数
- -Xms:初始堆大小,默认为物理内存的1/64(必须为1024的倍数,设置时建议与Xmx设置相同值,避免重新调整)
- -Xmx:最大堆大小,默认为物理内存1/4(必须为1024的倍数)
- -Xmn:新生代大小,默认整个堆的3/8
- -XX:+HeapDumpOnOutOfMemoryError:OOM时导出堆到文件
- -XX:+HeapDumpPath:导出OOM的路径
- -XX:OnOutOfMemoryError:在OOM时,执行一个脚本
- -XX:NewRatio:老年代与新生代的比值,如果xms=xmx,且设置了xmn的情况下,该参数不用设置
- -XX:ServivorRatio:Eden区和Servivor区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor占整个新生代1/10
- java堆内存分析工具
- eclipse下MAT(Memory Analyzer Tool)
- 使用-XX:+HeapDumpOnOutOfMemoryError导出对文件(*.hprof),使用mat插件打开查看
- idea下jprofiler插件和Jprofiler应用结合
- 使用-XX:+HeapDumpOnOutOfMemoryError导出对文件(*.hprof),使用Jprofiler应用打开查看
- eclipse下MAT(Memory Analyzer Tool)
- java栈内存参数
- -Xss:通常只有几百K,决定了函数调用的深度
- 元空间参数
- -XX:MetaspaceSize:初始元空间大小
- -XX:MaxMetaspaceSize:最大空间,默认是没有限制的
- -XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比
- -XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比
字节码执行引擎
字节码执行引擎
- jvm的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果
- 实现方式可能有通过解释器直接解析执行(按照命令的顺序,读一句解释语句,执行一句,此处读取的是类文件,程序流的指令集,非源文件)字节码,或者是通过即时编译器产生本地代码,也就是编译执行(执行更快),也可能两者都有(HotSpot两者皆有)
栈帧和局部变量表
-
栈帧概述
- 栈帧是用于支持jvm进行方法调用和方法执行的数据结构
- 栈帧会随着方法调用而创建,随着方法结束而销毁
- 栈帧中存储了方法的局部变量,操作数栈,动态连接,方法返回地址等信息
-
栈帧的概念结构
-
局部变量表:用来存放方法参数和方法内部定义的局部变量的存储空间
- 以变量槽slot为单位,目前一个slot存放32位以内的数据类型
- 对64位的数据占2个slot
- 对于实例方法,第0位slot存放的时this,然后从1到n,依次分配给参数列表,然后根据方法体内部定义的变量顺序和作用域来分配slot
- slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为
-
操作数栈:用来存放方法运行期间,各个指令操作的数据
- 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
- 虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用的数据
-
动态连接:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态连接
- 静态解析:类加载的时候,符号引用就转换为直接引用
- 动态解析:运行期间转换为直接引用
-
方法返回地址:方法执行后返回的地址
-
方法调用:方法调用就是确定具体调用哪一个方法,并不涉及方法内部的执行过程
- 部分方法是直接在类加载的解析阶段,就确定了直接引用关系(如静态方法,私有方法,实例构造器,父类方法等)
- 但是对于实例方法,也称虚方法,因为存在重载和多态,需要运行期间动态委派
-
分派:分为静态分派和动态分派
- 静态分派:所有依赖静态类型(传入的参数类型)来定位方法执行版本的分派方式,比如:重载方法
- 动态分派:根据运行期的实际类型来定位方法执行版本的分派方式,比如:覆盖方法
- 单分派和多分派:就是按照分派思考的维度,多于一个就算多分派,只有一个就是单分派
- 如何执行方法中的字节码指令:jvm通过基于栈的字节码解释执行引擎来执行指令,jvm的指令集也是基于栈的
-
垃圾回收
垃圾回收概述
-
什么是垃圾:简单说就是内存中已经不再被使用到的内存空间就是垃圾
-
垃圾回收的区域:堆和方法区
-
垃圾的判定方法
- 引用计数法(不使用):给对象添加一个引用计数器,有引用就加一,引用失效就减一
- 优点:实现简单,效率高;缺点:不能解决对象之间循环引用的问题
- 根搜索算法(可达性分析法,主流方法)
- 引用计数法(不使用):给对象添加一个引用计数器,有引用就加一,引用失效就减一
-
根搜索算法
- 从根(GC Roots)节点向下搜索对象节点,搜索走过的路径称为引用链,当一个对象到根之间没有连通的话,则该对象不可用
- 可作为GC Roots的对象包括:虚拟机栈(栈帧局部变量)中引用的对象,方法区类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象
- HotSpot使用了一组叫做OopMap的数据结构(描述对象间引用关系的数据结构,类加载完成后由虚拟机进行计算和记录)达到准确式GC的目的,从而不用每次回收都从GC Roots节点开始扫描
- 在OopMap的协助下,jvm可以很快的做完GC Roots枚举,但是jvm并没有为每一条指令生成一个OopMap
- OopMap会在特定位置去记录对象引用的信息,记录OopMap的这些“特定位置”被称为安全点,即当前线程执行到安全点后才允许暂停进行GC
- 如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始GC都是安全的,那么这个区域称为安全区域
-
引用分类
- 强引用:类似于Object a = new A()这样,不会被回收
- 软引用:还有用但并不必须的对象,用SoftReference来实现软引用,如果垃圾回收了内存还不够则会回收软引用对象
- 弱引用:非必须对象,比软引用还要弱,垃圾回收时会回收掉。用WeakReference来实现弱引用
-
虚引用:也称为幽灵引用或者幻影引用,是最弱的引用。垃圾回收时会回收掉。用PhantomReference来实现虚引用
-
跨代引用
- 跨代引用:也就是一个代中的对象引用另一个代中的对象
- 跨代引用假说:跨代引用相对于同代引用来说只是极少数
- 隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的
-
判断类实例是否是垃圾的步骤
- 根据搜索算法判断不可用
- 看是否有必要执行finalize方法(该方法只会执行一次,通常用于被回收时的自救操作,当自救一次后再次被回收时不能再自救)
- 两个步骤走完后对象仍然没人使用,那就属于垃圾
-
判断类无用的条件
- jvm中该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 没有任何地方引用该类的Class对象
- 无法在任何地方通过反射访问这个类
-
GC类型
- MinorGC/YoungGC:发生在新生代的收集动作
- MajorC/OldGC:发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为
- MixedGC:收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为
- FullGC:收集整个Java堆和方法区的GC
-
STW
- STW(Stop-The-World)是Java中一种全局暂停的现象,多半由于GC引起,所谓全局暂停,就是所有的Java代码停止运行,native代码可以执行,但不能和jvm交互。
- 其危害是长时间服务停止,没有响应,一般情况下,这种现象需要极力避免,垃圾收集器的发展方向缩短STW的时间
-
垃圾收集类型
- 串行收集:GC单线程内存回收,会暂停所有的用户线程,如:Serial
- 并行收集:多个GC线程并发工作,此时用户线程是暂停的,如:Parallel
- 并发收集:用户线程和GC线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,如:CMS
垃圾回收算法
-
标记清除算法(Mark-Sweep)
-
算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象
-
优缺点分析
- 优点:实现简单
- 缺点:效率不高,标记和清除的效率都不高;标记清除后会产生大量不连续的内存碎片,从而导致分配大对象时触发GC
-
-
复制算法(Coping)
-
把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉,新生代的存活区(from和to)即是使用的复制算法
-
优缺点分析
- 优点:实现简单,运行高效,不存在内存碎片问题
- 缺点:内存空间有些浪费,实际使用的内存只占整个内存的一半
-
jvm实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块Survivor空间,回收时,把存活的对象复制到另外一块Survivor
-
HotSpot默认的Eden和Survivor比是8:1,也就是每次能用90%的新生代空间
-
如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代
# 分配担保 # 当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间担保,步骤如下: # 1,在发生MinorGC前,jvm会检查老年代的最大可用连续空间是否大于新生代所有对象的总空间,如果大于,可以确保MinorGC是安全的 # 2,如果小于,那么jvm会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小 # 3,如果大于,则尝试进行一次MinorGC # 4,如果不大于,则改做一次FullGC
-
-
标记整理算法(Mark-Compact)
-
由于复制算法在存活区对象比较多的时候,效率较低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理算法
-
标记整理算法的标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存
-
垃圾收集器
-
概述
- 垃圾收集器是对垃圾收集算法的具体实现
- 不同厂商,不同版本的虚拟机实现差别很大
-
HotSpot包含的收集器如下
-
串行收集器
-
Serial(串行)收集器/Serial Old收集器,是一个单线程收集器,在垃圾收集时,会Stop-The-World
-
优点:实现简单,对于单cpu,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器
-
使用-XX:UseSerialGC来开启,开启后,会使用Serial+Serial Old的组合来分别进行新生代和老年代的垃圾回收
-
新生代使用复制算法,老年代使用标记整理算法
-
-
并行收集器
-
ParNew(并行)收集器使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World
-
在并发能力好的CPU环境里,它停顿的时间要比串行收集器短;但对于单CPU或者并发能力较弱的CPU,由于多线程的交互开销,可能比串行收集器更差
-
是Server模式下首选的新生代收集器,且能和CMS收集器配合使用
-
需要使用ParNew收集器时,不需要单独开启,直接使用-XX:+UseConcMarkSweepGC即可在新生代使用ParNew收集器(原因是二者是配合使用的)
-
-XX:ParallelGCThreads:指定线程数量,最好与CPU数量一致
-
新生代使用复制算法
-
-
其他并行收集器
-
新生代Parallel Scavenge收集器
- 新生代Parallel Scavenge收集器/Parallel Old收集器,是一个应用于新生代的,使用复制算法的并行收集器
- 跟parNew类似,但更关注吞吐量,能最高效的利用CPU,适合运行后台应用
- 使用-XX:+UseParallelGC来开启
- 使用-XX:+UseParallelOldGC来开启老年代使用Parallel Old收集器,使用Parallel Scavenge+Parallel Old的收集器组合
- -XX:MaxGCPauseMillis:设置GC的最大停顿时间
- 新生代使用复制算法,老年代使用标记整理算法
-
-
CMS收集器
-
CMS(Concurrent Mark and Sweep并发标记清除)收集器运行步骤
- 初始标记:只标记GC Roots能直接关联到的对象,从根节点往下只找一层
- 并发标记:进行GC Roots Tracing的过程,从根节点的一级节点往下追踪直到叶子节点,确认对象是否为垃圾
- 重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 并发清除:并发回收垃圾对象
-
在初始标记和重新标记两个阶段还是会发生Stop-the-World
-
使用标记清除算法,多线程并发收集的垃圾收集器
-
最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次收集做准备
-
优缺点
- 优点:低停顿,并发执行
- 缺点:并发执行,对CPU资源压力大;无法处理在程序处理过程中生成的垃圾(与用户线程并行),可能导致Full GC;采用标记清除算法会导致大量的碎片,从而在分配大对象时可能触发Full GC
-
开启:-XX:UseConcMarkSweepGC:使用ParNew+CMS+Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
-
-
G1(Garbage-First)收集器
-
G1收集器是一款面向服务端应用的收集器,与其他收集器相比,有如下特点
- G1把内存划分成多个独立的区域(Region)
- G1仍然采用分代思想,保留了新生代和老年代,但他们不是物理隔离的,而是一部分Region的集合,且不需要Region是连续的
- G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
- G1整体上采用标记-整理算法,局部采用复制算法,不会生成内存碎片
- G1的停顿时间可预测,能明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时间,但如果时间设置太短,将会增加触发收集的频率
- G1会跟踪各个Region里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限的时间内的高效收集
- G1运行的几个阶段(与CMS收集器非常相似):
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 最终标记(CMS的重新标记):修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化的回收
-
使用和配置G1:-XX:+UseG1GC:开启G1
-
-XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,jvm将尽可能(但不保证)停顿小于这个时间
-
-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
-
-XX:MaxTenuringThreshold=n:新生代到老年代的岁数,默认为15
-
-XX:G1HeapRegionSize=n:设置的G1区域的大小,值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域
-
-
ZGC收集器
- ZGC收集器是jdk11加入的具有实验性质的低延迟收集器
- ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%
- ZGC收集器是jdk11加入的具有实验性质的低延迟收集器
GC性能指标
- 吞吐量=应用代码执行的时间/运行的总时间
- GC负荷,与吞吐量相反,是GC时间/运行的总时间
- 暂停时间,就是发生Stop-the-World的总时间,交互式应用通常希望暂停时间越少越好
- GC频率,就是GC在一个时间段发生的次数
- 反应速度,就是从对象成为垃圾到被回收的时间
jvm内存配置原则
- 新生代尽可能设置大点,如果太小会导致
- 新生代GC次数更加频繁
- 可能导致新生代GC的对象进行老年代,如果此时老年代满了,会触发Full GC
- 对老年代,针对响应时间优先的应用,由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数
- 如果内存设置过小,高频率回收会导致应用暂停
- 如果内存设置过大,会需要较长的回收时间
- 对老年代,针对吞吐量优先的应用,通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象
- 依据对象的存活周期进行分类,如果对象的存活时间都较短,则新生代可分配更大空间,反之可分配更小空间
- 根据不同代的特点,选取合适的收集算法,少量对象存活,适合复制算法;大量对象存活,适合标记清除或标记整理算法
高效并发
java内存模型
-
内存模型
- 内存模式是在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象,简单说内存模型就是抽象怎样跟内存进行交互,怎样读写内存
- java内存模型主要关注jvm中把变量值存储到内存和从内存中读取变量值这样的底层细节
-
内存模型的基本要求
- 所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存;工作内存中保存该线程使用到的变量的主内存副本拷贝
- 线程对变量的所有操作(读和写)都应该在工作内存中完成
- 不同线程间不能相互访问工作内存,交互数据需要通过主内存
-
内存间的交互操作
- java内存模型规定了一些操作来实现内存间交互,jvm会保证它们是原子的
- lock:锁定,把变量标识为线程独占,作用于主内存变量
- unlock:解锁,把锁定的变量释放,别的线程才能使用,作用于主内存变量
- read:读取,把变量值从主内存读取到工作内存
- load:载入,把read读取到的值放入工作内存的变量副本中
- use:使用,把工作内存中一个变量的值传递给执行引擎
- assign:赋值,把从执行引擎中接收到的值赋给工作内存中的变量
- store:存储,把工作内存中一个变量的值传递到主内存中
- write:写入,把store进来的数据存放到主内存的变量中
- java内存模型规定了一些操作来实现内存间交互,jvm会保证它们是原子的
-
多线程中的可见性
- 可见性就是一个线程修改了变量,其他线程可以知道
- 保证可见性的常见方法:volatile,synchronized,final(一旦初始化完成,其他线程就可见)
- volatile
- volatile基本上是jvm提供的最轻量级的同步机制,用volatile修饰的变量,对所有的线程可见,即对volatile变量所做的写操作能立即反映到其他线程中
- 用volatile修饰的变量,在多线程环境下仍然是不安全的
- 用volatile修饰的变量,是禁止指令重排优化的
- 适合使用的场景
- 运算结果不依赖变量的当前值(依赖当前值的操作往往是多步执行且非原子操作的)
- 能确保只有一个线程修改变量的值
-
指令重排
- 指令重排指的是jvm为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行的后续指令,避开获取下一条指令所需数据造成的等待
- 不是所有的指令都能重排,例如
- 写后读:a=1;b=a;写一个变量之后,再读这个位置
- 写后写:a=1;a=2;写一个变量之后,再写这个变量
- 读后写:a=b;b=1;读一个变量之后,再写这个变量
- 指令重排基本规则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先于读发生
- 锁规则:解锁(unlock)必然发生在随后的加锁前
- 传递性:A先于B,B先于C,则A必然先于C
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join)
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
-
多线程中的有序性
- 在本线程内,操作都是有序的
- 在线程外观察,操作都是无序的,因为存在指令重排或主内存同步延时
java线程安全的处理方法
-
不可变是线程安全的(如:使用final修饰的变量)
-
互斥同步(阻塞同步):synchronized、ReentrantLock。建议优先使用synchronized,ReentrantLock增加了如下特性
-
等待可中断:当持有锁的线程长时间不释放锁,正在等待的线程可以选择放弃等待
-
公平锁(默认为非公平锁):多个线程等待同一个锁时,严格按照申请锁的时间顺序来获得锁
-
锁绑定多个条件:一个ReentrantLock对象可以绑定多个condition对象,而synchronized是针对一个条件的,如果要多个,就得有多个锁
-
-
非阻塞同步:是一种基于冲突检查的乐观锁定策略,通常是先操作,如果没有冲突,操作成功,否则,需要采取其他方式进行补偿处理
-
无同步方案:其实就是在多线程中,方法并不涉及贡献数据,自然也就无需同步了
锁优化(有虚拟机自动进行优化)
-
自旋锁与自适应自旋锁
- 自旋:如果线程可以很快获取到锁,那么可以不在操作系统层挂起线程,而是让做几个忙循环
- 自适应自旋:自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定
- 如果锁被占用时间很短,使用自旋锁能节省线程挂起以及切换时间,从而提升系统性能
-
锁消除
- 在编译代码的时候,检测到根本不存在共享数据竞争,自然也就无需同步加锁了;通过-XX:+EliminateLocks来开启,同时要使用-XX:+DoEscapeAnalysis开启逃逸分析
- 如果一个方法中定义一个对象,可能被外部方法引用,称为方法逃逸
- 如果对像可能外部线程访问,称为线程逃逸,比如赋值给类变量或者可以在其他线程中访问的实例变量
- 在编译代码的时候,检测到根本不存在共享数据竞争,自然也就无需同步加锁了;通过-XX:+EliminateLocks来开启,同时要使用-XX:+DoEscapeAnalysis开启逃逸分析
-
锁粗化
- 通常我们都要求同步块要小,但一系列连续的操作导致对一个对象反复的加锁和解锁,将导致不必要的性能损耗,这种情况建议把锁同步的范围加大到整个操作序列
-
轻量级锁
- 轻量级是相对与传统锁机制而言的,本意是没有多线程竞争的情况下,减少传统锁机制使用操作系统实现互斥所产生的性能损耗
- 实现原理:类似乐观锁的方式
- 如果轻量级锁失败,表示存在竞争,此时升级为重量级锁,导致性能下降
- 轻量级是相对与传统锁机制而言的,本意是没有多线程竞争的情况下,减少传统锁机制使用操作系统实现互斥所产生的性能损耗
-
偏向锁
- 偏向锁是在无竞争情况下,直接把整个同步消除了,连乐观锁都不用,从而提高性能;所谓的偏向,即偏心,即锁会偏向于当前已经占有锁的线程
- 只要没有竞争,获得偏向锁的线程,在将来进入同步块也不需要做同步
- 当有其他线程请求相同的锁时,偏向模式结束
- 如果程序中大多数锁总是被多个线程访问的时候,也就是竞争比较激烈,偏向锁反而会降低性能
- 可以使用-XX:-UseBiasedLocking来禁用偏向锁,默认开启
- 偏向锁是在无竞争情况下,直接把整个同步消除了,连乐观锁都不用,从而提高性能;所谓的偏向,即偏心,即锁会偏向于当前已经占有锁的线程
-
jvm获取锁的步骤
- 会先尝试偏向锁;然后尝试轻量级锁
- 在尝试自旋锁
- 最后尝试普通锁,使用操作系统互斥量在操作系统层挂起
-
同步代码的基本规则
- 尽量减少锁持有时间
- 尽量减小锁的粒度
性能监控与故障处理工具
命令行工具
-
jvm监控工具的作用
- 对jvm运行期间的内部情况进行监控,比如:对jvm参数,cpu,内存,堆等信息的查看
- 辅助进行性能调优
- 辅助解决应用运行时的一些问题,例如oom,线程死锁,锁争用,java进程消耗cpu过高等
-
jps
- jps(jvm process status tool):主要用来输出jvm中运行的进程状态信息,语法格式如下
- jps [options] [hostid]
- hostid字符串的语法与uri的语法基本一致:[protocal:][//][hostname][:port][servername],如果不指定hostid,默认为当前主机或服务器
- jps(jvm process status tool):主要用来输出jvm中运行的进程状态信息,语法格式如下
-
jinfo
- 打印给定进程或核心文件或远程调试服务器的配置信息,语法格式如下
- jinfo [option] pid #pid 指定进程的进程号
- jinfo [option] <executable <core> # 指定核心文件
- jinfo [option] [server-id@]<remote-hostname-or-IP> # 指定远程调试服务器
- 打印给定进程或核心文件或远程调试服务器的配置信息,语法格式如下
-
jstack
- jstack主要用来查看某个java进程内的线程堆栈信息,语法格式如下
- jstack [option] pid
- jstack [option] <executable <core> # 指定核心文件
- jstack [option] [server-id@]<remote-hostname-or-IP> # 指定远程调试服务器
- jstack主要用来查看某个java进程内的线程堆栈信息,语法格式如下
-
jmap
- jmap用来查看堆内存使用状况,语法格式如下
- jmap [option] pid
- jmap [option] <executable <core> # 指定核心文件
- jmap [option] [server-id@]<remote-hostname-or-IP> # 指定远程调试服务器
- jmap用来查看堆内存使用状况,语法格式如下
-
jstat
- jstat是jvm统计监测工具,查看各个区内存和GC的情况
-
jstated
- jstated是虚拟机的jstat守护进程,主要用于监控jvm的创建与终止,并提供一个接口,以运行远程监视工具附加到本地系统上运行的jvm
-
jcmd
- jvm诊断命令工具,将诊断命令请求发送到正在进行的java虚拟机,比如可以用来导出堆,查看java进程,导出线程信息,执行GC等
图形化工具
- jconsole
- 一个用于监视java虚拟机的符合jmx的图形工具,它可以监视本地和远程jvm,还可以监视和管理应用程序
- jmc(Jdk mission control)
- java任务控制(jmc)客户端包括用于监视和管理java应用程序的工具,而不会引入通常与这些类型的工具相关联的性能开销
- visualvm
- 一个图形工具,它提供有关在java虚拟机中运行的基于java技术的应用程序的详细信息
- java visualvm提供内存和cpu分析,堆转储分析,内存泄露检测,访问mbean和垃圾回收
- 可直接使用命令行来启动jfr(java flight recorder),记录一段时间的内存运行情况,然后使用visualvm来进行飞行记录的分析,记录飞行记录的命令为:
- jcmd 进程号 JFR.start delay=10s duration=1m filename=/Users/mac-st/Desktop/log.jfr
远程连接
-
两种远程连接方式
- jmx连接可以查看:系统信息,CPU使用情况,线程多少,手动执行垃圾回收等比较偏向于系统级层面的东西
- jstatd连接方式可以查看:jvm内存分布详细信息,垃圾回收分布图,线程详细信息,甚至可以看到某个对象使用内存的大小
-
远程连接tomcat
-
配置jmx的支持,需要在tomcat的catalina.sh里添加一些设置,如下
CATALINA_OPTS="-Xms800m -Xmx800m -Xmn350m -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -Dcom.sun.management.jmxremote=true -Djava.rmi.server.hostname=192.168.1.105 -Dcom.sun.management.jmxremote.port=6666 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
-
配置jstatd
-
自定义一个java.policy文件,添加(jdk13)
grant codebase "jrt:/jdk.statd" { permission java.security.AllPermission; } grant codebase "jrt:/jdk.internal.jvmstat" { permission java.security.AllPermission; }
-
然后在JDK_HOME/bin下面运行jstatd
./statd -J-Djava.rmi.server.hostname=192.168.1.102 -J-Djava.security.policy=java.policy(上文中的java.policy文件) -p 1099 &
-
-
jvm调优
调什么
- 调什么
- 内存方面
- jvm需要的内存总大小
- 各块内存分配,新生代,老年代,存活区
- 选择合适的垃圾回收算法,控制GC停顿的次数和时间
- 解决内存泄漏的问题,辅助代码优化
- 内存热点:检查哪些对象在系统中数量最大,辅助代码优化
- 线程方面
- 死锁检查,辅助代码优化
- dump线程详细信息:查看线程内部运行情况,查找竞争线程,辅助代码优化
- cpu热点:检查系统哪些方法占用了大量的CPU时间,辅助代码优化
- 内存方面
如何调优
- 如何调优
- 监控jvm的状态,主要是内存,线程,代码,io部分
- 分析结果,判断是否需要优化
- 调整:垃圾回收算法和内存分配,修改并优化代码
- 不断的重复监控,分析,调整,直至找到优化的平衡点
- jvm调优目标
- GC的时间足够小
- GC的次数足够少
- 将转移到老年代的对象数量降低到最小
- 较少Full GC的执行时间
- 发生Full GC的间隔足够的长
- 常见调优策略
- 减少创建对象的数量
- 减少使用全局变量和大对象
- 调整新生代和老年代的大小到最合适
- 选择合适的GC收集器,并设置合理的参数
- jvm调优冷思考
- 多数的java应用不需要在服务器上进行GC优化
- 多数导致GC问题的Java应用,往往都不是因为参数设置错误,而是代码问题
- 在应用上线之前,应该考虑将服务器的jvm参数设置到最优
- jvm优化是到最后不得已才采用的手段
- 在实际使用中,分析jvm情况优化代码比优化jvm本身多得多
- 如下情况通常不用优化
- MinorGC执行时间不到50ms
- MinorGC执行不频繁,约10s或以上一次
- FullGC执行时间不到1s
- FullGC执行频率不算频繁,不低于10分钟一次
jvm调优经验、内存泄露分析
- 调优经验
- 要注意32位和64位的区别,通常32位的仅支持2-3G内存
- 要注意client模式和server模式的选择
- 要想GC时间小,必须要一个更小的堆,而要保证GC的次数足够少,又必须保证一个更大的堆,这两个是冲突的,只能取其平衡
- 针对jvm堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小,最大值之间收缩堆而产生额外的时间,通常把最大和最小值设置为相同时间
- 新生代和老年代将根据默认的比例(1:2)分配对内存,可以通过调整二者之间的比率NewRatio来调整,也可以通过-XX:newSize -XX:MaxNewSize来设置其绝对大小,同样,为了防止新生的堆收缩,通常会把-XX:newSize和-XX:MaxNewSize设置为同样大小
- 合理规范新生代和老年代的大小
- 如果应用存在大量的临时对象,应该选择更大的新生代;如果存在相对较多的持久对象,老年代应该适当增大。在选择时应该本着FullGC尽量少的原则,让老年代尽量缓存常用对象,jvm的默认比例1:2也是这个道理
- 通过观察应用一段时间,看其在峰值时老年代会占多少内存,在不影响FullGC的前提下,根据实际情况加大新生代,但应该给老年代至少预留1/3的增长空间
- 线程堆栈的设置,每个线程默认会开启1M的堆栈,用于存放栈帧,调用参数,局部变量等,对大多数应用而言这个默认值太大了,一般256K就足够了。在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程
- 内存泄露
- 内存泄露导致系统崩溃前的一些现象,比如
- 每次垃圾回收的时间越来越长,FullGC时间也延长到好几秒
- FullGC的次数越来越多,最频繁时间隔不到1分钟就进行一次FullGC
- 老年代的内存越来越大,并且每次FullGC后老年代没有内存被释放
- 老年代堆空间被占满的情况
- 堆栈溢出的情况,通常抛出StackOverflowError的异常(递归没有退出,或循环调用导致)
- 内存泄露导致系统崩溃前的一些现象,比如
调优实战
内容补充
内存分配部分
- 为对象分配内存的基本方法:指针碰撞法,空闲列表法
- 内存分配并发问题的解决:CAS,TLAB(为每个线程预分配一部分堆空间,从这个方面来说,在分配内存时,堆空间不全是共享的,分配完成后才是共享的)