0. java代码的执行过程
了解Java虚拟机(JVM)首先需要了解一下一段Java代码的具体执行过程。
- Java代码的具体执行过程如下:
- 执行 javac 命令编译源代码为字节码
- 执行 java 命令,二进制字节码通过解释器翻译为机器码
- 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
- 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
- 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
- 需要创建对象,会使用堆内存来存储对象
- 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
- 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
- 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
- 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
- 对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
- 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能
- 将翻译的机器码交给cpu去运行
其中加粗的字体就是JVM的组件。
1. JVM组件
1.1.程序计数器
- 作用:记住下一条jvm指令的执行地址
- 特点:
- 线程私有
- 不会存在内存溢出
1.2. Java Virtual Machine Stacks (Java虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
- 常见问题栈溢出:Exception in thread “main” java.lang.StackOverflowError
1.2.1 栈帧
- 栈中的数据都是以**栈帧(Stack Frame)**的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。栈帧是每个方法运行时需要的内存
- 每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈。
- 栈帧中主要保存3 类数据:
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
- 栈操作(Operand Stack):记录出栈、入栈的操作。
- 栈帧数据(Frame Data):包括类文件、方法等等。
1.2.2本地方法栈(Native Method Stacks):
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
- 凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层c语言的库!
private native void starte(); //thread.start()的底层实现- 进入本地方法栈,然后调用JNI本地方法接口
- JNI作用:扩展Java的使用,融合不同的编程语言为Java所用。
- 进入本地方法栈,然后调用JNI本地方法接口
1.3. 堆(Heap)
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
1.3.1 新生代
- 新生区是对象的诞生、成长、消亡的区域,一个对象在这里产生,应用,最后被垃圾回收器收集,结束生命。一般占据堆的 1/3 空间。
- 新生代又分为 Eden 区、SurvivorFrom 、SurvivorTo三个区。
-
**Eden 区(伊甸园区):**Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。
-
**SurvivorFrom (幸存0区):**上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
-
**SurvivorTo(幸存1区):**保留了一次 MinorGC 过程中的幸存者
-
- 由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。
- 即当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收**(Minor GC(轻GC))**,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 From区。
1.3.2 老年代
老年代:主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以**MajorGC(重GC)**不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
- MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常
1.4.方法区(元空间)
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,**方法区还有一个别名Non-Heap(非堆);**方法区是一种定义,不同的Java虚拟机的实现方式不同。
- HotSpot VM把 GC 分代收集扩展至方法区,即使用 Java 堆的永久代来实现方法区,这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久代的内存回收的主要目标是针对常量池的回收和类型的卸载,因此收益一般很小)。
- 永久代是 Hotspot 虚拟机特有的概念,指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
- 永久代是jdk1.8之前,Hotspot对方法区的具体实现。
- 在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
- 元空间是jdk1.8以后,Hotspot对方法区的具体实现。
- 元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
- 类的元数据放入 native memory,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
2. JVM调优
2.1. 常用工具
2.1.1. 命令工具
2.1.1.1. JPS:JVM Process Status Tool,虚拟机进程状况工具
- 显示指定系统内所有的HotSpot虚拟机进程。
- 命令格式:
jps [options] [hostid]
- option参数
-
-l : 输出主类全名或jar路径
-
-q : 只输出LVMID
-
-m : 输出JVM启动时传递给main()的参数
-
-v : 输出JVM启动时显示指定的JVM参数
-
2.1.1.2. jstat 虚拟机统计信息监视工具
jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
-
命令格式:
jstat [option] LVMID [interval] [count]
-
参数
-
[option] : 操作参数
- -class 监视类装载、卸载数量、总空间以及耗费的时间
- -class 监视类装载、卸载数量、总空间以及耗费的时间
-
-compiler 输出JIT编译过的方法数量耗时等
-
-gc 垃圾回收堆的行为统计,常用命令
-
-gccapacity 同-gc,不过还会输出Java堆各区域使用到的最大、最小空间
-
-gcutil 同-gc,不过输出的是已使用空间占总空间的百分比
-
-gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因
-
-gcnew 统计新生代的行为
-
-gcnewcapacity 新生代与其相应的内存空间的统计
-
-gcold 统计旧生代的行为
-
-gcoldcapacity 统计旧生代的大小和空间
-
-gcpermcapacity 永生代行为统计
-
-printcompilation hotspot编译方法统计
-
-
LVMID : 本地虚拟机进程ID
-
[interval] : 连续输出的时间间隔
-
[count] : 连续输出的次数
2.1.1.3. jmap Java内存映像工具
- jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还可以使用**-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候自动生成dump文件**。 jmap不仅能生成dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
- 命令格式:
jmap [option] LVMID
- option参数
-
dump : 生成堆转储快照
-
finalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
-
heap : 显示Java堆详细信息
-
histo : 显示堆中对象的统计信息
-
permstat : 打印Java堆内存的永久保存区域的类加载器的智能统计信息
-
F : 当-dump没有响应时,强制生成dump快照
-
- 查看某个java进程所有参数:jinfo 进程号
- 查看某个java进程总结性垃圾回收统计:jstat -gc 20292
2.1.1.4. jhat 虚拟机堆转储快照分析工具
jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
- 命令格式:
jhat [dumpfile]
- 参数
-
-stack false|true 关闭对象分配调用栈跟踪(tracking object allocation call stack)。
- 如果分配位置信息在堆转储中不可用. 则必须将此标志设置为 false. 默认值为 true.>
-
-refs false|true 关闭对象引用跟踪(tracking of references to objects)。
- 默认值为 true. 默认情况下, 返回的指针是指向其他特定对象的对象,如反向链接或输入引用(referrers or incoming references), 会统计/计算堆中的所有对象。
-
-port port-number 设置 jhat HTTP server 的端口号. 默认值 7000
-
-exclude exclude-file 指定对象查询时需要排除的数据成员列表文件。
- 例如, 如果文件列列出了 java.lang.String.value , 那么当从某个特定对象 Object o 计算可达的对象列表时, 引用路径涉及 java.lang.String.value 的都会被排除。>
-
-baseline exclude-file 指定一个基准堆转储(baseline heap dump)。
- 在两个 heap dumps 中有相同 object ID 的对象会被标记为不是新的(marked as not being new). 其他对象被标记为新的(new). 在比较两个不同的堆转储时很有用.
-
-debug int 设置 debug 级别. 0 表示不输出调试信息。
- 值越大则表示输出更详细的 debug 信息.
-
-version 启动后只显示版本信息就退出>
-
-J< flag > 因为 jhat 命令实际上会启动一个JVM来执行, 通过 -J 可以在启动JVM时传入一些启动参数.
- 例如, -J-Xmx512m 则指定运行 jhat 的Java虚拟机使用的最大堆内存为 512 MB. 如果需要使用多个JVM启动参数,则传入多个 -Jxxxxxx.
-
2.1.1.5. jstack Java堆栈跟踪工具
- jstack用于生成java虚拟机当前时刻的线程快照。
- 线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
- 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
- 命令格式:
jstack [option] LVMID
- option参数
-
-F : 当正常输出请求不被响应时,强制输出线程堆栈
-
-l : 除堆栈外,显示关于锁的附加信息
-
-m : 如果调用到本地方法的话,可以显示C/C++的堆栈
-
2.1.1.6. jinfo Java配置信息工具
jinfo(JVM Configuration info)这个命令作用是实时查看和调整虚拟机运行参数。 之前的jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令
- 命令格式:
jinfo [option] [args] LVMID
- option参数
-
-flag : 输出指定args参数的值
-
-flags : 不需要args参数,输出所有JVM参数的值
-
-sysprops : 输出系统属性,等同于System.getProperties()
-
2.1.2. 可视化工具
2.1.2.1. jconsole Java监视与管理控制台
-
JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。
-
通过JDK/bin目录下的jconsole.exe启动
2.1.2.2. jvisual VM工具:多合一故障处理工具
VisualVM(All-in-One Java Troubleshooting Tool)是目前为止JDK发布的功能最强大的运行监视和故障处理程序。
-
通过JDK/bin目录下的jvisualvm.exe启动
-
使用idea分析dump工具
-
配置jvm参数
-
把上例中运行参数改成:
-Xmx50m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tmp
-XX:HeapDumpPath:生成dump文件路径。
-
再次执行:生成C:\tmp\java_pid20328.hprof文件
-
使用jvisualvm工具打开hprof文件
-
文件–>装入–>选择要打开的文件即可
-
2.2. JVM常用参数
- 标准参数(-),所有JVM都必须支持这些参数的功能,而且向后兼容;
- 非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容;
- 非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用;
-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 |
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。
- 老年代空间大小=堆空间大小-年轻代大空间大小
2.3. jvm参数设置
java代码查看jvm堆的默认值大小:
Runtime.getRuntime().maxMemory() // 堆的最大值,默认是内存的1/4
Runtime.getRuntime().totalMemory() // 堆的当前总大小,默认是内存的1/64
如果是命令行运行:
java -Xmx50m -Xms10m HeapDemo
2.4 测试/调优案例
- 在终端中输入指令
- 查看虚拟机运行参数
- "D:\Program Files\Java\jdk1.8.0_144\bin\java”-XX:+PrintFlagsFinal -version | findstr “Gc”
- 查看虚拟机运行参数
2.4.1. 线程诊断
- 线程CPU高占用查询
- 用top命令定位那个进程对cpu的占用过高
- ps H -e0 pid,tid,%cpu | grep 进程id,进一步定位那个线程对cpu占用过高
- jstack 进程id =⇒列出当前进程的所有线程,对应线程id(10进制,,jstack列出的线程的id是十六进制)
- 排查线程死锁问题
- jstack 进程id,查看是否存在死锁,若存在查看导致死锁的具体线程和具体问题。
2.4.2. 查看堆内存详情
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)
3.垃圾回收(GC)
- GC回收的是堆内存,不包含虚拟机栈,在方法调用结束后会自动释放方法的占用的内存
- GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
- GC的特点:
- 次数上频繁收集Young区
- 次数上较少收集Old区
- 基本不动Perm区
3.1. 垃圾判定
判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
3.1.1. 引用计数法(Reference-Counting)
- 引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:
- 简单,高效,现在的objective-c、python等用的就是这种算法。
缺点:
- 引用和去引用伴随着加减算法,影响性能
- 很难处理循环引用,相互引用的两个对象则无法释放。
- 因此目前主流的Java虚拟机都摒弃掉了这种算法。
3.1.2. 可达性分析算法(根搜索算法)
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
在Java语言中,可以作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中JNI(Native方法)的引用对象
真正标记以为对象为可回收状态至少要标记两次。- 第一次标记:不在 GC Roots 链中,标记为可回收对象。
- 第二次标记:判断当前对象是否实现了finalize() 方法,如果没有实现则直接判定这个对象可以回收,如果实现了就会先放入一个队列中。并由虚拟机建立一个低优先级的程序去执行它,随后就会进行第二次小规模标记,在这次被标记的对象就会真正被回收了!
3.1.3. 三色标记和并发漏标
3.1.3.1. 三色标记
即用三种颜色记录对象的标记状态
-
黑色 – 已标记
-
灰色 – 标记中
-
白色 – 还未标记
-
具体过程:
-
起始的三个对象还未处理完成,用灰色表示
-
该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
-
依次类推
-
沿着引用链都标记了一遍
-
最后为标记的白色对象,即为垃圾
-
3.1.3.2. 并发漏标问题
- 比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:
-
如图所示标记工作尚未完成
-
用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
-
但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
-
如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
-
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
- Incremental Update 增量更新法,CMS 垃圾回收器采用
- 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
- Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
- 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
- 新加对象会被记录
- 被删除引用关系的对象也被记录
3.1.4. 四种引用
平时只会用到强引用和软引用。
强引用:
- 类似于 Object obj = new Object();
- 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
**软引用:**SoftReference 类实现软引用。用来描述一些还有用但并非必需的对象。
- 在系统要发生内存溢出异常之前,才会将这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
- 软引用可用来实现内存敏感的高速缓存。
- 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果这个软引用所引用的对象被垃圾回收器回收,Java虚拟机就会将这个软引用加入到关联的引用队列中。
- 使用场景: 适用于网页缓存、图片缓存,防止内存溢出,在内存充足的时候,缓存对象会一直存在,在内存不足的时候,缓存对象占用的内存会被垃圾收集器回收。
**弱引用:**WeakReference 类实现弱引用。
- 对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果这个弱引用所引用的对象被垃圾回收器回收,Java虚拟机就会将这个弱引用加入到关联的引用队列中。
- 使用场景: 弱引用用于生命周期更短的,对内存更敏感的场景中,比如占用内存很大的Map,java api中就提供了WeakHashMap使用,就会使得大Map被及时清理掉。
**虚引用:**PhantomReference 类实现虚引用。
- 无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
- 作用:虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。你声明虚引用的时候是要传入一个queue的。当你的虚引用所引用的对象已经执行完finalize函数的时候,就会把对象加到queue里面。你可以通过判断queue里面是不是有对象来判断你的对象是不是要被回收了
- 使用场景: 判断一个对象是否被垃圾回收了,跟踪对象被垃圾回收回收的活动。一般可以通过虚引用达到回收一些非java内的一些资源比如堆外内存的行为。
- 例如:在 DirectByteBuffer 中,会创建一个 PhantomReference 的子类Cleaner的虚引用实例用来引用该 DirectByteBuffer 实例,Cleaner 创建时会添加一个 Runnable 实例,当被引用的 DirectByteBuffer 对象不可达被垃圾回收时,将会执行 Cleaner 实例内部的 Runnable 实例的 run 方法,用来回收堆外资源。
3.1.5. 内存泄漏
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。
导致内存泄漏的情况
- 资源未关闭造成的内存泄漏
- 各种连接,如数据库连接、网络连接和IO连接等,文件读写等,可以使用 try-with-resources 读取完文件,自动资源释放
- 全局缓存持有的对象不使用的时候没有及时移除,导致一直在内存中无法移除
- 静态集合类
- 如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。生命周期长的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
- 堆外内存无法回收
- 堆外内存不受gc的管理,可能因为第三方的bug出现内存泄漏
内存泄漏的解决方法
- 尽量减少使用静态变量,或者使用完及时 赋值为 null。
- 明确内存对象的有效作用域,尽量缩小对象的作用域,能用局部变量处理的不用成员变量,因为局部变量弹栈会自动回收;
- 减少长生命周期的对象持有短生命周期的引用;
- 使用StringBuilder和StringBuffer进行字符串连接,Sting和StringBuilder以及StringBuffer等都可以代表字符串,其中String字符串代表的是不可变的字符串,后两者表示可变的字符串。如果使用多个String对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
- 对于不需要使用的对象手动设置null值,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;
- 各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭。
内存泄漏的排查方法
- 查看内存中对象的数量和大小,判断是否在合理的范围,如果在合理的范围内,增大内存配置,调整内存比例就可以了。
- 命令:jmap -heap pid
- 分析gc是否正常执行
- 命令:jstat -gcutil 1000
- 确认下版本新增代码的改动,尽快从代码上找出问题。
- 开启各种命令行和 导出 dump 各种工具分析
3.2. 垃圾回收算法
在介绍JVM垃圾回收算法前,先介绍一个概念:Stop-the-World
Stop-the-world意味着 JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。
- minor gc 会触发Stop-the-World
- Minor GC 发生在新生代的垃圾回收,暂停时间短
- Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
- Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
3.2.1. 复制算法(Copying)
- 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
优点:
- 实现简单
- 不产生内存碎片
缺点:
- 代价太高,将内存缩小为原来的一半,浪费了一半的内存空间;如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
- 对象存活率高的时候,**复制对象和对象的引用地址时间花费高。**如果对象的存活率很高,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。复制算法要想使用,最起码对象的存活率要非常低才行。
3.2.2. 标记清除(Mark-Sweep)
- “标记-清除”(Mark Sweep)算法是几种GC算法中最基础的算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
- 算法分为2个阶段:
- 标记出需要回收的对象,使用的标记算法均为可达性分析算法。
- 回收被标记的对象。
缺点:
- 效率问题(两次遍历)
- 空间问题(标记清除后会产生大量不连续的碎片。JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。)
3.2.3. 标记整理(Mark-Compact)
标记-整理法是标记-清除法的一个改进版。分为标记和整理两个阶段
- 在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;
- 在整理阶段,该算法并没有直接对死亡的对象进行清理,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。
- 优点:标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价
- 缺点:如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
3.2.4. 分代收集算法(Generational-Collection)
-
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
-
内存整齐度:复制算法=标记整理算法>标记清除算法。
-
内存利用率:标记整理算法=标记清除算法>复制算法。
分代回收算法实际上是把复制算法和标记整理法的结合,老年代就是很少垃圾需要进行回收的,新生代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。
-
年轻代(Young Gen):年轻代特点是区域相对老年代较小,对像存活率低。
-
年轻代空间不足时,使用Minor GC(复制算法)回收整理,速度最快。
复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
MinorGC 的过程(复制 --> 清空 --> 互换)
- eden、From 复制到 To,年龄+1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到Survivor From区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置),则赋值到老年代区),同时把这些对象的年龄+1 - 清空 eden、Survivor From
然后,清空Eden和From中的对象 - To和 From 互换
最后,To和From互换,原To成为下一次GC时的From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代 - 大对象特殊情况
如果分配的新对象比较大Eden区放不下,但Old区可以放下时,对象会被直接分配到Old区(即没有晋升这一过程,直接到老年代了)
-
-
老年代(Tenure Gen):老年代的特点是区域较大,对像存活率高。
- 老年代空间不足,会先尝试触发Minor GC,如果空间仍不足,则触发Full GC,一般是由标记清除或者是标记清除与标记整理的混合实现。
3.3. 垃圾收集器(了解)
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
3.3**.1. Serial/Serial Old收集器**
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-整理;垃圾收集的过程中会Stop The World(服务暂停)。它还有对应老年代的版本:Serial Old
- 特点
- 单线程
- 堆内存较小,适合个人电脑
- 参数控制:
-XX:+UseSerialGC
串行收集器
3.3.2. ParNew 收集器
ParNew收集器收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The world、对象分配规则、回收策略等都与Serial收集器完全一样,实现上这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如下图所示。
ParNew收集器 ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-整理
-
参数控制:
-XX:+UseParNewGC
ParNew收集器-XX:ParallelGCThreads
限制线程数量
3.3.3. Parallel Scavenge/Parallel Old 收集器
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;
-
特点
- 多线程
- 堆内存较大,适合多核cpu
- 目标是让单位时间内,STW时间最短
-
垃圾回收时采用的算法
- eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
- old 内存不足发生** Full 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
3.3.4. CMS收集器(Concurrent Mark Sweep)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
- 特点
- 多线程
- 堆内存较大,适合多核cpu
- 尽可能让单次STW的时间最短
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,**其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。**包括:
-
初始标记(CMS initial mark)
- 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
-
并发标记(CMS concurrent mark)
- 并发标记阶段就是进行GC Roots Tracing的过程
- 并发标记时不需暂停用户线程
-
重新标记(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内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败
3.3.5. G1收集器(Garbage First)
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。
- 与CMS收集器相比G1收集器有以下特点:
- **并行与并发:**G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- **分代收集:**分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
- 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
- **可预测的停顿:**这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而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)
- 初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。
2、并发标记(Concurrent Marking)
- 并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。
3、最终标记(Final Marking)
- 而最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。
4、筛选回收(Live Data Counting and Evacuation)
- 筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。
下图为G1收集器运行示意图:
3.3.6. 垃圾回收器比较
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
整堆收集器: G1
回收器名称 | 算法分类 | 作用区域 | 是否多线程 | 类型 | 备注 |
---|---|---|---|---|---|
Serial | 复制算法 | 新生代 | 单线程 | 串行 | 简单高效、不建议使用 |
ParNew | 复制算法 | 新生代 | 多线程 | 并行 | 唯一和CMS搭配的新生代垃圾回收器 |
Parallel Scavenge | 复制算法 | 新生代 | 多线程 | 并行 | 更关注吞吐量 |
Serial Old | 标记整理 | 老年代 | 单线程 | 串行 | 能和所有young gc搭配使用 |
Parallel Old | 标记整理 | 老年代 | 多线程 | 并行 | 搭配Parallel Scavenge |
CMS | 标记清除 | 老年代 | 多线程 | 并发 | 追求最短的暂停时间 |
-
垃圾回收器选择策略
-
客户端程序 : Serial + Serial Old;
-
吞吐率优先的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;
-
响应时间优先的服务端程序 :ParNew + CMS。
-