垃圾回收
java需要回收的区域
回收线程共享的区域(方法去和堆区),线程不共享的区域(程序计数器,java虚拟机栈和方法堆栈)则不需要回收,栈的生命周期与线程的生命周期一致。当线程执行结束时,其线程栈也会被销毁,这些局部变量自然也会被释放,不需要垃圾回收。
常见的引用类型
强引用,最常见的引用方式,由可达性分析算法来判断
软引用,对象在没有强引用情况下,内存不足时会回收
弱引用,对象在没有强引用情况下,会直接回收
虚引用,通过虚引用知道对象被回收了
终结器引用,对象回收时可以自救,不建议使用
回收算法
1.标记清除算法
(1)标记阶段将所有活动的对象进行标记,java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活的对象
(2)清楚阶段从内存中删除没有被标记也就是非存活对象
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可
缺点:碎片化问题,由于内存是连续的,所以在对象被删除之后,内存会出现很多细小的可用内存单元,如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。分配速度慢,由于内存碎片的存在,需要维护一个空闲链表,及有肯能在每次需要遍历到链表的最后才能获得合适的内存空间。
2.复制算法
复制算法将内存分割为两个From空间和To空间,对象分配阶段,创建对象
GC阶段开始,将GC Root搬运到To空间,将GC Root关联的对象搬运到To空间
清理From空间,并把名称互换
优点:吞吐量高,复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历过程,因而性能较好,但是不如标记-清楚算法,因为标记清除算法不需要进行对象的移动。不会发生碎片化,复制算法再复制之后就会将对象按顺序放入To空间
缺点:只能让一半的空间来为创建对象使用。
3.标记整理算法
对标记清除算法进行改进,解决了内存碎片问题
优点是内存利用率高没有碎片,缺点是整理的消耗大,整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能
4.分代垃圾回收算法
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区,随着对象在伊甸区越来越多,如果Eden园区满了,新创建的对象已经无法放入,就会触发年轻代GC,称为Minor GC,或者Yong GC。Minor GC会把伊甸区中和From需要回收的对象回收,把没有回收的对象放入To区,即实现复制算法。
接下来,S0会变成To区,S1变成From。当伊甸区满了再往里面放对象,依然会发生Minor GC,此时回收伊甸园区和S1(From)中的对象,并把伊甸区和from区中的剩余对象放入S0。每一次Minor GC都会为对象记录他的年龄,初始值为0,每次GC完加1
如果Minor GC后对象的年龄达到闽值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
垃圾回收器组合
年轻代-Serial垃圾回收器
Serial是一种单线程串行回收年轻代的垃圾回收器,使用的复制算法来回收。
优点:单cpu处理器下吞吐量非常出色
缺点:多cpu下吞吐量不如其它垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
适用场景:java编写的客户端程序或者硬件配置有限的场景
老年代-SerialOld垃圾回收器
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收;使用的是标记-整理算法来回收
-XX:+UseSerialGC 新生代,老年代都使用串行回收器,流程和年轻代图一样。
优缺点和年轻代一样,同时老年代的使用场景还可以在CMS的特殊情况下使用
年轻代-ParNew垃圾回收器
本质上是对Serial在多cpu下的优化,使用多线程进行来集回收;采用复制算法。
-XX:+UseParNewGC 新生代使用的ParNew回收期,老年代使用的串行回收器
优点:多cpu处理器下停顿时间较短
缺点:吞吐量和停顿时间不如G1,在JDK9之后不建议使用
适用场景:jdk8及之前的版本,与CMS老年代垃圾回收器搭配使用
老年代-CMS垃圾回收器
CMS(concurrent mark sweep)关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同步执行,减少了用户线程的等待时间
参数:-XX:+UseConcMarkSweepGC ,使用的是标记清除算法
优点:系统由于垃圾回收出现的停顿时间段,用户体验好
缺点:内存碎片问题,退化问题,浮动垃圾问题
CMS执行步骤:
1.初始标记,用极短的时间标记出GCRoots能直接关联到的对象。
2并发标记,标记所有的对象,用户线程不需要暂停。
3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记
4.并发清理,清理死亡的对象,用户线程不需要暂停。
年轻代-Parallerl Scavenge垃圾回收器
Parallerl Scavenge(PS)是jdk8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量,具备自动调整堆大小的特点,使用的复制算法
优点:吞吐量高,而且手动可控,为了提高吞吐量,虚拟机会动态调整堆的参数
缺点:不能保证单次的停顿时间
适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象,例如大数据的处理,大文件导出
Parallel Scavenge允许手动设置最大暂停时间和吞吐量。Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
最大暂停时间:-XX:MaxGCPauseMillis=n设置每次垃圾回收时的最大停顿毫秒数
吞吐量:-XX:GCTimeRatio=n设置吞吐量为n(用户线程执行时间 =n/n + 1)
自动调整内存大小:-XX:+UseAdaptiveSizePolicy设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小
老年代-Parallel Old垃圾回收器
ParallelOld是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。算法用的是标记-整理算法
参数: -XX:+UseParallelGC 或-XX:+UseParallelOldGC可以使用Parallel Scavenge + Parallel Old这种组合(PS+PO)
优点:并发收集,在多核cpu下效率较高
缺点:暂停时间会比较长
G1垃圾回收器
JDK9之后默认的垃圾回收器是G1 (Garbage First) 垃圾回收器(JDK9之后强烈建议使用G1垃圾回收器)
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小;CMS关注暂停时间,但是吞吐量方面会下降。而G1设计目标就是将上述两种垃圾回收器的优点融合:
- 支持巨大的堆空间回收,并有较高的吞吐量
- 支持多CPU并行垃圾回收。
- 允许用户设置最大暂停时间。
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
有两种回收方式:
- 年轻代回收(Yong GC)回收Eden区和Survivor区中不用的对象。会导致STWG1中可以通过参数-XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。执行过程如下:
-
-
- 新创建的对象会存放在Eden区当G1判新年轻代区不足(max默认60%需要回收时会执行Young GC
- 标记出Eden和Survivor区域中的存活对象
- 根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域
- G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。比如-XX:MaxGCPauseMilis=n(默认200),每个Region回收耗时0ms,那么这次回收最多只能回收4个Region。
- 后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区.
- 当某个存活对象的年龄到达闯值 (默认15),将被放入老年代
- 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
- 多次回收之后,会出现很多Old老年代区,此时总堆占有率达到闻值时(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。
-
- 混合回收(Mixed GC)
- 混合回收分为:初始标记(initial mark)、并发标记(concurrent mark) 、最终标记(remark或者FinalizeMarking)、并发清理 (cleanup)
- G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbagefirst)名称的由来
注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。单线程执行标记-整理算法
Shenandoah回收器
ZGC 垃圾回收器
ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到16TB 的堆大小,堆大小对STW的时间基本没有影响。ZGC降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以如果Java服务比较关注QPS(每秒的查询次数)那么G1是比较不错的选择
OracleJDK和OpenJDK中都支持ZGC,阿里的DragonWell龙井JDK也支持ZGC属于其自行对OpenJDK 11的ZGC进行优化的版本。
建议使用JDK17之后的版本,延迟较低同时无需手动配置并行线程数分代 ZGC添加如下:
- 参数启用 -XX:+UseZGC -XX:+ZGenerational
- 非分代 ZGC通过命令行选项启用 -XX:+UseZGC
ZGC中可以使用Linux的Huge Page大页技术优化性能,提升吞吐量、降低延迟。注意:安装过程需要root权限,所以ZGC默认没有开启此功能。
操作步骤:
1、计算所需页数,Linuxx86架构中大页大小为2MB,根据所需堆内存的大小估算大页数量。比如堆空间需要
16G,预留2G (JVM需要额外的一些非堆空间),那么页数就是18G/2MB = 9216。
2、配置系统的大页池以具有所需的页数(需要root权限):
Secho 9216>/sys/kernel/mm/hugepages/hugepages-2048kB/nr hugepages
3、添加参数-XX:+UseLargePages 启动程序进行测试
内存泄漏
内存泄漏是指Java中不再使用的对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收
内存溢出是因为内存的使用量超过了java虚拟机可以分配的上限,最终产生了内存溢出OutOfMemory的错误,它是由内存泄漏或者并发请求导致的
1.代码中存在的内存泄漏
equals()和hashCode()导致内存泄漏
比如像一个list中存学生数据,当存入的id一样时,如果当作新学生存进去就会导致内存不断增加,最终系统内存溢出,因此不能简单地通过自带的equals来比较,需要重写。利用hash函数去存时也需要重写该函数,因为自带的hashCode它使用了取散列值加随机数的方式,每一次同一对象求得的hash值是不一样的,最终旧数据放到了一个新的位置上。
内部类引用外部类
- 非静态内部类都会默认持有外部类,尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。
- 匿名内部类如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者
public class Outer { private byte[] bytes = new byte[1024 * 1024 * 1024]; public List<String> newList() { List<String> list = new ArrayList<String>() {{ add("1"); add("2"); }}; return list; } public static void main(String[] args) throws IOException { System.in.read(); int count = 0; ArrayList<Object> objects = new ArrayList<>(); while (true) { System.out.println(++count); objects.add(new Outer().newList()); } } }
ThreadLocal错误使用
ThreadLocal可以存放线程的本地变量的。如果仅仅使用手动创建的线程,就算没有调用ThreadLocal的remove方法清理数据,也不会
产生内存泄漏。因为当线程被回收时,ThreadLocal也同样被回收。但是如果使用线程池就不一定了。
通过静态字段保存对象
如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存泄漏。
解决方案:
1、尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为null。
2、使用单例模式时,尽量使用懒加载,而不是立即加载
3、Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效
对象存在静态变量中
public class MemoryLeakExample { // 静态变量保存对象 private static SomeObject staticObject = new SomeObject(); public static void main(String[] args) { // 这里使用 staticObject // ... // 不再需要 staticObject,但是没有及时清理 // staticObject = null; // 缺少这行代码可能导致内存泄漏 } }
staticObject的生命周期不是类实例的生命周期,不随之被清理,会一直在内存,如果列表中存了太多无用对象会无法回收。
懒加载案例:
缓存案例:
资源没有正常关闭
2.并发导致的内存溢出-参数不当
由于参数设置不当,比如堆内存设置过小,导致并发量增加之后超过堆内存的上限。
解决方案:调整参数(增大堆内存)
案例分析1-分页查询文章接口的内存溢出
文章微服务中的分页接口在没有限制最大单次访问条数时,并且单个文章对象占用的内存量较大,在业务高峰期并发量较大时这部分从数据库获取到内存之后会占用大量的内存空间
解决方案:
- 与产品设计人员沟通,限制最大的单次访问条数。
- 分页接口如果只是为了展示文章列表,不需要获取文章内容,可以大大减少对象的大小
- 在高峰期对微服务进行限流保护
案例分析2-新增加一个查询id是否已存在接口导致溢出
Mybatis在使用foreach进行sql拼接时,会在内存中创建对象形成sql语句片段,如果foreach处理的数组护着集合元素个数过多,sql会占用大量的内存空间
解决思路:
- 限制参数中id的个数
- 将id缓存到redis或者内存缓存中,通过缓存进行校验
案例分析3-到处大文件内存溢出
excel文件导出如果没有使用POI的XSSFWorkbook,再大数据量(几十万)的情况下会占用大量的内存
解决思路:
- 使用poi的SXSSFWorkbook,这是原生的接口,实现起来要麻烦很多,因此不建议使用
- hutool提供的BigExcelWriter减少内存开销
- 使用easy excel,对内存进行了大量的优化
3.并发引起的内存溢出-设计不当
系统的方案设计不当,比如:
- 从数据库获取超大数据量的数据
- 线程池设计不当
- 生产者-消费者模型,消费者消费性能问题
解决方案:优化设计方案
案例分析4-ThreadLocal使用时占用大量内存
当一个用户请求过来的时候,在微服务中会有一个拦截器(HandlerInterceptor),包括以下三个函数:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在请求处理前执行的逻辑 return true;} // 返回 true 继续执行,返回 false 中断执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 在请求处理后,视图渲染之前执行的逻辑} @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 在整个请求完成后执行的逻辑}
拦截器根据用户的请求头信息组装一些对象放到threadLocal中,每一个线程对应一个threadLocal,由于tomcat会设置核心线程(min-spare:20),这些线程为了保证快速响应而一直存活,因此这些threadLocal保存的信息就不会被删除。保存的信息是为了在后面的controller层或者service层去使用。
问题产生的原因正是由于这些不会被清理的线程不断地创建数据到threadLocal中而浪费了内存,导致内存溢出,需要我们手动将其清理
案例分析5-文章内容审核接口的内存问题
在项目中如果要使用异步进行业务处理,或者实现生产者-消费者的模型,如果在java代码中实现,会占用大量的内存去保存中间数据;需要尽量使用MQ消息队列,可以很好的将中间数据单独进行保存,不会占用java的内存。同时也可以将生产者和消费者拆分成不用的微服务。
GC调优
对垃圾回收进行调优,避免因为垃圾回收导致性能下降。主要有以下三个核心功能:
- 通用Jvm参数的设置
- 特定垃圾回收器的Jvm参数的设置
- 解决频繁的FULLGC引起的程序性能的问题
核心指标
垃圾回收吞吐量:处理用户请求的时间/(处理用户请求的时间+GC的时间)
延迟:发请求到收到结果的时间
内存的使用量:java应用程序占系统内存的最大值
java Agent
实现java工具的核心技术,有两种加载模式:
静态加载模式:
动态加载模式:
虚拟机原理
栈上的数据存储
Java虚拟机的局部变量表(Local Variable Table)是用于存储方法中的局部变量的数据结构。局部变量表是每个线程私有的,它位于栈帧(Stack Frame)中,每个方法调用都会创建一个新的栈帧。但是局部变量表它的一个槽slot代表的字节数却不是固定的,在32位操作系统中,一个slot代表4个字节,64位则是8个字节,为了提高效率,虚拟机为局部变量分配的最小单元是一个槽。
如下图,long在32位虚拟机上占了8个字节刚好,但是在64位上占了16个字节
八大数据集结构的占用情况:
为了方便处理,在虚拟机中bool和byte等都是用int类型来存在局部变量表中的
问题1:栈中的数据要保存到堆上或者从堆中加载到栈上时应该怎么处理
- 队中数据加载到栈上,由于栈上的空间大于等于堆上的空间,所以直接处理但是需要注意一下符号位,boolean和char位无符号,地位复制,高位补零
- 栈中的数据要保存到堆上,byte,char,short由于堆上存储空间较少,需要讲噶为去掉,boolean比较特殊,只保留最后一位,其余不要
对象在堆中的存储
标记字段的内容
指针压缩技术
在64位的Java虚拟机中,Klass Pointer以及对象数据中的对象引用都需要占用8个字节,为了减少这部分的内存使用量,64 位Java 虚拟机使用指针压缩技术,将堆中原本 8个字节的 指针压缩成4个字节,此功能默认开启可以使用-XX:-UseCompressedOops关闭。压缩后klass pointer由8个字节减为4B,name字段的地址指针也减为4B.
为了减少内存消耗,我们存储的对象地址原本保存的时8B(64os),但是现在将内存划分为块,只保存块编号
内存对齐技术
主要是为了解决cpu缓存行失效的问题
同时为了节省空间,还有一个字段重排列技术,在Hotspot中,要求每个属性的偏移量Offset (字段地址- 起始地址)必须是字段长度的N倍,恰当的将变量在内存中重新排列来满足要求。如果还不行,就填充gap占用一段内存。
方法调用的原理
在JVM中,一共有五个字节码指令可以执行方法调用:
1、invokestatic:调用静态方法
2、invokespecial:调用对象的private方法、构造方法,以及使用 super关键字调用父类实例的方法、构造方法以及所实现接口的默认方法。
3、invokevirtual: 调用对象的非private方法
4、invokeinterface: 调用接口对象的方法
5、invokedynamic:用于调用动态方法,主要应用于lambda表达式中,机制极为复杂了解即可
Invoke方法的核心作用就是找到字节码指令并执行,找方法区中的类的方法位置
方法调用的原理 - 静态绑定
1、编译期间,invoke指令会携带一个参数符号引用,引用到常量池中的方法定义。方法定义中包含了类名+方法名 + 返回值 + 参数(在Java虚拟机中,常量池是一块用于存储符号引用和其他常量的区域。符号引用的解析过程通常涉及到查找常量池中的信息,以获取具体的方法、字段或类的信息)
2、在方法第一次调用时,这些符号引用就会被替换成内存地址的直接引用,这种方式称之为静态绑定(静态绑定适用于处理静态方法、私有方法、或者使用final修饰的方法,因为这些方法不能被继承之后重写。invokestatic,invokespecial,final修饰的invokevirtual)
方法调用的原理 - 动态绑定
对于非static、非private、非fina的方法,有可能存在子类重写方法,那么就需要通过动态绑定来完成方法地址绑定的T作。比如在这段代码中,调用的其实是Cat类对象的eat方法,但是编译完之后虚拟机指令中调用的是Animal类的eat方法这就需要在运行过程中通过动态绑定找到cat类的eat方法,这样就实现了多态
动态绑定是基于方法表来完成的,invokevirtual使用了虚方法表(vtable),invokeinterface使用了接口方法表(itable),整体思路类似。所以接下来使用invokevirtual和虚方法表来解释整个过程。每个类中都有一个虚方法表,本质上它是一个数组,记录了方法的地址。子类方法表中包含父类方法表中的所有方法;子类如果重写了父类方法,则使用自己类中方法的地址进行替换。
异常捕获的原理
异常表在编译期生成,存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。
起始/结束PC:此条异常捕获生效的字节码起始/结束位置
跳转PC:异常捕获之后,跳转到的字节码位置。
程序运行中触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异
常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。
1、如果匹配,跳转到“跳转PC”对应的字节码位置
2、如果遍历完都不能匹配,说明异常无法在当前方法执行时被捕获,此方法栈帧直接弹出,在上一层的栈中进行异常捕获的查询。
finally的处理方式就相对比较复杂一点了,分为以下几个步骤:
1、finally中的字节码指令会插入到try和 catch代码块中,保证在try和catch执行之后一定会执行finally中的代码。
2、如果抛出的异常范围超过了Exception,比如Error或者Throwable,此时也要执行finally,所以异常表中增加了两个条目。覆盖了try和catch两段字节码指令的范围,any代表可以捕获所有种类的异常。
JIT即时编译器
在Java中,JIT即时编译器是一项用来提升应用程序代码执行效率的技术。字节码指令被Java 虚拟机解释执行,如果有一些指令执行频率高,称之为热点代码,这些字节码指令则被JT即时编译器编译成机器码同时进行一些优化最后保存在内存中,将来执行时直接读取就可以运行在计算机硬件上
在HotSpot中,有三款即时编译器,c1、C2和Graal,其中Graal在GraalVM章节中已经介绍过C1编译效率比C2快,但是优化效果不如C2。所以C1适合优化一些执行时间较短的代码,c2适合优化服务端程序中长期执行的代码
JDK7之后,采用了分层编译的方式,在JVM中C1和C2会一同发挥作用,分层编译将整个优化级别分成了5个等级.
C1即时编译器和C2即时编译器都有独立的线程去进行处理,内部会保存一个队列,队列中存放需要编译的任务。一般即时编译器是针对方法级别来进行优化的,当然也有对循环进行优化的设计
JIT优化方法
- 方法内联
方法体中的字节码指令直接复制到调用方的字节码指令中,节省了创建栈帧的开销
方法内联的限制
并不是所有的方法都可以内联,内联有一定的限制:
1、方法编译之后的字节码指令总大小2、方法编译之后的字节码指令总大小
3、方法编译生成的机器码不能大于1000字节。 (通过-XX:InlineSmallCode=值 控制)
4、接口的实现必须小于3个,如果大于三个就不会发生内联。
- 逃逸分析
逃逸分析指的是如果JIT发现在方法内创建的对象不会被外部引用,那么就可以采用锁消除、标量替换等方式进行优化。
锁消除:锁消除指的是如果对象被判断不会逃逸出去,那么在对象就不存在并发访问问题,对象上的锁处理都不会执行
逃逸分析 - 标量替换:
逃逸分析真正对性能优化比较大的方式是标量替换,在Java虚拟机中,对象中的基本数据类型称为标量,引用的其他对象称为聚合量。标量替换指的是如果方法中的对象不会逃逸,那么其中的标量就可以直接在栈上分配。
垃圾回收器
1、卡表 Card Table
每一个Region都拥有一个自己的卡表,卡表是一个字节数组,如果产生了跨代引用(老年代引用年轻代),G1会将卡表上引用对象所在的位置字节内容进行修改为0,称为脏卡。卡表的主要作用是生成记忆集。卡表会占用一定的内存空间,堆大小是1G时,卡表大小为1G = 1024 MB /512 = 2MB
2、记忆集 Rememberedset (简称RS或RSet)
每一个Region都拥有一个自己的记忆集,如果产生了跨代引用,记忆集中会记录引用对象所在的卡表位置。标记阶段将记忆集中的对象加入GC ROOT集合中一起扫描,就可以将被引用的对象标记为存活。
3、写屏 Write Barrier
G1使用写屏障技术,在执行引用关系建立的代码执行后插入一段指令,完成卡表的维护工作会损失一部分的性能,大约在5%~10%之间。G1垃圾回收器原理 - 混合回收多次回收之后,会出现很多0ld老年代区,此时总堆占有率达到闻值(默认45%)时会触发混合回收MixedGC。混合回收会由年轻代回收之后或者大对象分配之后触发,混合回收会回收 整个年轻代 + 部分老年代老年代很多时候会有大量对象,要标记出所有存活对象耗时较长,所以整个标记过程要尽量能做到和用户线程并行执行。
混合回收的步骤:
1、初始标记,STw,采用三色标记法标记从GC Root可直达的对象
2、并发标记,并发执行,对存活对象进行标记
3、最终标记,STw,处理SATB相关的对象标记。
4、清理,STW,如果区域中没有任何存活对象就直接清理
5、转移,将存活对象复制到别的区域