本节主要介绍的内容有:Java 虚拟机的内存划分、垃圾回收机制、虚拟机内存分析工具。
Java以后发展的几个方向:
下图是 Java 虚拟机的内存主要区域划分,注意图中由浅蓝色标识的部分是所有线程共享的数据区;淡紫色标识的部分是每个线程私有的数据区域
。
我们这里对各个部分功能做简要的总结:
线程私有
,用来指示当前线程所执行的字节码的行号,就是用来标记线程现在执行的代码的位置;对 Java 方法,它存储的是字节码指令的地址;对于 Native 方法,该计数器的值为空。线程私有
,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。一个方法的执行和退出就是用一个栈帧的入栈和出栈表示的。通常我们不允许你使用递归就是因为,方法就是一个栈,太多的方法只执行而没有退出就会导致栈溢出,不过可以通过尾递归优化。栈又分为虚拟机栈和本地方法栈,一个对应 Java 方法,一个对应 Native 方法。几乎所有的对象实例(包括数组)都在上面分配
。它是垃圾收集器的主要管理区域,因此也叫 GC 堆。它实际上是一块内存区域,由于一些收集算法的原因,又将其细化分为新生代和老年代等。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。运行时常量池
是方法区的一部分,它用于存放编译器生成的各种字面量和符号引用,比如字符串常量等。根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。根据上面 JVM 内存区域的描述,因为程序计数器和两种栈的生命周期与线程相同,线程或者方法结束即可回收。所以,所谓的垃圾回收主要是针对方法区和堆内存而言。
可达性分析 四种引用类型 对象的自我救赎
判断一个对象是否可以回收通用的方式有两种。
根据可达性分析的原理,对象之间存在引用关系,但是 “是或否被引用” 不足以描述更多的场景,
所以在这基础之上人们又提出了四种引用类型的概念:强引用、软引用、弱引用和虚引用
,它们的引用强度依次减弱。
new
关键字创建一个对象的时候,这个对象就是强引用的,它绝对不会被回收,即使内存耗尽。你可以通过将其置为 null
来弱化对其的引用,但什么时候被回收还要取决于 GC 算法。SoftReference
和 WeakReference
来使用它们,它们的区别在于后者更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象;而软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。软引用可以用来做缓存,因为当 JVM 内存不足的时候才会被回收;而弱引用适合 Android 上面引用 Activity 等的时候使用,因为 Activity 被销毁不一定是因为内存不足,可能是正常的生命周期结束。如果此时使用软引用,而 JVM 内存仍然足够,则仍然会持有 Activity 的引用而造成内存泄漏。当一个对象不再被引用的时候,该对象也不一定被回收,理论上它还有一次救赎的机会,即通过覆写 finilize()
方法把对自己的引用从弱变强,即把自己赋值给全局的对象等。因为当对象不可达的时候,只有当 finilize()
没被覆写,或者 finilize()
已经被调用过,则该对象会被回收。否则,它会被放在一个队列中,并在稍后由一个低优先级的 Finilizer 线程执行它。所以,我们可以通过这个机制来完成救赎,但实际上没有必要那么干,因为覆写 finilize()
是不推荐的。
标记-清除算法 复制算法 标记-整理算法 分代收集算法
第一回收算法是标记-清除算法,这种算法直接在内存中把需要回收的对象“抠”出来。
好好的内存被它搞成了马蜂窝,所以效率不高,清除之后会产生内容碎片,造成内存不连续,当分配较大内存对象时可能会因内存不足而触发垃圾收集动作。
复制算法
将内存分成两块,一次只在一块内存中进行分配,垃圾回收一次之后,
就将该内存中的未被回收的对象移动到另一块内存中,然后将该内存一次清理掉。
比如将内存分成A和B,先在A中分配,当垃圾回收的时候把A中需要回收的内存清理掉,然后把不需要清理的所有对象复制到B里面。
复制算法
常被用来回收新生代,而且分配空间也不是1:1,而是较大的Eden空间和较小的Survivor空间。在HotSpot中,其比例是8:1。
类似于标记-清除
算法,只是回收了之后,它要对内存空间进行整理,以使得剩余的对象占用连续的存储空间。
上面是三种基本的垃圾回收算法,但实际上,我们通常根据对象存活周期的不同将内存划分成几块,然后根据其特点采用不同的回收算法。
这就是所谓的分代收集算法
。
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1
HotSpot当中共有7种垃圾回收器,按照它们负责区域,又可以分成三种:
当搭配起来使用的时候又可以得到以下几种组合关系:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1
下面我们按照这些收集器的相近关系来介绍以下它们:
Serial 收集器和 Serial Old 收集器:分别对应于新生代和老年代,都是单线程的,当进行垃圾回收的时候必须暂停其他工作线程,直到它收集结束,因此它们的性能会低一些。但它们也有自己的优势:简单高效。因没有线程交互,所以在单 CPU 环境中会有优势。当需要收集的垃圾比较少的时候,停顿时间会较小,因而是可以接受的。Serial 收集器使用的是复制算法和 Serial Old 收集器使用的是标记-整理算法。
ParNew 收集器、Parallel Scavenge 收集器和 Paralle Old 收集器:ParNew 是 Serial 收集器的多线程版本,使用多线程收集垃圾;Parallel Scavenge 相比于 ParNew 达到了吞吐量可控的目的,所谓吞吐量就是指运行用户代码的时间与 CPU 总耗时的比值,可以理解成执行你的代码的比例;而 Paralle Old 收集器可以看作 Parallel Scavenge 的老年版本。ParNew 收集器和 Parallel Scavenge 收集器使用复制算法,而 Paralle Old 收集器使用标记-整理算法。
CMS收集器:以获取最短回收停顿时间为目标,与其他收集器不同的是,它把垃圾收集过程分成了 4 个阶段:初始标记、并发标记、重新标记和并发清除。初始标记和重新标记过程需要 Stop the world,但是它们耗时比较短,而耗时比较长的并发标记和并发清除则与用户线程同时进行。因而,它可以大大减少垃圾回收过程中的停顿时间。但它有几个缺点:
G1收集器:G1 收集器可以管理整个堆,它汲取了 CMS 收集器的优点而回避了它的缺点。它放弃了标记-清理算法,而使用标记-整理算法,同时建立了可预测的停顿时间模型。它的垃圾回收也分成四个过程:初始标记、并发标记、最终标记和筛选回收,而且它的标记过程也与用户线程同时进行,只是在回收阶段,它会根据用户期望的 GC 停顿时间指定回收计划,只回收一部分区域,从而提高收集的效率。
对象内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配,少数情况下可能直接分配在老年代。
-XX:PretenureSizeThreshold
参数,当对象的大小大于它的值的时候将直接分配在老年代。这样做是为了避免在 Eden 区和 Survivor 区之间发生大量的内存复制。-XX:MaxTenuringThreshold
设置。GC 日志控制参数 日志含义
JVM 的 GC 日志的主要参数包括如下几个:
-XX:+PrintGC
输出 GC 日志-XX:+PrintGCDetails
输出 GC 的详细日志-XX:+PrintGCTimeStamps
输出 GC 的时间戳(以基准时间的形式)-XX:+PrintGCDateStamps
输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)-XX:+PrintHeapAtGC
在进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log
日志文件的输出路径比如:
-XX:+PrintGCDetails -Xloggc:../logs/gc.log -XX:+PrintGCTimeStamps
GC日志:
0.256: [GC (System.gc()) [PSYoungGen: 2236K->824K(18432K)] 2236K->832K(60928K), 0.0023996 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.259: [Full GC (System.gc()) [PSYoungGen: 824K->0K(18432K)] [ParOldGen: 8K->742K(42496K)] 832K->742K(60928K), [Metaspace: 3069K->3069K(1056768K)], 0.0134299 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
上面的 G C日志中各个信息的意义如下:
[GC
和 [Full GC
表示停顿的类型,Full
表示此次 GC 是发生了 Stop-The-World
;System.gc()
触发GC的时候就会出现 System.gc()
;[PSYoungGen
表示GC发生的区域,由收集器决定的。比如,PSYoungGen
表示的是 Parallel Scavenge
收集器的新生代,[DefNew
表示的是Serial收集器的新生代,ParNew
表示的是 Paralle 收集器的新生代,对老年代和永久代同理;824K->0K(18432K)
表示的是 GC前该内存区域已使用量->GC后该内存区域已使用量(该内存区域总容量)
。2236K->832K(60928K)
表示的是 GC前该Java堆已使用量->GC后该Java堆已使用量(该Java堆总容量)
。0.0023996 secs
表示该GC占用的时间。[Times: user=0.02 sys=0.00, real=0.01 secs]
中的时间分别表示:用户消耗的 CPU 时间、内核消耗的 CPU 时间和操作从开始到结束所经过的墙钟时间。放置在 jdk 的 bin
目录下面的可执行文件为我们提供了许多便利的工具。
jps (JVM Process Status Tool),用来列出正在运行的虚拟机进程,并显示虚拟机执行主类名称及这些进程的本地虚拟机唯一 ID。
命令格式:
jps [options] [hostid]
jps常用的选项
-p 只输出LVMID,省略主类的名称
-m 输出虚拟机进程启动时传递给主类main()函数的参数
-l 输出主类的全名,如果进程执行的是jar包,输出jar路径
-v 输出虚拟机进程启动时jvm参数
jstat (JVM Statistics Monitoring Tool),用于监视虚拟机各种运行状态信息,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾回收、JIT编译等运行数,是运行期定位虚拟机性能问题的首选工具。
命令格式:
jstat [option vmid [interval [s|ms] [count]] ]
比如
jstat -gc 2764 250 20
表示:每250毫米查询一次进程2764的垃圾收集状况,一共查询20次。如果省略后面两个参数,则说明只查询一次。vmid可以通过jps
获取到。
jinfo (Configuration info for java),用来实时查看和调整虚拟机各项参数。
命令格式:
jinfo [option] pid
jmap (Memory Map for java),用于生成堆转储快照。jmap 的作用并不仅仅为了获取 dump 文件,它还可以查询 finalize 执行队列、java 堆和永久代的详细信息。如空间使用率、当前用的是哪种收集器等。
命令格式:
jmap [option] vmid
用来分析 dump 生成的堆快照,不过功能可以完全被其他工具取代,而且该工具在可视化方面也不好,所以可以略过。
jstack 命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致长时间等待等。
命令格式:
jstack [option] vmid
参数:
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用到本地方法的话,可以显示c/c++的堆栈
在 jdk 的 bin 目录下面找到该软件之后打开,非常方便地使用。可以使用它连接到本地或者远程的 Java 进程,并可以对内存、线程、类VM等进行实时监控。
它是位于 jdk 的 bin 目录下面的一个名为 jvisualvm 的可执行文件,打开它之后在左侧双击我们需要监控的进行即可对进行进行监控。
我们可以在 “工具-插件” 中选择需要安装的插件,而它的页面中的功能选项卡也是基于所安装的插件的。从 “工具-插件” 中直接通过检查URL来获取插件已经行不通了,可以链接 中下载指定 jdk 版本的插件,然后在 “工具-插件-已下载” 中进行安装。
Java 虚拟机系列文章,
本系列以及其他系列的文章均维护在 Github 上面:Github / Awesome-Java,欢迎 Star & Fork. 如果你喜欢这篇文章,愿意支持作者的工作,请为这篇文章点个赞!