jvm总结

JVM运行时数据区域

在这里插入图片描述

程序计数器

线程私有,当前线程所执行的字节码的行号指示器

Java虚拟机栈

线程私有,它的生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法栈

与虚拟机栈类似,为native方法服务

线程共享,存放对象

方法区

线程共享,存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
常量池
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
描述符,无非就是描述字段和方法的,描述字段就是字段的类型,描述方法就是方法的参数表(包括参数数量、类型、顺序),返回值。

符号引用其实引用的就是常量池里面的字符串,看下图:
MethodRef 引用的就是#8和#18这两个字符串

public class TestInt {  
    private String str = "hello";  
    void printInt(){  
        System.out.println(65535);  
    }  
} 

在这里插入图片描述
符号引用与直接引用:

  1. 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  2. 直接引用:
    直接引用可以是
    (1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的指针)
    (2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
    (3)一个能间接定位到目标的句柄
    直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

在JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。而解析阶段即是虚拟机将常量池内的符号引用替换为直接引用的过程。

运行时常量池也叫动态常量池,java虚拟机会将静态常量池里的内容转移到动态常量池里。而且动态常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中,这里String常量池是包含在动态常量池里的,但在jdk1.8后,将String常量池放到了堆中。

参考:https://blog.csdn.net/wangbiao007/article/details/78545189
https://blog.csdn.net/u014656992/article/details/51107127

垃圾收集

如何判断对象是否存活

枚举根节点,进行可达性分析,判断对象到GC ROOTS之间是否可达。
GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中

垃圾收集器

CMS

分为四个阶段:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
初始标记、重新标记会产生停顿
在这里插入图片描述
优点:并发收集、低停顿
缺点:

  1. 对CPU资源非常敏感,并发阶段会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低
  2. 无法处理浮动垃圾,并发阶段,用户线程还在运行,还在制造垃圾,这部分垃圾无法在本次GC中回收,只能在下次GC时再回收。且在CMS收集期间,必须留有一部分空间供用户线程使用,不能等老年代空间填满了再进行收集,否则可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。可通过参数-XX:CMSInitiatingOccupancyFraction设置,JDK1.6默认92%。
  3. 产生大量内存碎片,CMS垃圾收集算法为“标记—清除”。可通过-XX:+UseCMSCompactAtFullCollection参数设置是否启用内存碎片整理,默认开启。碎片整理过程无法并发,停顿时间较长,可通过参数-XX:CMSFullGCsBeforeCompaction设置多少次FullGC后再进行碎片整理。

JVM各参数如何设置

先看看Java Performance里面的推荐公式:
在这里插入图片描述
具体来讲:
Java整个堆大小设置,Xmx 和 Xms:FullGC之后的老年代内存占用的3-4倍
永久代 PermSize和MaxPermSize:FullGC后永久代内存占用的1.2-1.5倍。
年轻代Xmn:FullGC后老年代内存占用的1-1.5倍。
老年代:FullGC后老年代内存占用的2-3倍。

BTW:
Sun官方建议年轻代的大小为整个堆的3/8左右, 所以按照上述设置的方式,基本符合Sun的建议。
一般Xmx 和 Xms、 PermSize和MaxPermSize设置一样大,避免内存弹性伸缩带来性能损耗。

如何确认老年代存活对象大小?
方式1(推荐/比较稳妥):
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)

方式2:(强制触发FullGC, 会影响线上服务,慎用)
使用jmap工具可触发FullGC
jmap -dump:live,format=b,file=heap.bin 将当前的存活对象dump到文件,此时会触发FullGC

吞吐量优先应用

吞吐量优先应用,新生代、老年代均使用parallel并行收集器,多条线程并行收集,速度较快,但停顿较长。

-Xms4g  -Xmx4g  -Xmn2g -Xss128k 
-XX: PermSize=512m  -XX:MaxPermSize=512m 
//新生代用parallel Scavenge收集器
-XX:+UseParallelGC  -XX:+UseParallelOldGC
-XX:+PrintGCDetails

响应时间优先应用

响应时间优先应用,使用CMS并发收集器,因为并发收集,低停顿。

-Xms4g  -Xmx4g  -Xmn2g  -Xss128k  
-XX: PermSize=512m  -XX:MaxPermSize=512m 
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
//是否启用内存碎片整理
-XX:+UseCMSCompactAtFullCollection
//5次FullGc后进行内存碎片整理
-XX:CMSFullGCsBeforeCompaction=5  
-XX:+PrintGCDetails

小结:
新生代MinorGC比老年代FullGC回收时间短,速度更快。
无论吞吐量优先还是响应时间优先的应用都应该把新生代设大一点,新生代进入老年代的对象年龄阈值设大一点(-XX:MaxTenuringThreshold),让对象尽可能在新生代被回收,以减少进入老年代对象的数量,减小老年代发生fullGC的概率。同时,新生代增大,老年代减小,老年代回收时间也会缩短。

收集器搭配如下图:
在这里插入图片描述

为什么新生代要有两个survivor区

对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
在这里插入图片描述
假设只有一个Eden区,一个survivor区,可以把Eden区中存活的对象拷贝到survivor区,由于Eden区远大于survivor区,两者角色不能互换,所以第二次GC,Eden区存活对象再次拷贝到survivor区,但此时survivor区也有对象死去,那survivor区的对象往哪里拷贝(总不能直接进入老年代吧,对象进入老年代是要有年龄条件的),这种情况下survivor区就没法用“标记复制算法”进行垃圾回收了。所以必须要有另一块survivor区。

调优案例

请注意,jvm调优,调的是稳定,目的是尽量减少GC发生的频率。并不能带给你性能的大幅提升。服务稳定的重要性就不用多说了,保证服务的稳定,gc永远会是Java程序员需要考虑的不稳定因素之一。复杂和高并发下的服务,必须保证每次gc不会出现性能下降,各种性能指标不会出现波动,gc回收规律而且干净,找到合适的jvm设置。详细了解jvm的话请看神书《深入理解java虚拟机》。说些题外话,面试发现,jvm调优很多人都没有经验,有人甚至怀疑这东西真正是否有用,有的公司统一jvm的设置贯穿所有服务。其实只是没碰到生产条件复杂的情况而已,

举个简单例子:我曾经的公司,碰到过服务运行超过14h直接死机的问题,头天下午压测,第二天上午服务自动重启了,按照当时习惯,新服务需要压力测试满12h,原则上我的服务通过测试,由于测试环境复杂,所有开发都可以登陆而且脚本很多,qa认为可能是有脚本误杀了,但是当时离上线deadline时间还早,于是决定再压力一次,成功复现,最后查看jvm发现每次fullgc之后o区总是会多一点,jmap打印内存栈发现char对象使用逐渐增大,最后撑满内存, 最后定位到调用JNI发生内存泄露,解决了这个问题。这只是简单的一次,在那家公司,由于服务偏算法而且流量很高,碰到过很多这种问题。

还有一次,压力测试loadrunner图像显示每隔一段时间的点上响应时间立刻下降,过2s又恢复正常,规律性很强,通过jstat发现频繁生成大对象直接进入老年代,老年代很快撑大触发full gc回收,回收时间过长造成服务暂停明显,立刻反应到压测的响应上。解决的办法是调大年轻代,让大对象可以在年轻代触发yong gc,调整大对象在年轻代的回收频次(-XX:MaxTenuringThreshold=20,默认15),尽可能保证大对象在年轻代回收,减小老年代,缩短回收时间,服务果然稳定下来。当时这么调整下来会有一点性能损失,基本可以忽略不计,但是提升了服务的稳定性,这才是这次jvm调优最重要的。

参考:https://www.jianshu.com/p/5946c0a414b5
https://blog.csdn.net/losetowin/article/details/78569001
《深入理解JAVA虚拟机》
https://blog.csdn.net/antony9118/article/details/51425581

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值