JVM
第一章 JVM介绍
Java Virtual Machine - java程序的运行环境(java二进制码的运行环境)
了解jvm有助于理解底层原理
特点:
- 一次编写,跨平台运行
- 自动内存管理,垃圾回收
- 数组下标越界检查
- 多态
第二章 JVM内存结构
2.1. 程序计数器
作用:记录下一条JVM指令的执行地址
javap -v StringTest.class > StringTest.txt 可以得到class文件的执行文件
指令执行原理:Jvm指令 -> 解释器 -> 机器码 -> CPU
特点:
线程私有的
不会存在内存溢出
2.2. Java虚拟机栈
每个Java虚拟机线程都有一个私有Java虚拟机栈,与该线程同时创建
每个线程运行时所需要的内存称为虚拟机栈
每个栈有多个栈帧组成,对应每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
-Xss1m 设置栈内存分配大小
栈内存不是越大越好,越大会导致线程数越少
栈内存溢出(StackOverflowError):
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
诊断:
CPU占用过多:
- 定位: top 命令定位哪个进程cpu占用过高
- ps H -eo pid,tid,%cpu|grep 进程id 查看哪个线程引起cpu过高
- Jstack 进程id 查看并找到问题线程(用计算器将线程id转换成十六进制)
2.3. 本地方法栈
Java虚拟机的实现可以使用传统的栈(俗称“ C栈”)来支持native方法(以Java编程语言以外的语言编写的方法)
2.4. 堆(Heap)
通过new创建对象会使用堆内存
特点:
线程共享,堆中的对象需要考虑线程安全问题
有垃圾回收机制
-Xms: 初始内存分配大小,默认为物理内存1/64
-Xmx1m 调整堆内存大小
-XX:+printGCDetails 输出详细的GC处理日志
堆内存溢出(OutOfMemoryError)
堆内存诊断:
Jps 查看当前进程中有哪些java进程
Jmap 查看堆内存占用情况 jmap -heap 进程id
Dump 垃圾回收信息:
Jmap -dump:format=b,live,file=1.bin 进程id(dump时会主动执行一次垃圾回收,用 Eclipse Memory Analyze工具打开分析)
Jconsole 图形界面的多功能检测工具,可以连续检测
Jvisualvm 图形界面
Dump 内存进行分析:
工具:jprofiles
-XX:+HeapDumpOnOutOfMemoryError 会在项目根目录下生产java_pid6820.hprof文件(需要特殊工具才能打开)
2.5. 方法区
Jdk8以前:
永久代(占用堆内存)
-XX:MaxPermSize=1m 最大永久代内存大小
永久代内存溢出:java.lang.OutOfMemoryError: PermGen space
Jdk8后:
元空间(不再由jvm管理内存)
-XX:MaxMetaspaceSize=1m 最大元空间大小
元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
Javap -v class文件 - 得到二进制字节码(javap -v StringTest.class > StringTest.txt)
二进制字节码:类基本信息,常量池,类方法定义,包含了虚拟机指令
常量池:常量池就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型等信息
运行时常量池:常量池时class文件中的,当类被加载,类的常量池信息就会放入运行时常量池,并把里面符号地址转换为真是地址。
StringTable特性:
- 常量池中的符号仅是符号,第一次用到时才变为对象
- 利用串池的机制来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder,见类:StringTest.java
- 字符串常量拼接的原理是编译期优化
- 可以使用intern的方法,主动将串池中还没有的字符串放入串池
例:s = new String(“ab”)
s1 = s.intern(); 将堆中string尝试放入串池,如果有则不放入(s!="ab"),没有则放入(放入后s=="ab"),返回串池对象s1=="ab"
-XX:StringTableSize=20000 设置StringTable桶个数
当字符串多时可以用intern()将字符串入池以减少堆内存的使用
直接内存:
常见于NIO操作时,用于数据缓冲区
分配回收成本高,当读写性能强
不受JVM内存回收管理
直接内存分配和回收原理:
- 使用了Unsafe对象完成直接内存的分配回收,回收主要调用freeMemory方法
ByteBuffer实现类内部使用了Cleaner检测ByteBuffer对象,ByteBuffer对象被垃圾回收时,就会由ReferenceHandler线程通过Cleaner的clear方法调用freeMemory来释放直接内存。
第三章 垃圾回收
3.1. 判断回收
3.1.1. 引用计数法
每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象可以被回收。
缺点:两个对象相互调用且无其他对象引用时,这两个对象无法被回收。
当失去引用后,实例1、实例2无法回收
3.1.2. 可达性算法
从GC roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC roots无法到达的对象便成了垃圾回收对象,可被GC回收。
可以作为GC roots对象:
- 虚拟机栈的栈帧的局部变量表所引用对象
- 本地方法栈的JNI所引用对象
- 方法区的静态变量和常量所引用对象
可达性:
Reference1 -> 实例1
Reference2 -> 实例2
Reference3 -> 实例4 -> 实例6
不可回收对象:实例1,实例2,实例4,实例6
可回收对象:实例3,实例5
3.2. 几种引用
3.2.1. 强引用
只有所有GC roots对象都不通过强引用引用该对象,该对象才能被垃圾回收。
3.2.2. 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象。
可以配合引用队列来释放软引用自身。
SoftReferenceTest
3.2.3. 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身。
WeakReferenceTest
由以上例子可以得出:
当执行minor GC时不会回收所有弱引用对象
当执行full GC时会回收所有弱引用对象
3.2.4. 虚引用(PhantomRefernce)
必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入引用队列,由ReferenceHandler线程调用虚引用相关方法释放直接内存。
ByteBuffer buffer = ByteBuffer.allocateDirect(size);//利用直接内存作为缓冲区
3.2.5. 终结器引用(FinalReference)
无需手动编程,但其内部配合引用队列使用,在垃圾回收时,终结器引用入引用队列(被引用对象暂时没有被回收),再有Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象。
3.3. 回收算法
3.3.1. 标记清除
特点:速度较快,会造成内存碎片。
3.3.2. 标记整理(标记压缩)
特点:速度慢,没有内存碎片。
3.3.3. 复制
特点:不会有内存碎片,需要占用双倍空间。
3.3.4. 分代收集算法
- 对象首先分配到Eden区。
- 新生代空间不足时,触发Minor GC,Eden和Survivor From区存活对象复制到Survivor To中,存活的年龄加1,并且交换Survivor From区和Survivor To区。
- Minor GC会引发STW(Stop the world),暂停其他的用户线程,等垃圾回收结束,用户线程才恢复运行。
- 当Survivor区对象寿命超过阀值时,会晋升到老年代,最大寿命为15(4bit)。
- 当老年代空间不足时,会先触发Minor GC,如果空间仍然不足,再触发Full GC,Full GC STW会更长。
3.4. 垃圾回收器
垃圾收集器是垃圾回收算法(标记清除、复制、标记整理、分代收集算法)的具体实现。
HotSpot虚拟机所包含的收集器及组合:
相关概念:
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%。吞吐量优先既让单位时间内,STW的时间最短。
3.4.1. Serial收集器
Serial收集器是最基本的、发展历史最悠久的收集器。
特点:
针对新生代
单线程
采用复制算法
简单高效(与其他收集器的单线程相比)
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率
收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:
适用于Client模式下的虚拟机。
Serial / Serial Old收集器运行示意图:
参数:
-XX:+UseSerialGC=Serial + SerialOld :显式的使用串行垃圾收集器
3.4.2. ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。
特点:
多线程,除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
应用场景:
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
ParNew/Serial Old组合收集器运行示意图如下:
参数:
-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器;
-XX:+UseParNewGC:强制指定使用ParNew;
-XX:ParallelGCThreads=n:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
3.4.3. Parallel Scavenge收集器
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,故也称为吞吐量优先收集器。
特点:新生代收集器采用复制算法,并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。
应用场景:
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间。
当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互。
Parallel Scavenge/Parallel Old收集器运行示意图如下:
参数:
-XX:MaxGCPauseMillis=ms:控制最大垃圾收集停顿时间,如果设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,因为可能导致垃圾收集发生得更频繁。
-XX:GCTimeRatio=ratio:设置垃圾收集时间占总时间的比率,0<n<100的整数,默认值是99(1/(1+ratio)即1/(1+99)比率为1%),相当于设置吞吐量大小。
-XX:+UseAdaptiveSizePolicy:当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold=n)等参数,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
3.4.4. Serial Old 收集器
Serial Old是Serial收集器的老年代版本。
特点:是单线程收集器,采用标记整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途:
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
Serial / Serial Old收集器运行示意图:
参数:
-XX:+UseSerialGC=Serial + SerialOld :显式的使用串行垃圾收集器
3.4.5. Parallel Old 收集器
Parallel Old 收集器是Parallel Scavenge收集器的老年代版本。
特点:针对老年代,多线程,采用标记整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
Parallel Scavenge/Parallel Old收集器运行示意图如下:
参数:
-XX:+UseParallelOldGC:指定使用Parallel Old收集器。
3.4.6. CMS收集器
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器,一种以获取最短回收停顿时间为目标的收集器(响应时间优先)。
CMS减少了执行老年代垃圾收集时应用暂停的时间,但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间。
特点:
针对老年代,基于标记清除算法实现,以获取最短回收停顿时间为目标,并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器运行示意图如下:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,标记出存活对象且用户线程可并发执行,并不能保证可以标记出所有的存活对象。
重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短。采用多线程并行执行来提升效率。
并发清除:并发的对标记的对象进行清除回收。
参数:
-XX:+UseConcMarkSweepGC:指定使用CMS收集器。
-XX:ConGCThreads=thread: 设置垃圾回收线程并发数量。
缺点:
- 对cpu资源敏感
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
2. 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
浮动垃圾:在并发清除时,用户线程新产生的垃圾,称为浮动垃圾。
因为浮动垃圾使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集,-XX:CMSInitiatingOccupancyFraction设置占用多少容量时触动垃圾回收(0~100)。
预留内存空间无法满足程序需要,会出现"Concurrent Mode Failure"失败,临时启用Serail Old收集器,而导致另一次Full GC的产生。
3.产生大量内存碎片
由于CMS基于"标记清除"算法,清除后不进行压缩操作,从而产生大量内存碎片。
产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
-XX:+UseCMSCompactAtFullCollection:使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程。
-XX:+CMSFullGCsBeforeCompaction: 设置执行多少次不压缩的Full GC后,来一次压缩整理。
3.4.7. G1收集器
一款面向服务端应用的垃圾收集器。JDK 7u4官方支持,JDK 9默认。
特点:
并行与并发:
能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW停顿时间,可以并发让垃圾收集线程与用户线程同时进行。
分代收集:
能够独自管理整个java堆,将整个Java堆划分为多个大小相等的独立区域(Region),并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
空间整合:
结合多种垃圾收集算法,从整体看,是基于标记整理算法,但两个Region间,基于复制算法,不会产生空间碎片,有利于长时间运行。
可预测的停顿:
除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
应用场景:
面向服务端应用,针对具有大内存、多处理器的机器,同时注重吞吐量和低延迟,默认暂停目标是200ms。
参数:
-XX:+UseG1GC:指定使用G1收集器
-XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
-XX:MaxGCPauseMillis:为G1设置暂停时间目标,默认值为200毫秒
-XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region
G1为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。
G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
这样就保证了在有限的时间内可以获取尽可能高的收集效率。
G1与其他收集器的区别:
其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
- 每个Region都有一个对应的Remembered Set
- 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作
- 检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象)
- 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中
- 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏
G1收集器运行示意图:
G1收集器大致可分为如下步骤:
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿STW,但耗时很短。)
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。上一阶段对象的变化记录在线程的Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿STW,但可并行执行。)
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存(可并发执行)
第四章 VM参数汇总
-XX:+HeapDumpOnOutOfMemoryError dump内存记录,会在项目根目录下生产java_pid6820.hprof文件(需要特殊工具才能打开如:jprofiles)