JVM
2024-05-08 14:57:23 0 举报
JVM思维笔记
作者其他创作
大纲/内容
垃圾收集器
parNew
触发Minor GC
Eden区没有足够空间分配给新创建的对象
CMS
触发majorGC
heap
Eden
S0
S1
类的生命周期
加载
类加载是在程序运行时动态进行的,只有在需要使用某个类时,JVM才会加载该类
创建类的实例
当通过关键字 new 创建类的实例时,JVM会加载该类。
访问类的静态变量或静态方法
如果通过类名直接访问静态成员,会触发类加载。
调用类的静态方法
当通过类名调用静态方法时,JVM会加载该类。
初始化子类
如果一个类初始化时,它的父类还没有被初始化,那么父类也会被加载并进行初始化。
使用反射机制
使用Java的反射机制,例如通过 Class.forName("ClassName") 方法来加载类时,会触发类加载。
启动应用程序时的主类
在Java应用程序启动时,JVM会通过指定的主类(包含 public static void main(String[] args) 方法的类)来启动应用程序,这个主类也会被加载。
加载可以分为三个步骤:
通过类的全限定名转换成类的二进制流。
将字节流代表的静态存储结构转换为方法区的运行时数据结构。
在内存中生成一个代表该类的Class对象,作为方法区中该类数据的访问入口。
连接
验证
准备
在准备阶段,JVM为类的静态变量分配内存,并设置默认初始值。这里所说的静态变量是使用static关键字定义的变量。
这个阶段不会执行静态变量的初始化赋值操作,只会分配内存并设置初始值,初始值通常为零或null。
解析(只作用于静态链接)
符号引用(Symbolic Reference)
符号引用是一种编译时的引用,它以一种符号化的形式来表示目标。在符号引用阶段,引用的目标尚未被解析成直接的内存地址或偏移量。
符号引用包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等信息。这些信息都是在编译阶段生成的,不涉及具体的内存地址。
通过符号引用,Java程序可以使用符号化的方式来引用类、字段和方法,而不需要关心具体的内存布局。
直接引用(Direct Reference)
直接引用是在解析阶段将符号引用转换为直接的内存地址或偏移量的引用。这是在程序运行时实际执行的引用。
通过直接引用,Java虚拟机可以直接定位并访问内存中的类、字段或方法。
直接引用的生成是解析阶段的最后一步,它将符号引用解析为具体的内存地址,从而实现对类、字段和方法的直接访问。
静态链接(Static Linking)
主要涉及到类加载的解析阶段。将符号引用解析为直接引用,以便在程序运行时能够直接定位和访问类、字段和方法。
动态链接(在程序运行时动态地加载、链接和执行类)
典型场景
运行时类加载
Java程序可以在运行时通过类加载器加载新的类。这种机制允许程序在不重启的情况下动态地添加、卸载和更新类,实现灵活的扩展和插件机制。
反射(Reflection)
反射是Java语言的一项强大的特性,允许在运行时检查和操作类、方法和字段。通过反射,程序可以动态地获取类的信息、创建实例、调用方法,从而实现一些高级的编程模式。
动态代理(Dynamic Proxy)
Java的动态代理机制允许在运行时创建接口的代理类。这种代理类可以在运行时动态地处理方法的调用,通常用于实现一些横切关注点,如日志、事务等。
模块化系统
在Java 9及更高版本中,引入了模块化系统(Project Jigsaw)。模块允许程序以模块化的方式组织和管理代码,支持在运行时动态地添加、移除和更新模块。
OSGi(Open Service Gateway Initiative)
OSGi是一个动态模块化系统,允许在运行时动态地安装、卸载和更新Java组件。这对于大型复杂的应用程序和服务体系结构非常有用。
热部署(Hot Deployment)
一些应用服务器(如Tomcat、JBoss等)允许在运行时热部署应用,即在不停机的情况下动态地部署、更新和卸载应用。
初始化
在初始化阶段,JVM负责执行类的初始化代码,包括静态变量的赋值和静态块的执行。
这是类加载过程的最后一步,也是真正开始执行程序的阶段。
类的初始化是在对类进行首次主动引用时触发的。主动引用包括创建类的实例、访问类或接口的静态变量、调用类或接口的静态方法等。
类加载器
JDK8
启动类加载器(BootstrapClassLoader)
<JAVA_HOME>/lib
-Xbootclasspath
用C C++码来实现的,不是Java类,因此在Java中无法直接获取到该类加载器的引用。
JDK8: 扩展类加载器(Extension ClassLoader )
<JAVA_HOME>/lib/ext
java.ext.dirs
应用程序类加载器(AppClassLoader)
classpath
JDK9+
启动类加载器(BootstrapClassLoader)
平台类加载器(PlatformClassLoader)
应用程序类加载器(AppClassLoader)
主要作用
加载类文件
类加载器负责从文件系统、网络或其他地方加载类的字节码文件到内存中。类加载是Java虚拟机启动和运行时的第一步,确保了类在运行时可用。
创建Class对象
加载类的字节码后,类加载器会将其定义成一个Class对象,这个对象包含了类的各种信息,如类的字段、方法、构造方法等。这个Class对象是程序在运行时访问类的入口。
命名空间隔离
每个类加载器都有自己的命名空间,相同名称的类可以被不同的类加载器加载,它们之间是隔离的。这确保了类的独立性,避免了不同类之间的命名冲突。
双亲委派模型
类加载器采用双亲委派模型,即在加载类的时候,先委派给父类加载器去尝试加载。这样做的好处是避免了类的重复加载,提高了类加载的效率,并确保了类的一致性。
动态加载
类加载器支持动态加载类,即在程序运行时可以动态地加载新的类。这为实现一些插件机制、动态扩展和热部署提供了可能性。
实现类加载的细节
类加载器可以根据实际需求,实现一些额外的加载行为。例如,可以通过自定义类加载器实现类的解密、动态生成、加载网络上的类等。
类加载器初始化过程
核心方法:sun.misc.Launcher#getLauncher
双亲委派机制
作用
沙箱安全机制
避免类的重复加载
全盘负责委托机制
委托机制
当一个类加载器收到加载类的请求时,它首先会将这个请求委托给它的父类加载器。这个过程一直持续到达到最顶层的启动类加载器。如果父类加载器能够成功加载该类,那么加载过程结束;否则,子类加载器会尝试加载。
全盘负责
类加载器在加载一个类时,不仅仅负责加载该类,还负责加载该类所依赖的其他类。这个机制确保了在加载某个类时,其所依赖的类也能够通过相同的加载器加载,从而保持整个类加载层次结构的一致性。
防止重复加载
通过委托机制,避免了同一个类被多个类加载器加载的情况。因为一个类加载器在加载某个类时,首先会尝试委托给父类加载器,而父类加载器如果能够成功加载,就不会再由子类加载器尝试加载,从而避免了重复加载。
自定义类加载器
继承java.lang.ClassLoader
重写findClass
重写loadClass会破坏双亲委派模型
自定义的类加载器在实例化时,如果没有显示调用 ClassLoader构造方法 ,则会隐式调用,此时会将 AppClassLoader 设置为自定义类加载器的父类
打破双亲委派机制
使用场景
Tomcat实现隔离性
热部署
JNDI服务
SPI
通过Thread的setContextClassLoader()
重写loadClass方法
命名空间
命名空间是指不同类加载器加载的类的隔离空间
每个类加载器都有自己的命名空间,这意味着由不同的类加载器加载的类,即使它们的类名和包名完全相同,也被认为是不同的类。这确保了类的唯一性,避免了类之间的冲突
不同类加载器加载的类之间是不能直接进行类型转换的,因为它们属于不同的命名空间。
命名空间的特性使得在不同的加载环境中可以加载相同类名的不同版本,或者加载不同的类库版本,而不会发生冲突。这对于实现模块化、热加载以及隔离不同应用程序的类加载环境非常有用。
JVM内存模型
程序计数器(Program Counter Register)
存储当前线程执行的字节码指令的地址。
每个线程都有一个独立的程序计数器。在线程切换时,程序计数器用于保存当前线程的执行状态,确保线程能够从正确的位置继续执行。
Java虚拟机栈(Java Virtual Machine Stacks)
栈帧(Stack Frame)
Java虚拟机栈以栈帧为单位,每个方法在执行时都会创建一个栈帧。栈帧包含了局部变量表、操作数栈、动态链接、方法出口等信息。当一个方法被调用时,就会创建一个新的栈帧,方法的参数和局部变量将被分配到这个栈帧的局部变量表中。
局部变量表(Local Variable Table)
局部变量表用于存储方法参数和方法内部定义的局部变量。在栈帧中,局部变量表的容量是提前确定的,每个局部变量的槽位大小是固定的。基本数据类型和对象引用都可以作为局部变量。
slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为
操作数栈(Operand Stack)
操作数栈用于执行操作指令,其中包含方法执行过程中的操作数。例如,两个数相加的指令会从操作数栈中取出两个数相加,并将结果压入栈中。
动态链接(Dynamic Linking)
动态链接部分包含一个指向运行时常量池中该栈帧所属方法的引用。在运行时,动态链接将方法调用转换为方法实际内存地址的过程。
方法出口(Return Address)
方法出口记录了方法调用结束后的返回地址。当一个方法调用完成时,程序将从调用该方法的位置继续执行。
本地方法栈(Native Method Stack)
类似于Java虚拟机栈,但用于执行本地方法,即由C或其他本地语言实现的方法。
本地方法栈也包含了栈帧的概念,但与Java虚拟机栈不同的是,它为本地方法的执行提供支持。
Java堆(Java Heap)
存储对象实例和数组。
所有线程共享Java堆,是垃圾回收的主要区域。堆的分配和回收对于Java程序的内存管理至关重要,垃圾回收负责释放不再使用的对象,防止内存泄漏。
方法区(Method Area)
存储类的元数据信息、静态变量、常量、编译后的代码等。
所有线程共享方法区。方法区中包含了类的结构信息,常量池,静态变量等。它也是永久代(在Java 7及之前的版本中)的一部分,而在Java 8及之后的版本中,永久代被元空间(Metaspace)所取代。
运行时常量池(Runtime Constant Pool)
存储编译时生成的常量,包括类、方法的引用、字符串常量等。
与方法区关联,用于支持动态性能优化。运行时常量池是在类加载的过程中,将编译时生成的常量池信息转换为运行时常量池的一部分。
在运行时可能会被动态修改,例如使用String.intern()方法
直接内存(Direct Memory)
与Java堆不同,不是虚拟机规范中定义的内存区域,但被划分为一部分内存。
主要通过ByteBuffer等类使用Native方法直接分配堆外内存。直接内存的使用可以提高I/O 操作性能,因为它可以通过本地调用直接与操作系统交互。
Class文件中的Constant Pool(编译时常量池)
存储在Class文件中,是编译器在编译期间生成的一系列符号引用和字面量常量。
包含字面量和符号引用,字面量可以直接存储,而符号引用需要在运行时解析。
内存参数设置
官网参数说明
vmoptions
-Xms
设置堆的初始可用大小,默认物理内存的1/64
-Xmx
设置堆的最大可用大小,默认物理内存的1/4
-Xss
线程的栈大小
Linux/ARM (32-bit): 320 KB
Linux/i386 (32-bit): 320 KB
Linux/x64 (64-bit): 1024 KB
macOS (64-bit): 1024 KB
Oracle Solaris/i386 (32-bit): 320 KB
Oracle Solaris/x64 (64-bit): 1024 KB
Windows: The default value depends on virtual memory
young generation
-Xmn
新生代的大小
Oracle 建议将年轻代的大小保持在整个堆大小的一半到四分之一之间。
太小则频繁回收
太大则回收时间过长
默认值
通常由JVM自动计算,根据堆的大小和-XX:NewRatio参数来确定,默认为堆大小的1/3
扩展
-XX:NewSize
设置新生代初始大小
等同于 -Xmn
-XX:MaxNewSize
设置新生代最大大小
-XX:NewRatio
默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
survivor space
-XX:SurvivorRatio
默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
Metaspace
-XX:MaxMetaspaceSize
设置元空间的最大值,默认为-1(表示不受此参数限制)
-XX:MetaspaceSize
元空间首次触发Fullgc的初始阈值(元空间无固定初始大小),这个参数只起触发作用, 以字节为单位,默认是21M左右
MetaspaceSize 的默认大小与平台有关,从 12 MB 到 20 MB 左右不等。
官网链接
建议将 -XX:MaxMetaspaceSize 和 -XX:MetaspaceSize 设置一样大
对象的创建
类加载
首先,类的字节码需要被加载到JVM中。这个步骤发生在程序启动时或者在首次使用类的时候。类加载由类加载器(ClassLoader)负责。
分配内存
一旦类被加载,JVM 会为对象分配内存。这通常是在堆内存中进行的,堆是专门用于存储对象的一块内存区域。
分配方式
指针碰撞(Bump the Pointer)(默认)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
初始化零值
在内存分配后,JVM 会对对象的内存空间进行初始化,将每个字段都设置为默认的零值。对于基本数据类型,零值是 0 或者 false,对于引用类型,零值是 null。
设置对象头
执行 <init> 方法
调用构造方法(构造函数)
接下来,执行对象的构造方法,对对象进行初始化。构造方法是一个特殊的方法,与类同名,没有返回值。构造方法可以设置对象的初始状态,为对象的属性赋值。
返回对象的引用
最后,对象创建完成,构造方法执行完毕,返回一个指向该对象的引用。这个引用可以被用来访问和操作对象。
对象大小与指针压缩
基本数据类型
整数类型
byte:1 字节(8 位)
short:2 字节(16 位)
int:4 字节(32 位)
long:8 字节(64 位)
浮点数类型
float:4 字节(32 位)
double:8 字节(64 位)
字符类型
char:2 字节(16 位)
布尔类型
boolean:理论上是 1 位,但在实际中通常使用 1 字节来存储
使用 jol-core 查询对象大小
在成员变量基础数据类型和引用类型之间可能会出现(alignment/padding gap),是因为需要对齐到2的整数次幂倍
指针压缩
启用指针压缩:-XX:+UseCompressedOops(默认开启)
所有指针(成员变量指针和klass point指针)都会压缩
同时开启klass point指针压缩:-XX:+UseCompressedClassPointers
禁止指针压缩:-XX:-UseCompressedOops
只开启klass point指针压缩:-XX:+UseCompressedClassPointers
适用于[4,32]G的范围区间
在内存中将35位指针压缩成32位,CPU使用时将32位解码成35位
2^32 = 4g
2^35 = 32g
对齐填充
对于大部分处理器,对象以8字节整数倍来对齐填充都是最高效的存取方式
在64位系统中,CPU的数据总线宽度是64位,也即8个字节。这意味着CPU一次可以读取8个字节的数据。
在32位系统中,CPU的数据总线宽度是32位,也即4个字节。这意味着CPU一次可以读取4个字节的数据
对象内存分配
栈内分配(依赖于逃逸分析和标量替换实现)
逃逸分析
逃逸行为
方法逃逸
如果一个对象在方法中被定义,然后被返回,那么我们称这个对象发生了方法逃逸。因为对象被返回后,方法外的其他方法就可能引用到这个对象。
线程逃逸
如果一个对象被跨线程共享,即存在一个线程写对象属性,其他线程读对象属性的情况,那么这个对象就发生了线程逃逸。
(JDK7后默认开启)开启:-XX:+DoEscapeAnalysis
关闭:-XX:-DoEscapeAnalysis
标量替换
(JDK7默认)开启标量替换:-XX:+EliminateAllocations
未出现逃逸行为,将聚合量替换成标量
堆内分配
Eden(8)
分配对象时,空间不足,触发一次Minor GC
Survivor
s0(1)
s1(1)
tenured
大对象直接进入老年代
Serial 和ParNew收集器
设置大小:-XX:PretenureSizeThreshold=3145728
指定大于该设置值的对象直接在老年代分配, 这样做的目的就是避免在Eden区及两个Survivor区之间来回复制, 产生大量的内存复制操作
这个参数不能与-Xmx之类的参数一样直接写 3MB
如果必须使用此参数进行调优, 可考虑ParNew加CMS的收集器组合。
主要原因
在分配空间时, 它容易导致内存明明还有不少空间时就提前触发垃圾收集, 以获取足够的连续空间才能安置好它们
而当复制对象时, 大对象就意味着高额的内存复制开销
长期存活的对象进入老年代
设置分代年龄:-XX:MaxTenuringThreshold
默认:15
CMS:6
对象动态年龄判断
设置动态年龄判断大小的Survivor容量比例:-XX:TargetSurvivorRatio ,默认是50%
一般是在minor gc之后触发的
从低年龄对象向高年龄对象累加
老年代空间分配担保机制
主要目的是为了防止在执行Minor GC时,由于老年代空间不足导致的程序出错。通过在垃圾回收前先进行空间的检查和Full GC,可以确保有足够的空间将新生代中的存活对象晋升到老年代。
可能会出现Premature Promotion
定位方式
GC 日志中出现“Desired survivor size 107347968 bytes, new threshold 1(max 6)”等信息,说明此时经历过一次 GC 就会放到 Old 区。
Full GC 比较频繁,且经历过一次 GC 之后 Old 区的变化比例非常大。
比如说 Old 区触发的回收阈值是 80%,经历过一次 GC 之后下降到了 10%,这就说明 Old 区的 70% 的对象存活时间其实很短
危害
Young GC 频繁,总的吞吐量下降。
Full GC 频繁,可能会有较大停顿
对象内存回收
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1
很难解决对象之间相互循环引用的问题
循环引用
可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为“引用链”(Reference Chain) , 如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时, 则证明此对象是不可能再被使用的。
GC Roots
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
在本地方法栈中JNI(即通常所说的Native方法) 引用的对象。
Java虚拟机内部的引用, 如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、 OutOfMemoryError)等,还有系统类加载器。
所有被同步锁(synchronized关键字)持有的对象。
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
常见引用类型
强引用(Strongly Re-ference)
在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
软引用(Soft Reference)
将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。
软引用可用来实现内存敏感的高速缓存
弱引用(Weak Reference)
将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
虚引用(Phantom Reference)
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
不推荐使用此方法
回收方法区
主要回收两部分内容
废弃的常量
与回收Java堆中的对象非常类似
没有任何字符串对象引用常量池中的“java”常量, 且虚拟机中也没有其他地方引用这个字面量。 如果在这时发生内存回收, 而且垃圾收集器判断确有必要的话, 这个“java”常量就将会被系统清理出常量池。 常量池中其他类(接口) 、 方法、 字段的符号引用也与此类似。
不再使用的类型
满足以下3点,才有可能(并不是一定)被回收:
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收, 这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、 JSP的重加载等, 否则通常是很难达成的。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
GC
分代收集理论
分代假说
1) 弱分代假说(Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭的。
2) 强分代假说(Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消亡。
对象会存在跨代引用
3) 跨代引用假说( Intergenerational Reference Hypothesis) : 跨代引用相对于同代引用来说仅占极少数。
记忆集与卡表
记忆集(存在于新生代)
这个结构把老年代划分成若干小块, 标识出老年代的哪一块内存会
存在跨代引用。 此后当发生Minor GC时, 只有包含了跨代引用的小块内存里的对象才会被加入到GC
Roots进行扫描。
存在跨代引用。 此后当发生Minor GC时, 只有包含了跨代引用的小块内存里的对象才会被加入到GC
Roots进行扫描。
实现方式
卡表
hotspot使用一种叫做“卡表”(Cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。
卡表是使用一个字节数组实现:CARD_TABLE[ ]
hotSpot使用的卡页是2^9大小,即512字节
每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”
一个卡页的内存中通常包含不止一个对象, 只要卡页内有一个(或更多) 对象的字段存在着跨代指针, 那就将对应卡表的数组元素的值标识为1, 称为这个元素变脏(Dirty) , 没有则标识为0
使用写屏障(类似于AOP的环形通知)维护卡表
在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier) , 在赋值后的则叫作写后屏障(Post-Write Barrier) 。 HotSpot虚拟机的许多收集器中都有使用到写屏障, 但直至G1收集器出现之前, 其他收集器都只用到了写后屏障。
在高并发环境下,可能出现缓存行伪共享问题
假设处理器的缓存行大小为64字节, 由于一个卡表元素占1个字节, 64个卡表元素将共享同一个缓存行。 这64个卡表元素对应的卡页总的内存为32KB(64×512字节) , 也就是说如果不同线程更新的对象正好处于这32KB的内存区域内, 就会导致更新卡表时正好写入同一个缓存行而影响性能。
解决方案
-XX:+UseCondCardMark, 用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题
垃圾收集算法
标记-清除算法
算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单
存在两个问题
效率问题 (如果需要标记的对象太多,效率不高)
空间问题(标记清除后会产生大量不连续的碎片)
后面的算法都是对其缺点进行改进
标记-复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
缺点:浪费空间
优化
Appel式回收
把新生代分为一块较大的Eden空间和两块较小的Survivor空间, 每次分配内存只使用Eden和其中一块Survivor
发生垃圾搜集时, 将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上, 然后直接清理掉Eden和已用过的那块Survivor空间
当Survivor空间不足以容纳一次Minor GC之后存活的对象时, 就需要依赖其他内存区域(实际上大多就是老年代) 进行分配担保(Handle Promotion)
标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
垃圾收集器
Serial收集器(-XX:+UseSerialGC)(单线程收集器)(标记复制)
Serial Old 使用标记整理
Parallel Scavenge 标记复制算法
达到可控制的吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
高吞吐量则可以最高效率地利用处理器资源, 尽快完成程序的运算任务, 主要适合在后台运算而不需要太多交互的分析任务
控制最大垃圾收集停顿时间
-XX:MaxGCPauseMillis=<N>
收集器将尽力保证内存回收花费的时间不超过用户设定值
设置过小,GC停顿时间缩短但是会牺牲吞吐量和新生代空间
直接设置吞吐量大小
-XX:GCTimeRatio=<N>
GC时间占总时间的比率
相当于吞吐量的倒数
默认值:99
即允许最大1%(即1/(1+99)) 的垃圾收集时间
PS Scavenge 设置自适应调节策略
-XX:+UseAdaptiveSizePolicy
这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
只需要设置
-Xmx设置最大堆
-XX: MaxGCPauseMillis 最大停顿时间 或 -XX: GCTimeRatio 吞吐量率
PS Scavenge 自适应调节策略,官网说明
Parallel Old 使用标记整理算法
ParNew 使用标记复制算法
CMS(Concurrent Mark Sweep)基于标记清除算法实现
收集步骤
1)初始标记(CMS initial mark)
Stop The World
仅仅只是标记一下GCRoots能直接关联到的对象
速度很快
2)并发标记(CMS concurrent mark)
从GC Roots的直接关联对象开始遍历整个对象图的过程
耗时较长但是不需要停顿用户线程
3)重新标记(CMS remark)
Stop The World
修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短
4)并发清除(CMS concurrent sweep)
清理删除掉标记阶段判断的已经死亡的对象
不需要移动存活对象, 所以与用户线程同时并发
优点
并发收集
低停顿
缺点
占用CPU资源
CMS默认启动的回收线程数是(处理器核心数量+3)/4
CPU core >=4
并发回收时垃圾收集线程只占用不超过25%的处理器运算资源
运算资源随着处理器核心数量的增加而下降
CPU core < 4
CPU需要分出一半的运算能力去执行收集器线程,对用户程序的影响就可能变得很大
无法处理“浮动垃圾”(Floating Garbage),可能出现“Concurrent Mode Failure”,导致临时启用Serial Old收集器
浮动垃圾
并发标记和并发清理阶段,程序产生新的垃圾对象是出现在标记过程结束以后,只能下一次回收。
由于GC时,需要预留足够的空间给程序使用
预留空间
JDK1.5
68%老年代空间触发 CMS GC
JDK1.6+
92%老年代空间触发 CMS GC
调整预留空间
-XX:CMSInitiatingOccupancyFraction=<N>
太高将会很容易导致大量的并发失败产生, 性能反而降低
太低导致GC频率变大
触发CMS 官网说明
预留空间无法满足程序分配新对象
又开始Full GC,但是上次Full GC还没有结束
出现一次“并发失败”(Concurrent Mode Failure)
启动后备预案
Stop The World
临时启用Serial Old
使用“标记-清除”算法
导致产生大量空间碎片
堆碎片化优化,官网说明
-XX:+UseCMSCompactAtFullCollection
默认开启
JDK9开始废弃
在CMS FullGC时,开启内存碎片的合并整理
-XX:CMSFullGCsBeforeCompaction=0
要求CMS收集器在执行过若干次(数量由参数值决定) 不整理空间的Full GC之后, 下一次进入Full GC前会先进行碎片整理
默认值为0, 表示每次进入Full GC时都进行碎片整理
G1
GCtuning 官网
0 条评论
下一页