JVM详解

一、串讲

一个程序的运行离不开JVM,所以我们从JVM的启动开始讲起,JVM是如何启动的呢,首先自然是装载配置(根据当前路径和系统版本找到JVM.cfg),然后根据配置文件寻找JVM.dll(jvm.dll是动态资源链接库,说白了就是JVM的主要实现),接着就是根据dll初始化JVM获得JNIEvement接口(findclass等操作就是通过这个接口实现的,要实现混合编程也少不了这个接口,什么是混合编程,实际开发过程中单一的Java语言可能满足不了我们的需求,这个时候我们可以使用其他的编程语言,只要其他编程语言的编译结果是一个有效的字节码文件,JVM就能有效的支持这种语言,使得Java能够与其他语言进行混合编程),最后找到main方法并运行,我们的JVM就启动了。JVM启动后我们就接着讲讲一个程序是如何通过jvm运行的整个流程,程序是源码,所以首先我们要将源码通过前端编译器javac编译成字节码文件(这个过程包括词法分析语法分析语义分析最后编译成class文件),然后jvm通过类加载器将字节码文件从本地或者网络传输加载到内存(加载的过程为:加载,验证,准备,解析,初始化,最终形成jvm直接使用的Java类型。同时加载器类型分又为启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器。它们自上往下查找类是否被加载,自下往上加载类,根据双亲委派模型顶层类加载器不能加载底层类加载器的对象),加载到内存后字节码的通过JVM的JIT(即时编译器,分为两种,client compare和server compare 简称C1和C2)来将字节码编译成机器指令最后由jvm执行引擎来执行。说到我们的class文件被加载到内存,我们就不得不说一下JVM的内存空间了,jvm的内存空间分为方法区、堆、栈、本地方法栈(方法区保存装载的类信息,常量池,静态数据等,同时被所有线程共享。堆保存类对象,线程共享。栈存放方法的局部变量,基本操作数栈,常量池指针,线程私有),而堆进一步细分的话,又分为新生代(Eden空间,幸存空间(From Survivor空间和To Survivor空间))和老年代,而且堆是gc的主要工作空间。所以接下来我们讲讲这个GC了,GC是用来垃圾收集管理内存的,它如何对垃圾进行收集呢,gc内部提供四种垃圾回收的算法(引用计数法、标记清除法、标记压缩法、复制法)。最后我们谈谈如何优化jvm,最牛逼的就是根据项目的情况重写一个jvm啦,例如taobaoVM,或者使用命令行(jps、jstat、jmap、jhat、jstack、jinfo等)查看内存各个区域的占用情况设置参数(-Xmm、-Xms、-Xmn、newRatio等)来优化jvm。
来源本作者

二、类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的Java.lang.Class对象,用来封装类在方法区类的对象。
JVM详解
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
JVM详解
图片来源

三、类加载器

  • 类加载器分类

JVM详解

启动类加载器

由C++语言实现,是虚拟机的一部分。负责将JAVA_HOME/lib目录中,或者被-Xbootclasspath参数指定的路径,但是文件名要能被虚拟机识别,名字不符合无法被启动类加载器加载。启动类加载器无法被Java程序直接引用。
扩展类加载器

由Java语言实现,负责加载JAVA_HOME/lib/ext目录,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器

由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,一般情况下这个就是程序中默认的类加载器。

自定义类加载器
由用户自己实现。

如果不想打破双亲委派模型,那么只需要重写findClass方法即可。
否则就重写整个loadClass方法。
  • 双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。父子不会以继承的关系类实现,而是都是使用组合关系来服用父加载器的代码。
在java.lang.ClassLoader的loadClass()方法中实现。
工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成(它的搜索范围中没有找到所需要的类)时才尝试自己加载
好处
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,从而使得基础类库得到同意。

JVM内存区域

由上图知JVM包含堆、元空间、Java虚拟机栈、本地方法栈、程序计数器等内存区域。其中,堆是占用内存最大
的一块。我们平常的-Xmx、-Xms等参数,就是针对于堆进行设计的。

  • 堆:JVM堆中的数据,是共享的,是占用内存最大的一块区域,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
  • 虚拟机栈:Java虚拟机栈,是基于线程的,私有的,用来服务字节码指令的运行,每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
  • 程序计数器:当前线程所执行的字节码的行号指示器
  • 本地方法栈: 区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 *Error 和 OutOfMemoryError 异常。
  • 方法区: 属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

垃圾回收算法

  • 标记-清除算法

JVM详解
标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。但它存在一个很大的问题,那就是内存碎片。
上图中等方块的假设是2M,小一些的是1M,大一些的是4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个2M的内存区域,其中有2个1M是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。

  • 复制算法

JVM详解
复制算法(Copying)是在标记清除算法基础上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况。

  • 标记-整理算法

JVM详解
标记-整理算法标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

标记整理算法解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。标记整理算法对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

如何判断对象可以被回收

  • 可达性分析法

在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系
列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当
一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的,
JVM详解
图片来源

注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至
少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对
在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).

性能调优

  • 调优命令

1、jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
2、jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进
程中的类装载、内存、垃圾收集、JIT编译等运行数据。
3、jmap,JVM Memory Map命令用于生成heap dump文件
4、jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一
个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
5、jstack,用于生成java虚拟机当前时刻的线程快照。
6、jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

  • JVM性能调优

设定堆内存大小
-Xmx:堆内存最大限制。
设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC

定位问题实例

1、top命令-找到CPU占比最高的进程
2、ps -ef或者jps定位
3、将需要的线程id转换为16进制格式
4、找出具体的问题代码

jstack 进程id | grep tid(16进制线程id小写英文)-A60
上一篇:Hi拼团,个人购买阿里云服务器首选优惠活动


下一篇:人工智能有能力部署到的几个应用