Java虚拟机基本结构
类加载子系统
负责从文件系统或者网路中加载Class
信息,加载的内容放置于方法区
中,方法区
存放类信息外,还会存放运行时的常量池
信息,包括字符串字面量
数字常量
。Java堆
在虚拟机启动的时候建立,是程序主要的内存工作区域。几乎所有的Java对象实例
都存放于Java堆中,堆空间是所有线程共享的
直接内存
是使用Java NIO
向系统申请的内存区间,直接内存的速度会优于Java 堆
,读写频繁的场合可以考虑使用直接内存。 直接内存的大小不受Xmx
指定的最大堆大小,受限于系统的内存。垃圾回收系统
主要对方法区、Java堆和直接内存
进行回收。Java堆是垃圾收集器的重点。Java栈
保存栈帧信息
,包括局部变量、方法参数,与方法的调用和返回也相关。每个线程都有一个Java栈,在线程创建的时候被创建。本地方法栈
和Java栈非常类似,区别是Java栈用于Java方法的调用,而本地方法栈用于本地方法的调用
PC寄存器
是每个线程的私有空间,PC寄存器Java线程的当前方法执行的指令,如果当前方法是本地方法,则值为undefined
执行引擎
负责执行虚拟机的字节码。
Java堆结构 Xmn
根据垃圾回收机制的不同,Java堆可能拥有不同的结构。最常见的结构是将堆分为
新生代
和老年代
- 新生代存放新生对象或者年龄不大的对象,新生代可能分为
eden区
from区
和to区
,from
和to
是两块大小相等、可以互换角色的内存空间。 - 老年代存放老年对象
绝大多数情况下,对象首先分配在
eden区
,在一次新生代回收后,如果对象还存货,则进入from
或者to
,之后,每经过一次新生代的回收后,对象如果还存活则年龄增加1,当对象的年龄达到了一定条件后,就会被认为是老年对象从而进入老年代。
Java栈结构 Xss
线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。 Java栈是
先进后出
的数据结果,只支持出栈
和入栈
两种操作。
每一次函数调用都会有一个对应的栈帧被压入Java栈,
每一个函数调用结束,都会有一个栈帧被弹出Java栈
在一个栈帧中,至少要包含
局部变量表
操作数栈
和帧数据
局部变量表
用于保存函数的参数和局部变量,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着栈帧的销毁,局部变量表也会随之销毁。
局部变量表位于栈帧中,如果函数的局部变量较多,会使局部变量表膨胀,从而每一次调用会占用更多的栈空间,最终导致函数的嵌套次数减少
。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中的变量直接或者间接引用都不会被回收
操作数栈
主要用来保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
帧数据区
帧数据区保存着访问常量池的指针,方便程序访问常量池。此外,还有一个
异常处理表
用于恢复调用者函数的栈帧。
栈上分配是Java虚拟机提供的一项优化技术,他的基本思想史,对于那些线程私有的对象,可以将他们打散分配在栈上,从而提高系统的性能。栈上分配
的一个技术基础是进行逃逸分析
。逃逸分析
的目的是判断对象的作用域是否有可能逃逸出函数体。
-server
逃逸分析只能在server模式下启用-XX:+DoEascapeAnalysis
启用逃逸分析-XX:+EliminateAllocations
开启标量替换,允许将对象打散分配在栈上-UseTLAB
关闭TLAB
对于大量零散的小对象,在栈上分配速度快,并且可以有效避免垃圾回收带来的负面影响,但由于栈空间较小,因此对于大对象也不适合在栈上分配
方法区结构
方法区是一块所有线程共享的内存区域,他用于保存系统的类信息(字段,方法,常量池等),方法区的大小决定了系统可以保存多少个类。方法区可以理解为永久区,但在JDK8中永久区已被彻底移除,取而代之的是元数据去,元数据区的大小可以使用
-XX:MaxMetaspaceSize
指定,这是一块堆外的直接内存,默认情况下会耗尽直接内存
垃圾回收
对象可触及性
垃圾回收的思想是考察每一个对象的
可触及性
,即从根节点开始是否可以访问到这个对象,如果从所有的根节点都无法访问到某个对象,则说明对象不在使用了,一般情况下需要被回收,但无法触及的对象有可能在某一个条件下复活。 因此需要通过可触及性的状态来判断什么时候才可以安全的回收对象。
- 可触及的:从根节点开始,可以访问到这个对象
- 可复活的:对象的所有引用都被释放,在对象有可能在
finalize()
函数中复活- 不可触及的:对象的finalize()函数被调用,并且没有复活,那么就会进入不可触及的状态。
对象“复活”
public class A{
static A a;
protected void finalize(){
super.finalize();
a = this; // 当对象销毁时,有变量指向了当前对象,故而对象被复活,但finalize方法只能执行一次
}
}
引用和可触及性的强度
- 强引用:一般使用的引用类型,强引用的对象是可触及的,不会被回收
- 软引用:当堆空间不足时会被回收,不会引起内存溢出。可用Java类
java.lang.ref.SoftReference
类实现,可提供一个引用队列跟踪对象回收。- 弱引用:当GC时,不管系统当前堆空间如何,弱引用对象都会被回收,但由于垃圾回收器的线程优先级较低,并不能很快的发现持有弱引用的对象,故持有弱引用的对象可以持续一段时间。使用
java.lang.ref.WeakRefrence
类实现- 虚引用:作用在于跟踪垃圾回收,必须和引用队列一起使用,可以将依次资源释放操作放置在虚引用中执行和记录。
java.lang.ref.PhantomRefrence
常用垃圾回收算法
引用计数法 Java中未使用
对于任意对象A,如有任何一个对象引用了对象A,则A的引用计数器+1,当应用失效时,计数器-1,当计数器为0时,对象可被回收。引用计数法的缺点:
- 无法处理循环应用的情况
- 在每次引用产生或者消除的情况下,需要伴随一个加减法操作,影响性能
标记清除法(Mark-Sweep)
将回收分为标记节点和清除阶段,在标记阶段标记所有可达对象,在清除阶段,清除未被标记的对象,此方法的缺点是将会产生空间碎片
复制算法(Copying)
将原有的内存块分为两部分,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存货对象复制到未使用的内存块中,之后再清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。复制算法回收后内存空间是没有碎片的,但代价是将可用内存折半适用于存活对象少,垃圾对象多的场景下(新生代)
标记压缩算法(Mark-Compact)
先从根对象开始对可达对象进行一次标记,然后将所有存货对象压缩到内存的一端,最后清理边界外的内存空间。没有碎片,不需要2倍的内存空间可用于老年代回收。
分代算法(Generational-Collecting)
将内存空间根据对象的特点分成几块,根据每块内存区间的特定使用不同的回收算法, 提高垃圾回收效率,比如新生代使用复制算法进行垃圾回收,老年代使用标记压缩算法进行回收。 新生代回收的频率很高,老年代回收的频率较低,为支持新生代的回收,虚拟机可能使用一种叫做**卡表(Card Table)**的数据结构,卡表的每一比特位用来表示老年代是否持有了新生代的引用,如果不持有卡表为0,垃圾回收的时候扫描卡表,当为1时,再去扫描给定的老年代对象。
分区算法(Region)
将整个堆空间分成连续的小空间,每一次都是独立使用,独立回收,可以控制一次回收多少个区间
垃圾收集器
串行回收器
- 仅使用单线程进行垃圾回收
- 独占式的垃圾回收
新生代串行回收器
采用复制算法,在CPU硬件等不适优越的场合,性能表现可以超过并行收集器和并发收集器.一般输出:
0.967: [GC 0.967: [DefNew: 17472K->2176K(19648K), 0.0188339 secs] 17472K->2375K(63360K), 0.189186 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
老年代串行回收器
采用标记压缩算法,一般输出:
9.231: [Full GC 9.231: [Tenured: 43711K->40302K(43712K), 0.2960446 secs] 63350K->40302K(63360K), [Perm: 17836K->17836K(32768K)], 0.2961554 secs] [Times: user=0.28 sys=0.12, real=0.30 secs]
使用方式
-XX:+UseSerialGC
新生代老年代都是用串行收集器-XX:+UseParNewGC
新生代使用ParNew收集器,老年代是使用串行收集器-XX:+UseParallelGC
新生代使用ParallelGC收集器,老年代使用串行收集器
并行回收器
并行回收器采用多个线程同时进行垃圾回收
新生代ParNew回收器
此回收器针对新生代进行垃圾回收,只将串行收集器多线程化,其他的策略、算法以及参数和新生代串行回收器一样,也是独占式回收器。
- 使用方式
-XX:+UseParNewGC
新生代使用ParNew
收集器,老年代使用串行收集器,回收器工作时的线程数量可使用-XX:ParallelGCThreads
参数指定,一般最好与CPU数量相当,默认情况下,当CPU数量小于8个时,ParallelGCThreads
的值相当于CPU数量,当CPU数量大于8个时,ParallelGCThreads的值等于3+((5*CPU_COUNT)/8)
0.967: [GC 0.967: [ParNew: 17472K->2176K(19648K), 0.0188339 secs] 17472K->2375K(63360K), 0.189186 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
新生代ParallelGC回收器
复制算法、独占式、多线程收集器,与ParNew相比比较
注重吐吞量
- 使用方式:
-XX:+UseParallelGC
新生代使用ParallelGC
收集器,老年代使用串行收集器;
-XX:+UseParallelOldGC
,新生代使用ParallelGC
收集器,老年代使用ParallelOldGC
收集器- 控制系统吐吞量
-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间,大于0的整数,如果值设置较小,则增加了垃圾收集次数
-XX:GCTimeRatio
设置吞吐量大小,它的值是一个0~100之间的整数,假设值为n,那么系统将花费不超过1(1+n)的时间用于垃圾收集
-XX:+UseAdaptiveSizePolicy
开启自适应GC策略:新生代的大小、eden和survivior的比例、晋升老年代对象年龄等参数会被自动调整
0.967: [GC 0.967: [PSYoungGen: 17472K->2176K(19648K), 0.0188339 secs] 17472K->2375K(63360K), 0.189186 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
老年代ParallelOldGC回收器
多线程并发收集器,采用标记压缩算法,在JDK1.6中才能使用
9.231: [Full GC 9.231: [PSYongGen: 43711K->40302K(43712K), 0.2960446 secs] 63350K->40302K(63360K), [Perm: 17836K->17836K(32768K)], 0.2961554 secs] [Times: user=0.28 sys=0.12, real=0.30 secs]
CMS回收器
Concurrent Mark Sweep
采用标记清除算法,多线程并行回收
CMS主要工作步骤
- 初始标记:
STW
独占系统资源,标记根对象- 并发标记: 并发标记可达对象
- 预清理: 并发,尝试控制一次的停顿时间,预测下一次重新标记的时间
-XX:-CMSPrecleaningEnabled
关闭预清理- 重新标记:
STW
独占系统资源- 并发清理: 垃圾回收
- 并发重置: 重新初始化CMS数据结构和数据
CMS主要设置参数
- 启用CMS
-XX:+UseConcMarkSweepGC
- 工作线程
-XX:ConcGCTreads
或者-XX:ParallelCMSThreads
,默认(ParallelGCThreads+3)/4
- 回收阈值
-XX:CMSInitiatingOccupancyFraction
,默认68
当老年代的空间超过68%
时会执行一次CMS回收。- 空间压缩
-XX:UseCMSCompactAtFullCollection
进行内存整理,独占式- 空间压缩执行频率
-XX:CMSFullGCsBeforeCompaction
多少次CMS后进行内存整理-XX:+CMSScavengeBeforeRemark
在执行remark操作之前先做一次Young GC
G1回收器
- 并行性:G1回收期间,多个线程并行处理,充分利用多核CPU
- 并发性:G1拥有与应用程序交替执行的能力
- 分代GC:同时兼顾老年代和新生代
- 空间整理:每次回收都会进行适当的对象移动
- 可预见性:选取部分分区进行回收,减小了回收范围,缩短了全局应用停顿
G1的收集过程
一般有4个阶段:
- 新生代GC
- 并发标记周期
- 混合收集
- 可能进行Full GC
新生代GC
新生代GC的主要工作是回收eden区和survivor区。当eden区被占满时,会启动新生代GC
并发标记周期
并发标记周期一般分为以下几步:
- 初始标记 标记从根节点开始直接可达的对象,这个阶段会伴随一次新生代GC,
产生全局停顿
,eden区被清空,对象进入survivor区- 根区域扫描 扫描由survivor区直接可达的老年代区域,并标记可达对象;此时如果发生新生代GC,新生代GC必须等待根区域扫描结束,
新生代GC将会被延长
- 并发标记扫描并查找整个堆的存活对象并进行标记,
并发过程
可被新生代GC打断- 重新标记由于并发标记时应用程序在运行,所以对标记结果进行修正。
会引起应用程序停顿
- 独占清理计算各个区域的存活对象和GC回收比例并进行排序,识别可混合回收的区域并进行标记,更新记忆集(Remebered Set,记录当前区域被其他区域引用的对象),
会引起应用程序停顿
- 并发清理阶段根据独占清理计算出的区域,直接进行并发回收不包含存活对象的区域
混合回收
此阶段回收垃圾占比比较高的区域,即会执行新生代GC又会选取一些被标记的老年代区域进行回收
必要时的Full GC
由于并发收集让应用程序和GC线程交替工作,因此不能完全避免内存不足的情况,如果在混合GC的时候发生空间不足或者在新生代GC时,survivor区和老年代无法容纳幸存的对象时,就会进行一次FullGC
G1常用参数
- 开启
-XX:+UseG1GC
- 最大停顿时间
-XX:MaxGCPauseMills
,如果在一次停顿超过这个值时,G1会尝试调整新生代和老年代的比例、调整堆大小、调整晋升年龄手段等,达到预设目标,值过小可能增加了Full GC的可能性- 工作线程数量
-XX:ParallelGCThreads
并行回收时,线程数量- 堆使用率
-XX:InitiatingHeapOccupancyPercent
默认值45,当堆使用率到45%时执行并发标记周期,如果值过大可能会导致Full GC的几率变大,如果值过小,可能会引起频繁GC
参考资料:《实战Java虚拟机–JVM故障诊断与性能调优》