一、从命令提示符开始引入
图1: java命令显示
安装完JVM环境之后,我们第一件事情就是:在cmd.exe中输入 java -version 来验证环境变量等是否设置成功,下面来主讲回显。
java version "1.8.0_131" 版本
Java(TM) SE Runtime Environment (build 1.8.0_131-b11) 环境参数
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode) java HotSpot
1、 HotSpot
JVM将class文件编译为本地可执行的代码,每次都需要转换,也是java跨平台的原理。但是每次编译转化会耗费时间,转换次数越多,则效率越低,所以就出现了HotSpot VM技术。HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。 通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
2、 server
当前显示的是server 也有些安装之后显示的是client; client 与server VM 同一个虚拟机的两种结构,服务于同的业务。在安装jdk的文件夹中找到jvm.cfg文件,在图3- jvm.cfg的内容中 我们可以看到server 在client上面(哪个在上,则启动的就是哪个版本,所以图1显示的是server,当然也可以自己修改)
图2: jvm.cfg的路径
图3: jvm.cfg的内容
当项目部署的时候:项目做的server 需要很多线程处理,访问量大的时候,可以设置成server(但是如果不需要那么大的内存的时候,可以修改成client版本)
二、JVM的结构
图4: JVM的基本结构
1、类加载子系统与方法区
类加载子系统(class loader)负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
2、java堆
java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。也是主讲点。
3、直接内存
java的NIO库允许java程序使用直接内存。直接内存(直接在内存上的,大小限制是取决于物理内存的大小)是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx(堆)指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
4、垃圾回收系统
垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。和C/C++不同,java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。
5、java栈
每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着帧信息,java栈中保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。
6、本地方法栈
本地方法栈和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈(操作系统的api 叫做 native method,就是本地方法,不同的操作系统有不同的api)则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)
7、PC(Program Counter)寄存器
PC(Program Counter)寄存器也是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined
8、执行引擎
执行引擎是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
三、java堆
1、JVM的内存分代策略
java虚拟机根据对象存活的周期不同,把堆分为了几块,通常分为新生代、老年代、永久代(对HotSpot 虚拟机而言,永久代逐渐去掉了),这就是分代策略。堆内存是虚拟机管理的内存中最大一块,也是垃圾回收最频繁的区域,程序所有的对象都存储在堆区,给内存分代是为了提高对象内存分配和垃圾回收的效率。
原因:
如果不同周期的对象都混乱申请,垃圾回收机制每次都要遍历所有的对象,这样会极大的降低垃圾回收机制的效率,而且会造成大量的内存碎片,降低内存使用率。
如果分代情况就不同了,新创建对象会在新生代分配内存,经过多次GC回收仍然活下来的对象放在老年代中,静态属性、类信息等放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC;老年代生命周期长,内存回收频率相对较低,不需要频繁进行回收;永久代回收效果太差,JVM一般不对永久代进行回收;还可以根据不同年代的内存采用合适的垃圾回收算法,分代收集给内存带来以上好处。
java虚拟机将堆内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念,它采用了永久代的方式来实现方法区,其他的虚拟机没有此概念,而且HotSpot 也有取消永久代的趋势。jdk 1.7 已经在取代永久代了,将字符常量等都放到方法区中了。内存示意图如下:堆被划分为新生代和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(s0)和ToSpace(s1)组成。tenured中包含永久代(一般不指出来)。
图5: 堆内存代划分
2、每一代作用
新生代:新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor(s0、s1)中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例 (这个比例默认为 8:1:1,由于Eden 一次GC之后能回收掉大部分的对象,所以只剩下小部分需要转移到Survivor,所以Survivor比例占用很小,Survivor区域利用拷贝-复制算法进行回收,所以有两块内存)。新生代中对象一般存活周期较短,进行一次垃圾回收,回收率在75%~95%,回收率很高。
新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden内存不够时,JVM会发起一次minor GC ,当GC开始时,对象只存在于Eden、s0 中,s1 是空的(作保留区域);GC进行时,Eden区中所有存活的对象都会被复制到s1区,而在s0区中仍活的对象会根据他们的年龄决定去向,年龄达到年龄阀值(默以认为15,新生代中的对每,经过1轮拉圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的过会转移到老年代中;没有达到年龄阀值的对象会被复制到 To Survivor区,接看清空Eden和S0区。接着s0和s1交换角色,保证每次GC中S1都是空的。当S1区没有足够的空间存放新生代收集下来的存活对象时,需老年代进行分配担保,将这些对象存放在老年代中。
老年代:用于存放新生代中经过多次垃圾回收仍然存活的对象。在绝大多数情况下,对象首先分配在Eden区,在一次新生代回收之后,如果对象还存活,则进入s0或者s1,每经过一次新生代回收,对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。在老年代中GC频率低,且回收速度慢。
永久代:存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这类区域规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收立圾回收。这已经被取代了,存放的东西放入了代码区。
四、JVM中垃圾回收算法和收集器
1、垃圾回收算法
1)引用计数法(Reference Counting)
许多教科书上判断对象是否存活都是这个算法(C++只能指针就使用的是引用计数),但是在主流的Java虚拟机里没有选用这个算法来管理内存。算法原理其实就是为对象中添加一个引用计数器,每当一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;垃圾回收计数器为0的对象。此算法无法处理循环引用的问题。
2)复制算法(Copying)
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,利用率不高。这种算法其实就是在s0和s1之间使用算法。如图6所示:蓝色为空,粉红色需要被复制的,黄色为没有被使用的空间,灰色为需要被回收的对象。GC前与GC后的布局如下所示。
图6:复制算法示意
3)标记-清除算法(Mark-Sweep)
最基本的收集算法“标记-清除”(Mark-Sweep)算法,之所以说它是最基本的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
算法分为“标记”和“清除”两个阶段:首先标记出所有被引用的对象,然后把未标记的对象清除。此算法暂停整个应用,同时产生内存碎片。它的主要不足有两个:一是效率问题:标记和清除效率都不高;二是空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
图7: 标记-清除算法
4)标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间就要使用额外的空间进行分配担保(Handle Promotion当空间不够时,需要依赖其他内存),以应对被使用的内存中所有对象都100%存活的极端情况。
“标记-整理”算法结合“标记-清除”与“复制”算法,标记过程仍与“标记-清除”算法一样,首先从根结点遍历并标记所有被引用对象,第二阶段,遍历整个堆,清除未标记对象,将存活对象压缩到一块并按照顺序存储。此算法避免了“标记-清除”算法的碎片化问题,也避免了“复制”算法的空间问题。
图8:标记整理算法
2、垃圾收集器
1)Scavenge GC(次收集)
新生代GC( Scavenge GC): Scavenge GC指发生在新生代的GC,因为新生代的GC对象大多生命周期短,所以 Scavenge GC非常频繁,回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发 Scavenge GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor区。然后整理 Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代,因为大分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以s区需要使用快速高效的GC算法。
触发条件: 当年轻代堆空间紧张时会被触发, 相对于全收集而言,收集间隔较短
2)full GC(全收集)
老年代GC( Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC般会伴随至少一次的 Minor GC(老年代的对象大部分是 Minor gc过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比 Minor GC慢10倍以上。当老年代内存不足或者显式调用 System.gc()方法时,会触发Full GC。
触发条件:当老年代或者永久代堆空间满了,会触发全收集操作。可以使用 System.gc()方法来显式的启动全收集全收集,一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。不过,如果全收集时间超过3到5秒钟,那就太长了。
3)、收集器示意图
具体的介绍不在阐述,请参考:https://www.cnblogs.com/chengxuyuanzhilu/p/7088316.html
图9:垃圾收集器
如有错误,欢迎指正!2020-09-25