【JAVA】JVM

1 JVM运行时数据区(只是一个概念模型)

在这里插入图片描述
概括地说来,JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)。

⑴程序计数器

唯一一个没有规定任何OutOfMemoryError区域。用来指示当前线程执行的字节码到了第几行(因为线程会因为没有获取CPU时间片而间断)

⑵虚拟机栈(就是我们常说的栈)

生命周期和线程相同,方法调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。

⑶本地方法栈

与虚拟机栈相似,虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机使用的Native方法服务。

⑷堆

堆是垃圾收集管理的主要区域

⑸方法区

主要存储类的相关信息,它的实现就是永久代,JDK8之后永久代被移到与一个堆不相连的本地内存区域,也即就是元空间,由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
在这里插入图片描述

2 不同数据类型存放位置
⑴堆

主要用于存放对象和基本数据类型成员变量

⑵栈

基本数据类型的局部变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量

⑶方法区

主要存储类的相关信息,它的实现就是永久代,JDK8之后永久代被移到与一个堆不相连的本地内存区域,也即就是元空间,由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。

3 JVM垃圾回收
⑴可达性算法

采用可达性算法来确定对象需要被垃圾回收。
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(或者说从GC Roots到这个对象不可达),则证明此对象是不可用的。
注意:不可到达的对象也并非是非死不可的。对象死亡必须标记两次,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(也就是说对象的finalize()方法只能被调用一次),虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只需要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

⑵垃圾回收算法
①标记清除

第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。
优点:

  • 简单,不会增加编译器或赋值函数的负担
    缺点:
  • 耗时
  • 产生大量不连续内存碎片,导致新进大对象无法找到连续内存而触发新的垃圾回收
②标记复制

内存按容量划分为大小相等的两块每次只使用其中的一块。当这一块的内存使用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:

  • 简单高效
  • 没有内存碎片
    缺点:
  • 内存缩小为原来一半
③标记整理

第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。但是标记整理算法一看效率就不高。

④分代收集

当前商业虚拟机使用的主流时算法:

  • 新生代:
    采用改进的标记复制算法。
    改进的标记复制算法:
    将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。新创建的对象在Eden区,当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间,清理完成后,刚刚被清理的Eden和另一块在回收时放入存活对象的Survivor空间作为使用内存,刚被清理的Survivor作为保留空间,以便后面用来回收之用。
    改进的收集算法的缺点:那块空的Survivor空间能否放得下Eden和使用的Survivor空间中还存活的对象,如果Survivor空间不够存放上一次新生代收集下来的存活对象,此时就需要向老年代“借”内存,那些剩余未放下的对象就通过分配担保机制进入老年代。
    老年代:
  • 对象存活率高,采用标记清除或标记整理算法。
4.垃圾收集器

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
我们先看看什么叫吞吐量:吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

⑴Serial收集器

单线程,进行垃圾回收时,必须暂停其它所有工作线程。这个基本不用,所以也不多讲。

⑵Parallel(并行)收集器

可以使用多个线程扫描并压缩堆。进行垃圾回收时也会停止工作线程。吞吐量大,停顿时间较短。

  • Parallel New:用于年轻代,复制算法,多线程回收
  • Parallel Scavenge:用于年轻代,复制算法,多线程回收,关注吞吐量,它不能和CMS一起使用,和Parallel New相比,它可以设置最大gc停顿时间(-XX:MaxGCPauseMills)以及gc时间占比(-XX:GCTimeRatio)。
  • Parallel Old:用于年老代,标记-整理算法,多线程回收
⑶CMS收集器

标记-清除算法,用于年老代,并发收集,低停顿,但是会产生大量碎片,吞吐量稍低,适用于对系统响应时间要求较高的系统中,如页面请求/web服务器。

⑷G1收集器

出生于JDK1.7,对大于4G的堆有更好的支持
基础算法:标记-整理算法,因而不会产生内存碎片

⑸ZGC

ZGC是java11的一项新技术,它可以控制GC时间在10秒以内

5.JVM参数
  • -Xmx:最大堆大小
  • -Xms:初始堆大小
  • -Xmn:年轻代大小
  • -XXSurvivorRatio:年轻代中Eden区与Survivor区的大小比值
  • -Xss128k:设置每个线程的栈大小,默认JDK1.4中是256K,JDK1.5+中是1M,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误.

-Xmx10240m -Xms10240m -Xmn5120m -XXSurvivorRatio=3
年轻代5120m, Eden:Survivor=3,Survivor区大小=1024m(Survivor区有两个,即将年轻代分为5份,每个Survivor区占一份),总大小为2048m。

6.逃逸分析

逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸。

  • 1、方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中;
  • 2、线程逃逸:如类变量或实例变量,可能被其它线程访问到;
    如果不存在逃逸行为,则可以对该对象进行如下优化:栈上分配、标量替换和同步消除。
-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析
⑴栈上分配

栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能。

⑵标量替换

Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。标量替换基于分析逃逸基础之上,开启标量替换必须开启逃逸分析。

-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭标量替换
⑶同步消除

线程同步本身比较耗,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析

-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
7.堆外内存

堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。
堆外内存了解即可,可以通过设置-XX:MaxDirectMemorySize=10M控制堆外内存的大小。
优点:

  • 减少了垃圾回收的工作
  • 加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
    缺点:
  • 堆外内存难以控制,如果内存泄漏,那么很难排查
  • 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。
8.内存泄漏和内存溢出
1.内存泄漏

对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。比如A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除。

2.内存溢出

是指程序在申请内存时,没有足够的内存空间供其使用。

⑴栈溢出

方法调用、方法内的局部变量都是在栈空间申请的,如果这一块空间不够用了就会产生StackOverflowError

⑵堆溢出

对象需要的内存大于了我们给虚拟机配置的内存,导致OutOfMemoryError。
不断的创建线程,导致OutOfMemoryError

9.基本类型的虚拟机内部符号

在这里插入图片描述

10.即时编译

一般情况下代码会先被 Java 虚拟机解释执行,之后反复执行的热点代码(一般都是方法级)则会被即时编译成为机器码(即时编译器将加载进JVM中的类的某个方法所对应的byte数组编译为机器码。也可以理解为从class文件到机器码)。
通常通过编译器调优的情况很小,因为能做的即时编译器都做了。

⑴分层编译

HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal。

  • C1 的启动性能好
  • C2 的峰值性能好
  • Graal 是一个实验性质的即时编译器,可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并且替换 C2。

Java 8 默认开启了分层编译,分层编译的五个层级:

  • 解释执行;
  • 执行不带 profiling 的 C1 代码;
  • 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
  • 执行带所有 profiling 的 C1 代码;
  • 执行 C2 代码。

通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。然而,对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层(profiling是指在程序执行过程中,收集能够反映程序执行状态的数据, profiling越多,其额外的性能开销越大),分层编译中的 0 层、2 层和 3 层都会进行 profiling

⑵即时编译触发

即时编译是由方法调用计数器和循环回边计数器(循环尾部到循环头部的控制流边)触发的。在使用分层编译的情况下,触发编译的阈值是根据当前待编译的方法数目动态调整的。

⑶OSR编译

可以看到,决定一个方法是否为热点代码的因素有两个:方法的调用次数、循环回边的执行次数。即时编译便是根据这两个计数器的和来触发的。为什么 Java 虚拟机需要维护两个不同的计数器呢?

实际上,除了以方法为单位的即时编译之外,Java 虚拟机还存在着另一种以循环为单位的即时编译,叫做 On-Stack-Replacement(OSR)编译。循环回边计数器便是用来触发这种类型的编译的。

11.方法内联

方法内联只发生在即时编译器中。
在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。
即时编译器不会无限制地进行方法内联,因为内联越多,编译时间越长。
最后,即时编译器将根据方法调用指令所在的程序路径的热度,目标方法的调用次数及大 小,以及当前 IR 图的大小来决定方法调用能否被内联。

12.intrinsic

在 HotSpot 虚拟机中,所有被@HotSpotIntrinsicCandidate标注的方法都是 HotSpot intrinsic。对这些方法的调用,会被 HotSpot 虚拟机替换成高效的指令序列。而原本的方法实现则会被忽略掉。
因为有的时候我们可以用JAVA程序无法表达的CPU指令,比如Integer.bitCount在JAVA代码中是这样的:

@HotSpotIntrinsicCandidate
 public static int bitCount(int i) {
 // HD, Figure 5-2
 i = i - ((i >>> 1) & 0x55555555);
 i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
 i = (i + (i >>> 4)) & 0x0f0f0f0f;
 i = i + (i >>> 8);
 i = i + (i >>> 16);
 return i & 0x3f;
}

但是在 X86_64 体系架构中,我们仅需要一条指令popcnt,便可以直接统计出 int 值 中 1 的个数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值