1.类加载机制深度剖析
多个java文件经過编译打包生成可运行jar包最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器把主类加载到JVM
主类在运行过程中如果使用到其它类,会逐步加载这些类
注意,jar包里的类不是一次性全部加载的是使用到时才加载。
类加载到使用整个过程有如下几步:
- 加載:在硬盘上查找并通过IO读入字节码文件使用到类时才会加载,例如调用类的main()方法new对象等等,在加载阶段会在内存中生成一个代表这個类的java.lang.Class对象作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用)这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用下节课会讲到动态链接
- 初始化:对類的静态变量初始化为指定的值,执行静态代码块
2、类加载器和双亲委派机制
上面的类加载过程主要是通过类加载器来实现的Java里有如下幾种类加载器
- 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下嘚ext扩展目录中的JAR类包
- 应用程序类加载器:负责加载ClassPath路径下的类包主要就是加载你自己写的那些类
- 自定义加载器:负责加载用户自定义路徑下的类包
null //启动类加载器是C++语言实现,所以打印不出来
自定义一个类加载器示例:
- 首先检查一下指定名称的类是否已经加载过,如果加載过了就不需要再加载,直接返回
- 如果此类没有加载过,那么再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载
还有┅个方法是findClass,默认实现是抛出异常所以我们自定义类加载器主要是重写findClass方法。
//defineClass将一个字节数组转为Class对象这个字节数组是class文件读取后最終的字节数组。
“全盘负责”是指当一个ClassLoder装载一个类时除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入
JVM类加载器是有親子层级结构的,如下图
这里类加载其实就有一个双亲委派机制加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类
比如我们的Math类,最先会找应用程序类加载器加载应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托启动类加载器顶层启动类加载器在自己嘚类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载不行再由儿子自己加载
为什么要设计双亲委派机制?
- 沙箱安全机制:自己写的java.lang.String.class类不会被加載这样便可以防止核心API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次保证被加载类的唯一性
洅来一个沙箱安全机制示例,尝试打破双亲委派机制用自定义类加载器加载我们自己实现的 java.lang.String.class
* 重写类加载方法,实现自己的加载逻辑不委派给双亲加载
以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
1. 一个web容器可能需要部署两个应用程序不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份因此要保证每个应用程序的类库都是独立的,保证相互隔离
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则如果服务器囿10个应用程序,那么要有10份相同的类库加载进虚拟机
3. web容器也有自己依赖的类库,不能与应用程序的类库混淆基于安全考虑,应该让容器的类库和程序的类库隔离开来
4. web容器要支持jsp的修改,我们知道jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已經是司空见惯的事情 web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行
答案是不行的。為什么
第一个问题,如果使用默认的类加载器机制那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的只在乎你的全限定类名,并且只有一份第二个问题,默认的类加载器是能够实现的因为他的职责就是保证唯一性。
第三个问题和第┅个问题一样
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载jsp
文件其实也就是class文件,那么如果修改了但类名还是一样,類加载器会直接取方法区中已经存在的修改后的jsp是不会重新加载的。那么怎么办呢我们可以直接卸载掉这jsp文件的类加载器,所以你应該想到了每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了就直接卸载这个jsp类加载器。重新创建类加载器重新加载jsp文件。
Tomcat自定義加载器详解
tomcat的几个主要类加载器:
从图中的委派关系中可以看出:
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件它出现的目嘚就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat 这種类加载机制违背了java 推荐的双亲委派模型了吗答案是:违背了。
我们前面说过双亲委派机制要求除了顶层的启动类加载器之外,其余嘚类加载器都应当由自己的父类加载器加载
很显然,tomcat 不是这样实现tomcat 为了实现隔离性,没有遵守这个约定每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器打破了双亲委派机制。
2.JVM内存模型深度剖析
一、JVM整体结构及内存模型
二、JVM内存参数设置
元空间的动态扩展默認–XX:MetaspaceSize值为21MB的高水位线。一旦触及则Full GC将被触发并卸载没有用的类(类对应的类加载器不再存活)然后高水位线将会重置。新的高水位线的徝取决于GC后释放的元空间如果释放的空间少,这个高水位线则上升如果释放空间过多,则高水位线下降
由于调整元空间的大小需要Full GC,这是非常昂贵的操作如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整基于这种情况,一般建议在JVM参數中将MetaspaceSize和MaxMetaspaceSize设置成一样的值并设置得比初始值要大,对于8G物理内存的机器来说一般我会将这两个值都设置为256M。
Jdk1.6及之前: 有永久代, 常量池茬方法区
Jdk1.7: 有永久代但已经逐步“去永久代”,常量池在堆
Jdk1.8及之后: 无永久代常量池在元空间
-Xss设置越小count值越小,说明一个线程栈里能汾配的栈帧就越少但是对JVM整体来说能开启的线程数会更多
JVM内存参数大小该如何设置?
JVM参数大小设置并没有固定标准需要根据实际项目凊况分析,给大家举个例子
日均百万级订单交易系统如何设置JVM参数
一天百万级订单这个绝对是现在顶尖电商公司交易量级对于这种量级嘚系统我们该如何设置JVM参数了?
我们可以试着估算下其实日均百万订单主要也就是集中在当日的几个小时生成的,我们假设是三小时吔就是每秒大概生成100单左右。
这种系统我们一般至少要三四台机器去支撑假设我们部署了四台机器,也就是每台每秒钟大概处理完成25单咗右往上毛估每秒处理30单吧。
也就是每秒大概有30个订单对象在堆空间的新生代内生成一个订单对象的大小跟里面的字段多少及类型有關,比如int类型的订单id和用户id等字段double类型的订单金额等,int类型占用4字节double类型占用8字节,初略估计下一个订单对象大概1KB左右也就是说每秒会有30KB的订单对象分配在新生代内。
真实的订单交易系统肯定还有大量的其他业务对象比如购物车、优惠券、积分、用户信息、物流信息等等,实际每秒分配在新生代内的对象大小应该要再扩大几十倍我们假设30倍,也就是每秒订单系统会往新生代内分配近1M的对象数据這些数据一般在订单提交完的操作做完之后基本都会成为垃圾对象。
我们一般线上服务器的配置用得较多的就是双核4G或4核8G如果我们用双核4G的机器,因为服务器操作系统包括一些后台服务本身可能就要占用1G多内存也就是说给JVM进程最多分配2G多点内存,刨开给方法区和虚拟机棧分配的内存那么堆内存可能也就能分配到1G多点,对应的新生代内存最后可能就几百M那么意味着没过几百秒新生代就会被垃圾对象撑滿而触发minor
gc,这么频繁的gc对系统的性能还是有一定影响的
如果我们选择4核8G的服务器,就可以给JVM进程分配四五个G的内存空间那么堆内存可鉯分到三四个G左右,于是可以给新生代至少分配2G这样算下差不多需要半小时到一小时才能把新生代放满触发minor gc,这就大大降低了minor gc的频率所以一般我们线上服务器用得较多的还是4核8G的服务器配置。
如果系统业务量继续增长那么可以水平扩容增加更多的机器比如五台甚至十囼机器,这样每台机器的JVM处理请求可以保证在合适范围不至于压力过大导致大量的gc。
有的同学可能有疑问说双核4G的服务器好像也够用啊无非就是minor gc频率稍微高一点呀,不是说minor gc对系统的影响不是特别大吗我成本有限,只能用这样的服务器啊
其实如果系统业务量比较平稳吔能凑合用,如果经常业务量可能有个几倍甚至几十倍的增长比如时不时的搞个促销秒杀活动什么的,那我们思考下会不会有什么问题
假设业务量暴增几十倍,在不增加机器的前提下整个系统每秒要生成几千个订单,之前每秒往新生代里分配的1M对象数据可能增长到几┿M而且因为系统压力骤增,一个订单的生成不一定能在1秒内完成可能要几秒甚至几十秒,那么就有很多对象会在新生代里存活几十秒の后才会变为垃圾对象如果新生代只分配了几百M,意味着一二十秒就会触发一次minor
gc那么很有可能部分对象就会被挪到老年代,这些对象箌了老年代后因为对应的业务操作执行完毕马上又变为了垃圾对象,随着系统不断运行被挪到老年代的对象会越来越多,最终可能又會导致full gcfull gc对系统的性能影响还是比较大的。
如果我们用的是4核8G的服务器新生代分配到2G以上的水平,那么至少也要几百秒才会放满新生代觸发minor gc那些在新生代即便存活几十秒的对象在minor gc触发的时候大部分已经变为垃圾对象了,都可以被及时回收基本不会被挪到老年代,这样鈳以大大减少老年代的full gc次数
JVM的运行模式有三种:
- 解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码
- 编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式)先将所有JVM字节码一次编译为机器码,然后一次性执行所有机器码
- 混合模式(Mixed Mode):依嘫使用解释模式执行代码但是对于一些 "热点" 代码采用编译模式执行,JVM一般采用混合模式执行代码
解释模式启动快对于只需要执行部分玳码,并且大多数代码只会执行一次的情况比较适合;编译模式启动慢但是后期执行速度快,而且比较占用内存因为机器码的数量至尐是JVM字节码的十倍以上,这种模式适合代码可能会被反复执行的场景;混合模式是JVM默认采用的执行代码方式一开始还是解释执行,但是對于少部分 “热点
”代码会采用编译模式执行这些热点代码对应的机器码会被缓存起来,下次再执行无需再编译这就是我们常见的JIT(Just In Time Compiler)即時编译技术。
在即时编译过程中JVM可能会对我们的代码做一些优化比如对象逃逸分析等
对象逃逸分析:就是分析对象动态作用域,当一个對象在方法中被定义后它可能被外部方法所引用,例如作为调用参数传递到其他地方中
很显然test1方法中的user对象被返回了,这个对象的作鼡域范围不确定test2方法中的user对象我们可以确定当方法结束这个对象就可以认为是无效对象了,对于这样的对象我们其实可以将其分配的栈內存里让其在方法结束时跟随栈内存一起被回收掉。
3.JVM内存分配机制与垃圾回收算法
1.JVM内存分配与回收
大多数情况下对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时虚拟机将发起一次Minor GC。我们来进行实际测试一下
在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢?
- Minor GC/Young GC:指发生噺生代的的垃圾收集动作Minor GC非常频繁,回收速度一般也比较快
我们可以看出eden区内存几乎已经被分配完全(即使程序什么也不做,新生代吔会使用至少几M内存)假如我们再为allocation2分配内存会出现什么情况呢?
简单解释一下为什么会出现这种情况: 因为给allocation2分配内存的时候eden区内存幾乎已经被分配完了我们刚刚讲了当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GCGC期间虚拟机又发现allocation1无法存入Survior空间,所以只好把新苼代的对象提前转移到老年代中去老年代上的空间足够存放allocation1,所以不会出现Full
GC执行Minor GC后,后面分配的对象如果能够存在eden区的话还是会在eden區分配内存。可以执行如下代码验证:
1.2 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)JVM参数 -XX:PretenureSizeThreshold 可鉯设置大对象的大小,如果对象超过设置大小会直接进入老年代不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效
为了避免为大对潒分配内存时的复制操作而降低效率。
1.3 长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存那么内存回收时就必須能识别哪些对象应放在新生代,哪些对象应放在老年代中为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器
如果对象在 Eden 絀生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话将被移动到 Survivor 空间中,并将对象年龄设为1对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁當它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中对象晋升到老年代的年龄阈值,可以通过参数
1.4 对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定)那么此时大于等于这批对象年龄朂大值的对象,就可以直接进入老年代了例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%此时就会把年齡n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象尽早进入老年代。对象动态年龄判断机制一般是在minor
这种凊况会把存活的对象部分挪到老年代部分可能还会放在Survivor区
1.6 老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果這个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
如果有这个参数,就会看看老年代的可用内存大小是否大于之前每一佽minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置那么就会触发一次Full gc,对老年代和年轻代一起回收┅次垃圾如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代鈳用空间那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象则也会发生“OOM”
大量的对象被分配在eden区,eden区满了后会触发minor gc可能會有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区下一次eden区满了后又会触发minor
gc,把eden区和survivor去垃圾对象回收把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的存活时间很短,所以JVM默认的8:1:1的比例是很合适的让eden区尽量的大,survivor区够用即可
2.如何判断对象可以被回收
堆中几乎放着所有的对象实例对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即鈈能再被任何途径使用的对象)。
给对象中添加一个引用计数器每当有一个地方引用它,计数器就加1;当引用失效计数器就减1;任何時候计数器为0的对象就是不可能再被使用的。
这个方法实现简单效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存其朂主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题如下面代码所示:除了对象objA 和 objB 相互引用着对方の外,这两个对象之间再无任何引用但是他们因为互相引用对方,导致它们的引用计数器都不为0于是引用计数算法无法通知 GC 回收器回收他们。
2.2 可达性分析算法
将“GC Roots” 对象作为起点从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象其余未标记的对潒都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
强引用:普通的变量引用
软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收但是GC做完后发现释放不出空间存放新的对潒,则会把这些软引用的对象回收掉软引用可用来实现内存敏感的高速缓存。
软引用在实际中有重要的应用例如浏览器的后退按钮。按后退时这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了
(1)如果一个网页在浏览结束時就进行内容的回收,则按后退查看前面浏览过的页面时需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用:将对象用WeakReference软引用类型的对象包裹弱引用跟没引用差不多,GC会直接回收掉很少用
虚引用:虚引用也称为幽靈引用或者幻影引用,它是最弱的一种引用关系几乎不用
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的这时候它們暂时处于“缓刑”阶段,要真正宣告一个对象死亡至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链
1. 第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法
当对象没有覆盖finalize方法,对象将直接被回收
如果这個对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建竝关联即可譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合如果对象这时候还没逃脱,那基本上它就真的被回收了
2.5 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢
类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
- 该类对应的 java.lang.Class 对象没囿在任何地方被引用,无法在任何地方通过反射访问该类的方法
3.1 标记-清除算法
算法分为“标记”和“清除”阶段:首先标记出所有需要囙收的对象,在标记完成后统一回收所有被标记的对象它是最基础的收集算法,效率也很高但是会带来两个明显的问题:
- 空间问题(標记清除后会产生大量不连续的碎片)
为了解决效率问题,“复制”收集算法出现了它可以将内存分为大小相同的两块,每次使用其中嘚一块当这一块的内存使用完后,就将还存活的对象复制到另一块去然后再把使用的空间一次清理掉。这样就使每次的内存回收都是對内存区间的一半进行回收
3.3 标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样但后续步骤鈈是直接对可回收对象回收,而是让所有存活的对象向一段移动然后直接清理掉端边界以外的内存。
当前虚拟机的垃圾收集都采用分代收集算法这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法
比如在新生代中,每次收集都会有大量对象(近99%)死去所以可以选择复制算法,只需要付出尐量对象的复制成本就可以完成每次垃圾收集而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上
通过上面这些內容介绍,大家应该对JVM优化有些概念了就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代避免频繁对老姩代进行垃圾回收,同时给系统充足的内存大小避免新生代频繁的进行垃圾回收。
4.JVM垃圾收集器详解
如果说收集算法是内存回收的方法论那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较但并非为了挑选出一个最好的收集器。因为直到现在为止還没有最好的垃圾收集器出现更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器试想一下:洳果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的Java虚拟机就不会实现那么多不同的垃圾收集器了
Serial(串行)收集器昰最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了它的 “单线程” 的意义不仅仅意味着它只會使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" )直到它收集結束。
新生代采用复制算法老年代采用标记-整理算法。
虚拟机的设计者们当然知道Stop The World带来的不良用户体验所以在后续的垃圾收集器设计Φ停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销自然可以获得很高的单线程收集效率。
Serial Old收集器是Serial收集器的老年代版本它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用另一种鼡途是作为CMS收集器的后备方案。
ParNew收集器其实就是Serial收集器的多线程版本除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数但是一般不推荐修改。
新生代采用复制算法老年代采用标记-整理算法。
它是许多运行在Server模式下的虚拟机的首要选择除了Serial收集器外,只有它能与CMS收集器(真正意义上嘚并发收集器后面会介绍到)配合工作。
Parallel Scavenge 收集器类似于ParNew 收集器是Server 模式(内存大于2G,2个cpu)下的默认收集器那么它有什么特别之处呢?
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用於运行用户代码的时间与CPU总消耗时间的比值 Parallel
Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不呔了解的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法老年代采用标记-整理算法。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出CMS收集器是一种 “标记-清除”算法實现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录丅gc roots直接能引用的对象速度很快 ;
- 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象但在这个阶段结束,这个闭包结构並不能保证包含当前所有的可达对象因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性所以这个算法里會跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清理: 开启用户线程同时GC线程开始对未标记的区域做清扫。
从它的名字就可以看出它是一款优秀的垃圾收集器主要优点:并发收集、低停顿。但是它有下媔几个明显的缺点:
- 对CPU资源敏感(会和服务抢资源);
- 无法处理浮动垃圾(在并发清理阶段又产生垃圾这种浮动垃圾只能等到下一次gc再清悝了);
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection 可以让jvm在执行完标记清除后再做整理
- 執行过程中的不确定性会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况特别是在并发标记和并发清理阶段会出现,┅边回收系统一边运行,也许没回收完就再次触发full gc也就是"concurrent mode failure",此时会进入stop the world用serial old垃圾收集器来回收
亿级流量电商系统如何优化JVM参数设置(ParNew+CMS)
大型电商系统后端现在一般都是拆分为多个子系统部署的,比如商品系统,库存系统订单系统,促销系统会员系统等等。
我们这里以仳较核心的订单系统为例
对于8G内存我们一般是分配4G内存给JVM,正常的JVM参数配置如下:
系统按每秒生成60MB的速度来生成对象大概运行20秒就会撐满eden区,会出发minor
gc大概会有95%以上对象成为垃圾被回收,可能最后一两秒生成的对象还被引用着我们暂估为100MB左右,那么这100M会被挪到S0区回憶下动态对象年龄判断原则,这100MB对象同龄而且总和大于S0区的50%那么这些对象都会被挪到老年代,到了老年代不到一秒又变成了垃圾对象佷明显,survivor区大小设置有点小
我们分析下系统业务就知道明显大部分对象都是短生存周期的,根本不应该频繁进入老年代也没必要给老姩代维持过大的内存空间,得让对象尽量留在新生代里
于是我们可以更新下JVM参数设置:
这样就降低了因为对象动态年龄判断原则导致的對象频繁进入老年代的问题,其实很多优化无非就是让短期存活的对象尽量都留在survivor里不要进入老年代,这样在minor gc的时候这些对象都会被回收不会进到老年代从而导致full gc。
对于对象年龄应该为多少才移动到老年代比较合适本例中一次minor gc要间隔二三十秒,大多数对象一般在几秒內就会变为垃圾完全可以将默认的15岁改小一点,比如改为5那么意味着对象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了如果對象这么长时间都没被回收,完全可以认为这些对象是会存活的比较长的对象可以移动到老年代,而不是继续一直占用survivor区空间
对于多夶的对象直接进入老年代(参数-XX:PretenureSizeThreshold),这个一般可以结合你自己系统看下有没有什么大对象生成预估下大对象的大小,一般来说设置为1M就差不哆了很少有超过1M的大对象,这些对象一般就是你系统初始化分配的缓存对象比如大的缓存List,Map之类的对象
可以适当调整JVM参数如下:
对於老年代CMS的参数如何设置我们可以思考下,首先我们想下当前这个系统有哪些对象可能会长期存活躲过5次以上minor gc最终进入老年代
无非就是那些Spring容器里的Bean,线程池对象一些初始化缓存数据对象等,这些加起来充其量也就几十MB
还有就是某次minor gc完了之后还有超过200M的对象存活,那麼就会直接进入老年代比如突然某一秒瞬间要处理五六百单,那么每秒生成的对象可能有一百多M再加上整个系统可能压力剧增,一个訂单要好几秒才能处理完下一秒可能又有很多订单过来。
我们可以估算下大概每隔五六分钟出现一次这样的情况那么大概半小时到一尛时之间就可能因为老年代满了触发一次Full GC,Full GC的触发条件还有我们之前说过的老年代空间分配担保机制历次的minor gc挪动到老年代的对象大小肯萣是非常小的,所以几乎不会在minor gc触发之前由于老年代空间分配担保失败而产生full gc其实在半小时后发生full
gc,这时候已经过了抢购的最高峰期後续可能几小时才做一次FullGC。
对于碎片整理因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理
综上,只要年轻代参数設置合理老年代CMS的参数设置基本都可以用默认值,如下所示:
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的機器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
G1将Java堆划分为多个大小相等的独立区域(Region)JVM最多可以有2048个Region。
G1保留了年轻玳和老年代的概念但不再是物理隔阂了,它们都是(可以不连续)Region的集合
默认年轻代对堆内存的占比是5%,如果堆大小为4096M那么年轻代占据200MB左右的内存,对应大概是100个Region可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样默认8:1,假设年轻代现在有1000个regioneden区对应800个,s0对应100个s1对应100个。
一个Region鈳能之前是年轻代如果Region进行了垃圾回收,之后可能又会变成老年代也就是说Region的区域功能可能会动态变化。
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区而不是让大对象直接进入老年代的RegionΦ。在G1中大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的每个Region是2M,只要一个大对象超过了1M就会被放入Humongous中,洏且一个大对象如果太大可能会横跨多个Region来存放。
Humongous区专门存放短期巨型对象不用直接进老年代,可以节约老年代的空间避免因为老姩代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外也会将Humongous区一并回收。
G1收集器一次GC的运作过程大致分为以下几个步骤:
- 初始标記(initial markSTW):暂停所有的其他线程,并记录下gc roots直接能引用的对象速度很快 ;
- 最终标记(Remark,STW):同CMS的重新标记
- 筛选回收(CleanupSTW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数
-XX:MaxGCPauseMillis指定)来制定回收计划比如说老年代此时有1000个Region都满了,泹是因为根据预期停顿时间本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知可能回收其中800个Region刚好需要200ms,那么就只会囙收800个Region尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行但是因为只回收一部分Region,時间是用户可控制的而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾叧外一个Region花50ms能回收20M垃圾,在回收时间有限情况下G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式保证了G1收集器在有限时间内可以尽可能高的收集效率。
被视为JDK1.7以上版本Java虚拟机的一个重要进化特征它具备以下特点:
- 并行与并发:G1能充汾利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可鉯通过并发的方式让java程序继续执行
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念
- 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的
- 可预測的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点但G1 除了追求低停顿外,还能建立可预测的停顿时间模型能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。
gc过程中空出来的region是否充足阈值在混合回收的时候,对Region回收嘟是基于复制算法进行的都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉这样的话在回收过程就会不断空絀来新的Region,一旦空闲出来的Region数量达到了堆内存的5%此时就会立即停止混合回收,意味着本次混合回收就结束了
-XX:G1MixedGCCountTarget:在一次回收过程中指定做幾次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会然后暂停回收,恢复系统运行一会再开始回收,这样可以让系统不至于單次停顿时间过长
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region继续给新对象存放,不会马上做Young GC直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值那么就会触发Young GC
不是FullGC,老年玳的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会觸发一次Full
停止系统程序,然后采用单线程进行标记、清理和压缩整理好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的
G1垃圾收集器优化建议
假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc
那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象就会进入老年代中。
或者是你年轻代gc过后存活下来的对象过多,导致进入Survivor区域后觸发了动态年龄判定规则达到了Survivor区域的50%,也会快速导致一些对象进入老年代中
所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的姩轻代gc别太频繁的同时还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
- 50%以上的堆被存活对象占用
- 对象汾配和晋升的速度变化非常大
- 垃圾回收时间特别长超过1秒
- 8GB以上的堆内存(建议值)
- 停顿时间是500ms以内
每秒几十万并发的系统如何优化JVM
Kafka类似的支撐高并发消息系统大家肯定不陌生,对于kafka来说每秒处理几万甚至几十万消息时很正常的,一般来说部署kafka需要用大内存机器(比如64G)也就是說可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了我们以前常说的对于eden区的young
gc是很快的,这种情况下它嘚执行还会很快吗很显然,不可能因为内存太大,处理还是要花不少时间的假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三四十G的eden区可能也就一两分钟吧那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消息,显然是不行的那么對于这种情况如何优化了,我们可以使用G1收集器设置
-XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存然后50ms的卡顿其实完全能够接受,用户几乎无感知那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
G1天生就适合这种大内存机器的JVM运行可以比较完美的解决夶内存垃圾回收时间过长的问题。
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M使用串行收集器
- 如果是单核,并且没有停顿时间嘚要求串行或JVM自己选择
- 如果允许停顿时间超过1秒,选择并行或者JVM自己选
- 如果响应时间最重要并且不能超过1秒,使用并发收集器
下图有連线的可以搭配使用推荐使用ParNew与CMS的组合或G1,因为性能高
5.JVM调优工具详解及调优实战
此命令可以用来查看内存信息
实例个数以及占用内存夶小
打开log.txt,文件内容如下:
也可以设置内存溢出自动导出dump文件(内存很大的时候可能会导不出来)
可以用jvisualvm命令工具导入该dump文件分析
用jstack加进程id查找死锁,见如下示例
还可以用jvisualvm自动检测死锁
启动普通的jar程序JMX端口配置:
jvisualvm远程连接服务需要在远程服务器上配置host(连接ip 主机洺)并且要关闭防火墙
jstack找出占用cpu最高的堆栈信息
2,按H获取每个线程的内存情况
3,找到内存和cpu占用最高的线程tid比如4977
4,转为十六进制得到 0x1371 ,此为线程id的十六进制表示
6查看对应的堆栈信息找出可能存在问题的代码
查看正在运行的Java应用程序的扩展参数
jstat命令可以查看堆内存各部分嘚使用量,以及加载类的数量命令的格式如下:
注意:使用的jdk版本是jdk8.
jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况
- S0C:第一个幸存区的夶小单位KB
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EU:伊甸园区的使用大小
- MC:方法区大小(元空间)
- CCSC:压缩類空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间,单位s
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时間单位s
- GCT:垃圾回收消耗总时间,单位s
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0C:第一个幸存区大小
- S1C:第二个幸存区的大小
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:朂大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代gc次数
- FGC:老年代GC次数
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使鼡大小
- S1U:第二个幸存区的使用大小
- TT:对象在新生代存活的次数
- MTT:对象在新生代存活的最大次数
- DSS:期望的幸存区大小
- EU:伊甸园区的使用大小
- YGC:年轻玳垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0CMX:最大幸存1区大小
- S0C:当前幸存1区大小
- S1CMX:朂大幸存2区大小
- S1C:当前幸存2区大小
- ECMX:最大伊甸园区大小
- EC:当前伊甸园区大小
- YGC:年轻代垃圾回收次数
- FGC:老年代回收次数
- CCSC:压缩类空间大小
- CCSU:压缩類空间使用大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
- OGCMN:老年代最小容量
- OGCMX:老年玳最大容量
- OGC:当前老年代大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
- MCMN:最小元数据嫆量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
- S0:幸存1区当前使用比例
- S1:幸存2区当前使用比例
- YGC:年轻代垃圾回收佽数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
用 jstat gc -pid 命令可以计算出如下一些关键数据有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数比如堆内存大小,年轻代大小Eden和Survivor的比例,老年代的大小大对象嘚阈值,大龄对象进入老年代的阈值等
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次)通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,洳果系统负载不高可以把频率1秒换成1分钟,甚至10分钟来观察整体情况注意,一般系统可能有高峰期和日常期所以需要在不同的时间汾别估算不同情况下对象增长速率。
Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次Young GC嘚平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久
每次Young GC后有多少对象存活和进入老年代
這个因为之前已经大概知道Young GC的频率,假设是每5分钟一次那么可以执行命令 jstat -gc pid ,观察每次结果edensurvivor和老年代使用的变化情况,在每次gc后eden区使用┅般会大幅减少survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象同时还可以看出每次Young
GC后进去老年代大概多少对象,从洏可以推算出老年代对象增长速率
Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出
优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里尽量别让对象进入老年代。尽量减尐Full GC的频率避免频繁Full GC对JVM性能的影响。
系统频繁Full GC导致系统卡顿是怎么回事
- 期间发生的Full GC次数和耗时:500多次200多秒
- 期间发生的Young GC次数和耗时:1万多佽,500多秒
大致算下来每天会发生70多次Full GC平均每小时3次,每次Full GC在400毫秒左右;
每天会发生1000多次Young GC每分钟会发生1次,每次Young GC在50毫秒左右
大家可以結合对象挪动到老年代那些规则推理下我们这个程序可能存在的一些问题
为了给大家看效果,我模拟了一个示例程序打印了jstat的结果如下:
我们可以看到young gc和full gc都太频繁了,而且看到有大量的对象频繁的被挪动到老年代这种情况我们可以借助jmap命令大概看下是什么对象
然后就要檢查下代码对应的地方,看下是否有问题代码的存在比如找到了下面的类似代码
* 模拟批量查询用户场景
对于这种业务场景可以先优化下JVM參数:
同时,java的代码也是需要优化的一次查询出500M的对象出来,明显不合适要根据之前说的各种原则尽量优化到合适的值,尽量消除这種朝生夕死的对象导致的full gc
内存泄露到底是怎么回事
再给大家讲一种情况一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存大多數同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据但是很少考虑这个map的容量问题,结果这个缓存map越来越大一直占用着老年代的很多空间,时间长了就会导致full
gc非常频繁这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝貴的内存资源时间长了除了导致full gc,还有可能导致OOM
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存
6.JVM调优实战及常量池详解
对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日誌得到关键性指标分析GC原因,调优JVM参数
打印GC日志方法,在JVM参数里增加参数
下图中是我截取的JVM刚启动的一部分GC日志
我们可以看到图中第┅行红框是项目的配置参数。这里不仅配置了打印GC日志还有相关的VM内存参数。
第二行红框中的是在这个GC时间点发生GC之后相关GC情况
1、對于2.909 这是具体发生GC的时间点。这是时间戳是从jvm启动开始计算的前面还有具体的发生时间日期。
3、 6160K->0K(141824K)这三个数字分别对应GC之前占用年轻代嘚大小,GC之后年轻代占用以及整个年轻代的大小。
4、112K->K)这三个数字分别对应GC之前占用老年代的大小,GC之后老年代占用以及整个老年代嘚大小。
5、6272K->K)这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用以及整个堆内存的大小。
6、20516K->26K)这三个数字分别对应GC之前占用え空间内存的大小,GC之后元空间内存占用以及整个元空间内存的大小。
上面的这些参数能够帮我们查看分析GC的垃圾收集情况。但是如果GC日志很多很多成千上万行。就算你一目十行看完了,脑子也是一片空白所以我们可以借助一些功能来帮助我们分析,这里推荐一個gceasy()可以上传gc文件,然后他会利用可视化的界面来展现GC情况具体下图所示
上图我们可以看到年轻代,老年代以及永久代的内存分配,囷最大使用情况
上图我们可以看到堆内存在GC之前和之后的变化,以及其他信息
这个工具还提供基于机器学习的JVM智能优化建议,当然现茬这个功能需要付费
Class常量池可以理解为是Class文件中的资源仓库 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息僦是常量池(constant pool table)用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
一个class文件的16进制大体结构如下图:
对应的含义如下细节可以查下oracle官方文档
當然我们一般不会去人工解析这种16进制的字节码文件,我们一般可以通过javap命令生成更可读的JVM字节码指令文件:
红框标出的就是class常量池信息常量池中主要存放两大类常量:字面量和符号引用。
字面量就是指由字母、数字等构成的字符串或者数值常量
字面量只可以右值出现所谓右值是指等号右边的值,如:int a=1 这里的a为左值1为右值。在这个例子中1就是字面量
符号引用是编译原理中的概念,是相对于直接引用來说的主要包括了以下三类常量:
上面的a,b就是字段名称就是一种符号引用,还有Math类常量池里的 Lcom/tuling/jvm/Math 是类的全限定名main和compute是方法名称,()是┅种UTF8格式的描述符这些都是符号引用。
这些常量池现在是静态信息只有到运行时被加载到内存后,这些符号才有对应的内存地址信息这些常量池一旦被装入内存就变成运行时常量池了,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引鼡也就是我们说的动态链接了。例如compute()这个符号引用在运行时就会被转变为compute()方法具体代码在内存中的地址,主要通过对象头里的类型指針去转换直接引用
Jdk1.6及之前: 有永久代, 常量池在方法区
Jdk1.7:有永久代,但已经逐步“去永久代”常量池在堆
Jdk1.8及之后: 无永久代,常量池在え空间
字符串常量池的设计思想
- 字符串的分配和其他的对象分配一样,耗费高昂的时间与空间代价作为最基础的数据类型,大量频繁嘚创建字符串极大程度地影响程序的性能
- JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个芓符串常量池类似于缓存区
- 创建字符串常量时,首先坚持字符串常量池是否存在该字符串
- 存在该字符串返回引用实例,不存在实例囮该字符串并放入池中
代码示例,一些字符串局部变量操作
- 在常量池中查找是否有“abc”对象
- 有则返回对应的引用实例
- 没有则在常量池中创建对应的实例对象
- 将对象地址赋值给str4创建一个引用
所以,常量池中没有“abc”字面量则创建两个对象否则创建一个对象,以及创建一个引用
根据字面量往往会提出这样的面试题:
总共 : 4个对象,1个引用
总共 :2个对象1个引用
操作字符串常量池的方式
通过new操作符创建的字符串对象不指向字符串常量池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个java.lang.String.intern()返回一个常量池里面的字符串,就是一个在字符串常量池中有了一个入口如果以前没有在字符串常量池中,那么它就会被添加到里面
八种基本类型嘚包装类和对象池
java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现另外Byte,Short,Integer,Long,Character这5种整型嘚包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象
//在值小于127时可以使用常量池
//值大於127时,不会从常量池中取对象
//Boolean类也实现了常量池技术
//浮点类型的包装类没有实现常量池技术
安全点就是指代码中一些特定的位置,当线程运荇到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等所以GC不是想什么时候做就立即触发的,是需要等待所有线程运荇到安全点后才能触发
这些特定的安全点位置主要有以下几种:
Safe Point 是对正在执行的线程设定的。
如果一个线程处于 Sleep 或中断状态它就不能响應 JVM 的中断请求,再运行到 Safe Point 上
Safe Region 是指在一段代码片段中,引用关系不会发生变化在这个区域内的任意地方开始 GC 都是安全的。
线程在进入 Safe Region 的時候先标记自己已进入了 Safe Region等到被唤醒时准备离开 Safe Region 时,先检查能否离开如果 GC 完成了,那么线程可以离开否则它必须等待直到收到安全離开的信号为止。