1. Java内存结构,JVM堆的基本结构。
一、java运行时的数据结构
1)类加载子系统:负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
在JDK1.6、JDK1.7中,方法区可以理解为永久区(Perm持久代)。永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:MaxPermSize为64M。一个大的永久区可以保存更多的类信息。如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类,如果这样,就需要设置一个合理的永久区大小,确保不发生永久区内存溢出。
在JDK1.8中,永久区已经被彻底移除,取而代之的是元数据区,元数据区大小可以使用参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),这是一块堆外的直接内存。与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。
2)java堆:在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。
3)直接内存:java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
4)垃圾回收系统:是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。
5)java栈:每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着帧信息,java栈中保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。
6)本地方法栈:和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)
7)PC(Program Counter)寄存器:是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined
8)执行引擎:是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
二、堆的基本结构
对象优先在eden区分配,大对象直接进入老年代(原因是避免在eden区和f两个survivor区域频繁复制),长期存活的对象直接进入老年代(此处有对象年龄的判定:对象在eden区分配,经历过minor GC后仍然存活,且能够放入survivor区域中,那么年龄加一)
在Java 6中,方法区中包含的数据,除了JIT编译生成的代码存放在native memory的CodeCache区域,其他都存放在永久代;
在Java 7中,Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内);
在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
如果“堆”是说GC heap,那么这个错误。PermGen是HotSpot的GC heap的一部分。
方法区是逻辑区,具体存在哪里要看不同虚拟机的具体实现,永久代是Hotspot虚拟机特有的概念,是方法区的一种实现。
2. JVM的垃圾收集算法有哪几种?
- 标记清除:标记出需要回收的对象,然后清除。有内存碎片问题;
- 标记整理:标记出需要回收的对象,然后将存活的对象向一端移动;
- 复制算法:将内存分为一块较大的eden区和两块较小的survivor区,每次只使用eden区和一块survivor区,回收时,将这两块区域的数据复制到另一个survivor区中,最后清除eden区和一块survivor区;
- 分代收集:新生代采取复制算法,老年代采取标记清除或标记整理。
3. 什么情况下会出现OOM(Java程序是否会内存溢出?)
3.1. OOM && SOF定义
- OutOfMemoryError异常: 对象的数量超出最大堆容量限制(需判断是内存溢出还是内存泄漏);除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能;
内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素 - StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。 因为栈一般默认为1-2M,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1M而导致溢出。
3.2. 发生了内存泄露或溢出怎么办?
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
(1)通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现OOM异常的时候Dump出内存映像以便于分析。
(2)一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。(到底是出现了内存泄漏还是内存溢出)
哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系,还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。
(3)如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 找到引用信息,可以准确的定位出内存泄漏的代码位置。(HashMap中的元素的某些属性改变了,影响了hashcode的值会发生内存泄漏)
(4)如果不存在内存泄漏,就应当检查虚拟机的参数(-Xmx与-Xms)的设置是否适当,是否可以调大;修改代码逻辑,把某些对象生命周期过长,持有状态时间过长等情况的代码修改。
3.3. 内存泄漏的场景
(1)使用静态的集合类
静态的集合类的生命周期和应用程序的生命周期一样长,所以在程序结束前容器中的对象不能被释放,会造成内存泄露。
解决办法是最好不使用静态的集合类,如果使用的话,在不需要容器时要将其赋值为null。
修改hashset中对象的参数值,且参数是计算哈希值的字段
(2)单例模式可能会造成内存泄露(长生命周期的对象持有短生命周期对象的引用)
单例模式只允许应用程序存在一个实例对象,并且这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收。
解决办法是单例对象中持有的其他对象使用弱引用,弱引用对象在GC线程工作时,其占用的内存会被回收掉。
(3)数据库、网络、输入输出流,这些资源没有显示的关闭
垃圾回收只负责内存回收,如果对象正在使用资源的话,Java虚拟机不能判断这些对象是不是正在进行操作,比如输入输出,也就不能回收这些对象占用的内存,所以在资源使用完后要调用close()方法关闭。
3.4. 内存溢出的场景
3.4.1 Java Heap 溢出
在jvm规范中,堆中的内存是用来生成对象实例和数组的。
如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。
当生成新对象时,内存的申请过程如下:
- jvm先尝试在eden区分配新建对象所需的内存;
- 如果内存大小足够,申请结束,否则下一步;
- jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
- Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
- 当OLD区空间不够时,JVM会在OLD区进行full GC;
- full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”: outOfMemoryError:java heap space
3.4.2 虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 不断创建线程,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
- 这里需要注意当栈的大小越大可分配的线程数就越少;使用-Xss设置栈大小
- 栈溢出的原因:
递归调用
大量循环或死循环
全局变量是否过多
数组、List、map数据过大
3.4.3 方法区和运行时的常量池溢出:产生了大量的类信息,导致方法区溢出(考虑卸载部分类)
jdk1.6 常量池在永久代中(堆分为新生代老年代永久代等,永久带和堆逻辑上是区分开的,实际上还是在堆上),jdk1.7及以上,常量池移动到元空间(本机内存上)(1.7移出部分,1.8全部移出)
- 异常信息:java.lang.OutOfMemoryError:PermGen space
- 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。 - 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。
- 该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
- 由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
3.4.4 直接内存溢出:可能是使用了NIO
dump文件无明显异常,且文件大小很小,需检查是否使用了nio
3.4.5 java.lang.OutOfMemoryError: GC overhead limit exceeded
原因:执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。
目的是为了让应用终止,给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。
解决方法:
1. 大对象在使用之后指向null。
2. 增加参数,-XX:-UseGCOverheadLimit,关闭这个特性;
3. 增加heap大小,-Xmx1024m
3.5. 如何避免发生内存泄露和溢出
1、尽早释放无用对象的引用
2、使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
3、尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
4、避免在循环中创建对象
5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
4. JVM有哪些常用启动参数可以调整?分别什么作用?(JVM配置和调优参数都有哪些?)
项目中经常使用的参数,已用红色字体标出;
关于参数名称:
- 标准参数(-),所有JVM都必须支持这些参数的功能,而且向后兼容;例如:
-client——设置JVM使用Client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位环境下直接运行Java程序默认启用该模式。
-server——设置JVM使Server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。
- 非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容;
- 非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用;
一、堆设置
- -Xms1800m:设置JVM初始堆内存。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
- -Xmx1800m:设置JVM最大堆内存。
- -Xmn680m:设置年轻代大小。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。
- -Xss256k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
- -XX:MetaspaceSize=340m 元空间初始值
- -XX:MaxMetaspaceSize=340m 元空间最大值
二、收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置为并行收集器(吞吐量优先)。此配置仅对年轻代有效。即年轻代使用并行收集,而年老代仍使用串行收集。
- -XX:+UseParalledlOldGC:设置并行老年代收集器
- -XX:+UseConcMarkSweepGC:CMS收集,设置年老代为并发收集。CMS收集是JDK1.4后期版本开始引入的新GC算法。它的主要适合场景是对响应时间的重要性需求大于对吞吐量的需求,能够承受垃圾回收线程和应用线程共享CPU资源,并且应用中存在比较多的长生命周期对象。CMS收集的目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存。
- -XX:+UseParNewGC:设置年轻代为并发收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此参数。
三、垃圾回收统计信息
- -XX:+PrintGC:每次GC时打印相关信息。
- -XX:+PrintGCDetails:每次GC时打印详细信息。
- -XX:+PrintGCTimeStamps:打印每次GC的时间戳。
- -XX:ErrorFile=./hs_err_pid.log:保存错误日志或数据到指定文件中。
- -XX:HeapDumpPath=/home/admin/logs指定Dump堆内存时的路径。
- -XX:HeapDumpOnOutOfMemoryError:当首次遭遇内存溢出时Dump出此时的堆内存。
四、并行收集器设置(吞吐量优先)
- -XX:ParallelGCThreads=n:配置并行收集器的线程数,即:同时有多少个线程一起进行垃圾回收。此值建议配置与CPU数目相等。
- -XX:MaxGCPauseMillis=n:设置每次年轻代垃圾回收的最长时间(单位毫秒)。如果无法满足此时间,JVM会自动调整年轻代大小,以满足此时间。
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
- -XX:UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动调整年轻代Eden区大小和Survivor区大小的比例,以达成目标系统规定的最低响应时间或者收集频率等指标。此参数建议在使用并行收集器时,一直打开。
五、并发收集器设置(响应时间优先)
- -XX:CMSFullGCsBeforeCompactinotallow=0:由于并发收集器不对内存空间进行压缩和整理,所以运行一段时间并行收集以后会产生内存碎片,内存使用效率降低。此参数设置运行0次Full GC后对内存空间进行压缩和整理,即每次Full GC后立刻开始压缩和整理内存。
- -XX:+UseCMSCompactAtFullCollection:打开内存空间的压缩和整理,在Full GC后执行。可能会影响性能,但可以消除内存碎片。
- -XX:+CMSIncrementalMode:设置为增量收集模式。一般适用于单CPU情况。
- -XX:CMSInitiatingOccupancyFractinotallow=70:表示年老代内存空间使用到70%时就开始执行CMS收集,以确保年老代有足够的空间接纳来自年轻代的对象,避免Full GC的发生。
六、其它垃圾回收参数
- -XX:+ScavengeBeforeFullGC:年轻代GC优于Full GC执行。
- -XX:-DisableExplicitGC:不响应 System.gc() 代码。
- -XX:+UseThreadPriorities:启用本地线程优先级API。即使 java.lang.Thread.setPriority() 生效,不启用则无效。
- -XX:SoftRefLRUPolicyMSPerMB=0:软引用对象在最后一次被访问后能存活0毫秒(JVM默认为1000毫秒)。
- -XX:TargetSurvivorRatio=90:允许90%的Survivor区被占用(JVM默认为50%)。提高对于Survivor区的使用率。
5. 如何查看JVM的内存使用情况?
- jps:查看当前jvm中运行的所有虚拟机进程。通常用来定位进程。
- jstat:显示本地或者远程虚拟机进程的类装载,内存,垃圾回收,JIT编译等统计信息。与visualVm可视化工具有些类似,当jvm所在的环境不存在可视化工具时,该命令是一中常用的监控方式。
- jinfo:实时查看和调整虚拟机的各项配置信息。
- jmap:生成堆转储快照。
- jhat:与jmap结合使用,用来分析jmap生成的dump文件。该工具比较少用,因为分析dump文件一般比较耗时,会将dump复制到另一台机器上进行。同时有许多功能更强大,更专业和图形化的分析工具可以使用,例如jconsole,visualVm,Eclipse Memory Analyzer,IBM HeapAnalyzer。
- jstack:用于生成虚拟机当前时刻的线程快照(称为threaddump或者javacore)。目的是定位线程长时间停顿的原因,例如死锁,死循环,等待外部资源.
6.常用的垃圾收集器
serial收集器:一个线程完成垃圾收集工作,并暂停其他用户线程
parNew收集器:开启多个线程完成垃圾回收工作,并暂停其他用户线程
parallel scavenge收集器:并行多线程的收集器,与parNew的不同是,该收集器是高吞吐量优先的收集器
serial old收集器:serial的老年代版本,使用标记-整理算法
parallel old:parallel scavenge的老年代版本,使用标记-整理算法
CMS收集器:并发低停顿收集器。以最低停顿时间位目标的垃圾收集器,使用标记-清除算法
7. Java内存模型
java虚拟机规范中试图定义一种java内存模型来屏蔽物理和操作系统的内存差异,以让java程序在不同的平台上都有一致的内存访问效果。
java内存模型主要目标是定义各个变量的访问规则,即虚拟机将变量存储到内存和从内存中读取变量这样的底层细节。这里的变量是指实例字段,静态字段和构成数组对象的元素,不包含局部变量和方法参数。
图片源于:周志明-深入理解java虚拟机
8. CMS算法的过程,CMS回收过程中JVM是否需要暂停?(CMS原理?)
1)算法过程
初始标记:标记一下gc roots能关联到的对象
并发标记:gc root tracing的过程
重新标记:修正“并发标记”期间因用户线程导致对象产生变动的标记记录,这个阶段比初始标记稍微长一些,但远比并发标记短。
并发清除
2)初始标记和重新标记需要暂停其他用户线程
3)缺点
- 对cpu资源敏感:因为在并发阶段,虽然不会导致用户线程暂停,但是占用了cpu资源,导致程序变慢,总吞吐量下降。
- 无法回收浮动垃圾:可能导致concurrent mode failure,而导致full gc。在并发清理阶段,用户线程同步进行,还会有新的垃圾产生,这部分垃圾在标记之后产生的,无法清理。
- 内存碎片:由于采用了标记-清除算法,会有空间碎片。为了解决这个问题,使用-XX:+UseCMSCompactAtFullColletion开关参数,用于在CMS收集器顶不住要进行fgc时开启内存碎片的合并整理过程,但此过程不能并发,用户线程停顿时间会变长。另一个解决方法是使用-XX:CMSFullGCsBeforeCompaction,用户设置执行n次不压缩的fgc后,跟着来一个带压缩的gc.
图片源于:周志明-深入理解java虚拟机
10. G1的原理?
G1收集器是一款在server端运行的垃圾收集器,专门针对于拥有多核处理器和大内存的机器,在JDK 7u4版本发行时被正式推出,在JDK9中更被指定为官方GC收集器。它满足高吞吐量的同时满足GC停顿的时间尽可能短。G1收集器专门针对以下应用场景设计
- 可以像CMS收集器一样可以和应用并发运行
- 压缩空闲的内存碎片,却不需要冗长的GC停顿
- 对GC停顿可以做更好的预测
- 不想牺牲大量的吞吐量性能
- 不需要更大的Java Heap
G1采用了一种全新的内存布局
在G1中堆被分成一块块大小相等的heap region,一般有2000多块,这些region在逻辑上是连续的。每块region都会被打唯一的分代标志(eden,survivor,old)。在逻辑上,eden regions构成Eden空间,survivor regions构成Survivor空间,old regions构成了old 空间。
通过命令行参数-XX:NewRatio=n
来配置新生代与老年代的比例,默认为2,即比例为2:1;-XX:SurvivorRatio=n
则可以配置Eden与Survivor的比例,默认为8。
GC时G1的运行方式与CMS方式类似,会有一个全局并发标记(concurrent global marking phase)的过程,去确定堆里对象的的存活情况。并发标记完成之后,G1知道哪些regions空闲空间多(可回收对象多),优先回收这些空的regions,释放出大量的空闲空间。这是为什么这种垃圾回收方式叫G1的原因(Garbage-First)。
G1将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域,使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数量。
需要注意的是,G1不是实时收集器。它能够以较高的概率满足设定的暂停时间目标,但不是绝对确定的。根据以前收集的数据,G1估算出在用户指定的目标时间内可以收集多少个区域。因此,收集器对于收集区域的成本有一个相当准确的模型,它使用这个模型来确定在暂停时间目标内收集哪些区域和收集多少区域。
G1中的GC收集
G1保留了YGC并加上了一种全新的MIXGC用于收集老年代.
当Eden空间被占满之后,就会触发YGC。在G1中YGC依然采用复制存活对象到survivor空间的方式,当对象的存活年龄满足晋升条件时,把对象提升到old generation regions(老年代)。
G1控制YGC开销的手段是动态改变young region的个数,YGC的过程中依然会STW(stop the world 应用停顿),并采用多线程并发复制对象,减少GC停顿时间。
1)局并发标记过程:
初始标记:标记gc roots能够关联到的对象,修改TAMS(next top at mark start),让下一阶段的用户线程能够并发运行,能在正确可用的region中创建对象。该阶段需要stw,且时间短。
并发标记:gc root进行可达性分析,不需要stw,时间相对较长。
最终标记:标记因用户程序并发进行而导致的标记产生变化的那部分数据,这部分变化记录会记录在Remembered Set Logs中,并将数据合并到Remembered Set中,需要stw
筛选回收:对各个region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。因为回收的区域比较少,stw会提高效率
11. 常用的GC策略,什么时候会触发YGC,什么时候触发FGC ?
1)默认GC策略
默认情况下,JVM针对上述不同的分代区域,使用哪些GC策略,如表所示:
运行模式 | 新生代垃GC策略 | 年老代GC策略 |
Client | Serial GC | Serial Old GC |
Server | Parallel Scavenge GC | Serial Old GC(PS MarkSweep) |
2)GC策略搭配
在进行JVM调优的过程中,并非任何一种新生代GC策略都可以和另一种年老代GC策略进行配合工作。
新生代GC策略 | 年老代GC策略 | 说明 | |
组合1 | Serial | Serial Old | Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。 |
组合2 | Serial | CMS+Serial Old | CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。 |
组合3 | ParNew | CMS | 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。 如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。 |
组合4 | ParNew | Serial Old | 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。 |
组合5 | Parallel Scavenge | Serial Old | Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。 |
组合6 | Parallel Scavenge | Parallel Old | Parallel Old是Serial Old的并行版本 |
3)什么时候进行YGC?
YGC(或者叫minor gc:MGC )称为新生代gc,因为java对象有朝生夕灭,所以minor gc比较频繁。
当新生代中eden区空间不足时,会发生一次YGC.
4)什么时候进行FGC?
触发JVM进行Full GC的情况及应对策略_gc次数多
FGC(major gc/Full gc)老年代GC,FGC比YGC慢10倍以上。
(1)老年代空间不足;
老年代的内存使用率达到了一定阈值(可调整参数),直接触发FGC。
老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。分配大对象时导致的FGC可以通过碎片整理的方式来解决。
可能发生的在老年代分配对象的情况
- YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。CMS GC时出现promotion failed(感觉可以和下面的第(2)条总结为一种情况)
- 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。CMS GC时出现promotion failed和concurrent mode failure;
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代.
对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕
后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
- 经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
- 动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
(2)
空间分配担保:统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间;
在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则说明是安全的,直接进行minor gc。
如果小于,说明YGC是不安全的。检查参数 HandlePromotionFailure 是否被设置成了允许担保失败(空间分配担保),如果不允许则直接触发Full GC;
如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。
(3)Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
(4)System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
(5)永生区空间不足;
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
6)GC发生的条件和时间
由于 Full GC(或Minor GC) 会影响性能,所以我们要在一个合适的时间点发起 GC,这个时间点被称为 Safe Point,这个时间点的选定既不能太少以让 GC 时间太长导致程序过长时间卡顿,也不能过于频繁以至于过分增大运行时的负荷。一般当线程在这个时间点上状态是可以确定的,如确定 GC Root 的信息等,可以使 JVM 开始安全地 GC。Safe Point 主要指的是以下特定位置:
- 循环的末尾
- 方法返回前
- 调用方法的 call 之后
- 抛出异常的位置 另外需要注意的是由于新生代的特点(大部分对象经过 Minor GC后会消亡), Minor GC 用的是复制算法,而在老生代由于对象比较多,占用的空间较大,使用复制算法会有较大开销(复制算法在对象存活率较高时要进行多次复制操作,同时浪费一半空间)所以根据老生代特点,在老年代进行的 GC 一般采用的是标记整理法来进行回收。
12. A.class,class.forName(A.class),实例.getClass(),ClassLoader.loadClass(name)的区别是什么?
类的加载顺序是:
加载(加载class文件到方法区);
验证(验证class文件的正确性);
准备(类变量分配内存并设置初始值,不包含实例变量。初始值是这些变量类型的默认值不是程序中设置的值);
解析(将常量池中的符号引用转换为直接引用);
初始化(类变量赋值和静态语句块的语句,与构造函数不同,不需要显示调用。虚拟机保证父类clinit方法执行完成后再执行子类的clinit方法,这样就保证了第一个执行的是Object的clinit方法。这里就决定了第13条的加载顺序,但是要知道初始化一般只进行一次)
验证代码:(注意Test类中的三个测试分别执行才能测试是否执行了初始化,因为初始化一般只会执行一次)
public class Person {
public Person(){
System.out.println("构造函数");
}
static {
System.out.println("Initializing Person");
}
public static String string = "static param initialize";
private String name = "Alfira";
public void getName() {
System.out.println(name);
}
public void setName(String name, int a) {
this.name = name + a;
}
}
public class Test {
public static void main(String[] args) {
show("com.alipay.mrchservbase.member.biz.service.impl.Person");
}
private static void show(String name) {
try {
// 返回运行时真正所指的对象
Person p = new Person();
Class classType = p.getClass();
System.out.println("执行初始化了吗?");
Method getMethod = classType.getMethod("getName", new Class[] {});
Class[] parameterTypes = {String.class, int.class};
Method setMethod = classType.getMethod("setName", parameterTypes);
getMethod.invoke(p);
setMethod.invoke(p, "Setting new ", 1);
getMethod.invoke(p);
System.out.println("实例化对象.getClass()----end\n");
// 装入类,并做类的初始化
Class classType2 = Class.forName(name);
Method getMethod2 = classType2.getMethod("getName", new Class[] {});
Class[] parameterTypes2 = {String.class, int.class};
Method setMethod2 = classType2.getMethod("setName", parameterTypes2);
System.out.println("执行初始化了吗?");
// 实例化对象
Object obj2 = classType2.newInstance();
// 通过实例化后的对象调用方法
getMethod2.invoke(obj2);
setMethod2.invoke(obj2, "Setting new ", 2); // 设置
getMethod2.invoke(obj2);
System.out.println("Class.forName----end\n");
// JVM将使用类A的类装载器,将类A装入内存(前提是:类A还没有装入内存),不对类A做类的初始化工作
Class classType3 = Person.class;
Method getMethod3 = classType3.getMethod("getName", new Class[] {});
Class[] parameterTypes3 = {String.class, int.class};
Method setMethod3 = classType3.getMethod("setName", parameterTypes3);
System.out.println("执行初始化了吗?");
// 实例化对象,因为这一句才会输出“静态初始化”以及“初始化”
Object obj3 = classType3.newInstance();
getMethod3.invoke(obj3);
setMethod3.invoke(obj3, "Setting new ", 3); // 设置
getMethod3.invoke(obj3);
System.out.println("Person.class----end");
} catch (Exception e) {
System.out.println(e);
}
}
}
(1)A.class ,这种形式创建Class对象引用时,不会自动初始化Class对象。初始化被延迟到了对静态方法或者非常数静态域首次引用时才执行(例如调用了clazz.newInstance()方法)
(2)class.forName(A.class),装入类A,并做类的初始化,通知JVM查找并加载指定的类,也就是说JVM会执行该类的静态代码段。
源码中的initialize设为的true,因此是做了类的初始化的
(3)实例.getClass(),装入类A,并做类的初始化。是动态的,其余是静态的。使用get方法时能获取当前运行对象的最新值,而上面两中方式只能获取默认值。
(4)ClassLoader.loadClass(name)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
resolve参数为是否解析。 实际上代码已经写死了,只能是false,表示不解析该类。
类加载的过程中,不 解析
目标对象,后面的 初始化
步骤也不会执行,那么静态块和静态对象就不会得到执行 。
Class.forName() 的类加载过程图解:
Classloder.loaderClass() 的类加载过程图解:
因此如果一个类有静态块和静态变量需要初始化时,可以使用class.forName()方法
13. java类加载和初始化顺序
一般顺序是:静态变量、静态块->私有变量、构造方法
如果有父类则先执行父类的
父类的静态变量、静态块->子类的静态变量、静态块->父类的私有变量、构造方法->子类的私有变量、构造方法
通过子类引用父类字段不会引发子类的初始化
通过数组定义引用类不会触发引用类的初始化
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到常量的类,因此不会引发类初始化
14. String str='abc'的内存分配过程
先在栈中创建一个对String类的对象引用变量str,然后通过符号引用去字符串常量池里找有没有"abc",如果没有,则将"abc"存放进字符串常量池,并令str指向”abc”,如果已经有”abc”则直接令str指向“abc”。
String.intern()是一个native方法,他的作用是:如果字符串常量池中已经包含一个等于次String对应的字符串,则返回代表池中这个字符串的String对象,否则将此对象加入常量池中,并返回此对象的引用。
而String s = new String("abc")是现在栈上创建一个引用,然后再在堆中分配空间存储对象的实例数据和对象类型指针
15.java堆内存一定是线程共享的吗?
堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,那么,在并发场景中,如果两个线程先后把对象引用指向了同一个内存区域,怎么办。
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是我们都知道,无论是使用哪种同步方案(实际上虚拟机使用的可能是CAS),都会影响内存的分配效率。
TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。
因为有了TLAB技术,堆内存并不是完完全全的线程共享,其eden区域中还是有一部分空间是分配给线程独享的。
这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。
TLAB是在eden区分配的,因为eden区域本身就不太大,而且TLAB空间的内存也非常小,默认情况下仅占有整个Eden空间的1%。所以,必然存在一些大对象是无法在TLAB直接分配。
遇到TLAB中无法分配的大对象,对象还是可能在eden区或者老年代等进行分配的,但是这种分配就需要进行同步控制,这也是为什么我们经常说:小的对象比大的对象分配起来更加高效。
16.在什么情况下,GC会对程序产生影响?
不管YGC还是FGC,都会造成一定程度的程序卡顿(即Stop The World问题:GC线程开始工作,其他工作线程被挂起),即使采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减少卡顿时间,而并不能完全消除卡顿。
那到底什么情况下,GC会对程序产生影响呢?根据严重程度从高到底,我认为包括以下4种情况:
- FGC过于频繁:FGC通常是比较慢的,少则几百毫秒,多则几秒,正常情况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。但是,一旦出现FGC频繁(比如几十分钟就会执行一次),这种肯定是存在问题的,它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。
- YGC耗时过长:一般来说,YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒,这种情况几乎对用户无感知,对程序的影响可以忽略不计。但是如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大,加上YGC本身比较频繁,就会导致比较多的服务超时问题。
- FGC耗时过长:FGC耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致FGC期间比较多的超时问题,可用性降低,这种也需要关注。
- YGC过于频繁:即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。
其中,「FGC过于频繁」和「YGC耗时过长」,这两种情况属于比较典型的GC问题,大概率会对程序的服务质量产生影响。剩余两种情况的严重程度低一些,但是对于高并发或者高可用的程序也需要关注。
17. 怎么排查,有哪些原因导致FGC?
- 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
- 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
- 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
- 程序BUG导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.
- 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
- JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。