了解Java虚拟机(JVM)首先需要了解一下一段Java代码的具体执行过程。
其中加粗的字体就是JVM的组件。
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
**Eden 区(伊甸园区):**Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。
**SurvivorFrom (幸存0区):**上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
**SurvivorTo(幸存1区):**保留了一次 MinorGC 过程中的幸存者
老年代:主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以**MajorGC(重GC)**不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,**方法区还有一个别名Non-Heap(非堆);**方法区是一种定义,不同的Java虚拟机的实现方式不同。
jps [options] [hostid]
-l : 输出主类全名或jar路径
-q : 只输出LVMID
-m : 输出JVM启动时传递给main()的参数
-v : 输出JVM启动时显示指定的JVM参数
jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
命令格式:jstat [option] LVMID [interval] [count]
参数
[option] : 操作参数
-gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因
-gcnew 统计新生代的行为
-gcnewcapacity 新生代与其相应的内存空间的统计
-gcold 统计旧生代的行为
-gcoldcapacity 统计旧生代的大小和空间
-gcpermcapacity 永生代行为统计
-printcompilation hotspot编译方法统计
LVMID : 本地虚拟机进程ID
[interval] : 连续输出的时间间隔
[count] : 连续输出的次数
jmap [option] LVMID
dump : 生成堆转储快照
finalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
heap : 显示Java堆详细信息
histo : 显示堆中对象的统计信息
permstat : 打印Java堆内存的永久保存区域的类加载器的智能统计信息
F : 当-dump没有响应时,强制生成dump快照
jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
jhat [dumpfile]
-stack false|true 关闭对象分配调用栈跟踪(tracking object allocation call stack)。
-refs false|true 关闭对象引用跟踪(tracking of references to objects)。
-port port-number 设置 jhat HTTP server 的端口号. 默认值 7000
-exclude exclude-file 指定对象查询时需要排除的数据成员列表文件。
-baseline exclude-file 指定一个基准堆转储(baseline heap dump)。
-debug int 设置 debug 级别. 0 表示不输出调试信息。
-version 启动后只显示版本信息就退出>
-J< flag > 因为 jhat 命令实际上会启动一个JVM来执行, 通过 -J 可以在启动JVM时传入一些启动参数.
jstack [option] LVMID
-F : 当正常输出请求不被响应时,强制输出线程堆栈
-l : 除堆栈外,显示关于锁的附加信息
-m : 如果调用到本地方法的话,可以显示C/C++的堆栈
jinfo(JVM Configuration info)这个命令作用是实时查看和调整虚拟机运行参数。 之前的jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令
jinfo [option] [args] LVMID
-flag : 输出指定args参数的值
-flags : 不需要args参数,输出所有JVM参数的值
-sysprops : 输出系统属性,等同于System.getProperties()
VisualVM(All-in-One Java Troubleshooting Tool)是目前为止JDK发布的功能最强大的运行监视和故障处理程序。
使用idea分析dump工具
-Xss | 设置每个线程的堆栈大小。 |
-Xmx | 设置堆的最大空间大小。默认是内存的1/4 |
-Xms | 设置堆的初始空间大小,默认是内存的1/64 |
-XX:NewSize | 设置新生代最小空间大小。 |
-XX:MaxNewSize | 设置新生代最大空间大小。 |
-Xmn | 设置新生代堆大小,最大=最小 |
-XX:PermSize | 设置永久代最小空间大小。 |
-XX:MaxPermSize | 设置永久代最大空间大小。 |
-Xx:SurvivorRatio=ratio | 幸存区比例(伊甸区:survivor from区) |
-XX:MaxTenuringThreshold=threshold | 新生代到老年代的晋升阈值 |
-XX:NewRatio=ratio | 老年代和新生代的比值(老年代:新生代) |
-XX:+PrintTenuringDistribution | 晋升详情 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
-XX:+PrintGCDetails -verbose:gc | Gc详情 |
-XX:+ScavengeBeforeFullGC | FullGC前MinorGC |
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。
- 老年代空间大小=堆空间大小-年轻代大空间大小
java代码查看jvm堆的默认值大小:
Runtime.getRuntime().maxMemory() // 堆的最大值,默认是内存的1/4
Runtime.getRuntime().totalMemory() // 堆的当前总大小,默认是内存的1/64
如果是命令行运行:
java -Xmx50m -Xms10m HeapDemo
public class Demo2 {
public static void main(String[] args) {
System.out.print("最大堆大小:");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================================================");
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
}
public class HeapDemo {
public static void main(String args[]) {
System.out.println("=====================Begin=========================");
System.out.print("最大堆大小:Xmx=");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================First Allocated===================");
byte[] b1 = new byte[5 * 1024 * 1024];
System.out.println("5MB array allocated");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("=================Second Allocated===================");
byte[] b2 = new byte[10 * 1024 * 1024];
System.out.println("10MB array allocated");
System.out.print("剩余堆大小:free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("=====================OOM=========================");
System.out.println("OOM!!!");
System.gc();
byte[] b3 = new byte[40 * 1024 * 1024];
}
}
jvm参数设置成最大堆内存100M,当前堆内存10M:-Xmx100m -Xms10m -XX:+PrintGCDetails,再次运行,可以看到minor GC和full GC日志:
把上面案例中的jvm参数改成最大堆内存设置成50M,当前堆内存设置成10M,执行测试: -Xmx50m -Xms10m
=====================Begin=========================
剩余堆大小:free mem=8.186859130859375M
当前堆大小:total mem=9.5M
=================First Allocated=====================
5MB array allocated
剩余堆大小:free mem=3.1868438720703125M
当前堆大小:total mem=9.5M
================Second Allocated====================
10MB array allocated
剩余堆大小:free mem=3.68682861328125M
当前堆大小:total mem=20.0M
=====================OOM=========================
OOM!!!
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.atguigu.demo.HeapDemo.main(HeapDemo.java:40)
判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
优点:
缺点:
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
在Java语言中,可以作为GC Roots的对象包括下面几种:
即用三种颜色记录对象的标记状态
黑色 – 已标记
灰色 – 标记中
白色 – 还未标记
具体过程:
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
平时只会用到强引用和软引用。
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。
在介绍JVM垃圾回收算法前,先介绍一个概念:Stop-the-World
Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。
优点:
缺点:
缺点:
标记-整理法是标记-清除法的一个改进版。分为标记和整理两个阶段
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。
分代回收算法实际上是把复制算法和标记整理法的结合,老年代就是很少垃圾需要进行回收的,新生代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。
年轻代(Young Gen):年轻代特点是区域相对老年代较小,对像存活率低。
年轻代空间不足时,使用Minor GC(复制算法)回收整理,速度最快。
复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen):老年代的特点是区域较大,对像存活率高。
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-整理;垃圾收集的过程中会Stop The World(服务暂停)。它还有对应老年代的版本:Serial Old
ParNew收集器收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The world、对象分配规则、回收策略等都与Serial收集器完全一样,实现上这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如下图所示。
ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-整理
参数控制:
-XX:+UseParNewGC
ParNew收集器
-XX:ParallelGCThreads
限制线程数量
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;
特点
垃圾回收时采用的算法
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
参数控制:
-XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC
使用Parallel收集器+ 老年代并行
-XX:GCTimeRatio=ratio
GC时间比例,1/(1+ratio),目标是提高吞吐率
-XX:MaxGCPauseMillis=ms
最大GC暂停毫秒数,默认200ms
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,**其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。**包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。可以用作老年代收集器(新生代使用ParNew)
优点: 并发收集、低停顿
缺点: 产生大量空间碎片、并发阶段会降低吞吐量
参数控制:
-XX:+UseConcMarkSweepGC
使用CMS收集器
-XX:+UseCMSCompactAtFullCollection
Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量(一般情况约等于可用CPU数量),默认为4
-XX:CMSInitiatingOccupancyFraction=percent
执行CMS回收的内存占比
-XX:+CMSScavengeBeforeRemark
做重新标记前,对新生代进行一次回收,+打开,-关闭
cms是一种预处理垃圾回收器,它不能等到old内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集
- G1跟踪各个Region里垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
1、初始标记(Initial Making)
2、并发标记(Concurrent Marking)
3、最终标记(Final Marking)
4、筛选回收(Live Data Counting and Evacuation)
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
整堆收集器: G1
回收器名称 | 算法分类 | 作用区域 | 是否多线程 | 类型 | 备注 |
---|---|---|---|---|---|
Serial | 复制算法 | 新生代 | 单线程 | 串行 | 简单高效、不建议使用 |
ParNew | 复制算法 | 新生代 | 多线程 | 并行 | 唯一和CMS搭配的新生代垃圾回收器 |
Parallel Scavenge | 复制算法 | 新生代 | 多线程 | 并行 | 更关注吞吐量 |
Serial Old | 标记整理 | 老年代 | 单线程 | 串行 | 能和所有young gc搭配使用 |
Parallel Old | 标记整理 | 老年代 | 多线程 | 并行 | 搭配Parallel Scavenge |
CMS | 标记清除 | 老年代 | 多线程 | 并发 | 追求最短的暂停时间 |
垃圾回收器选择策略
客户端程序 : Serial + Serial Old;
吞吐率优先的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;
响应时间优先的服务端程序 :ParNew + CMS。