JAVA虚拟机(JVM)底层原理

目录

1.0.什么是JVM

2.0 JVM的组成

2.1类加载器子系统

 2.2运行时数据区

2.3.程序计数器

2.4.Java虚拟机栈

2.5.本地方法栈

2.6.方法区

2.7.堆内存

2.8元空间

2.9.执行引擎

3.0 JVM内存溢出

3.1.1堆内存溢出

3.2.2 虚拟机栈/本地方法栈溢出

3.3. 方法区溢出-元空间

  3.4.本机直接内存溢出

4.0 JVM垃圾回收 

4.1 判断对象是否已死

4.2 常用垃圾回收算法

4.2.常见垃圾收集器

5.0 JVM的优化

5.1JIT编译器优化

5.2JVM内存分区优化

5.3.jvisualvm分析内存快照

5.4.内存溢出问题定位

5.5常用JVM参数参考

5.6.设置jvm参数的几种方式



1.0.什么是JVM

与JVM的初次见面,是在我们Java开始的时候就已经相见了.时隔多日,我们先来回顾一下.

Java的广告语是,”编写一次,到处运行,而它凭借的就是JVM(Java Virtual Machine).而对于不同的平台,Windows,Linux,Mac OS等,有具体不同的JVM版本.这些JVM屏蔽了平台的不同,提供了统一的运行环境,让Java代码无需考虑平台的差异,运行在不同的环境中.

而至于JRE和JDK,就不再赘述了,包含关系应该很清楚的,而今天我们的重点就在于对JVM的进一步认识以及对它进行优化调整.

1.1为什么要优化JVM

正如前面我们所回顾的,我们的Java代码都是运行在JVM中的,而部署的硬件及应用场景有所不同时,仍然采用默认的配置不见得能起到最好的效果,甚至可能会导致运行效率更差,又或者面临高并发情况下,想让程序平稳顺畅的运行,所以我们需要针对实际的需要来进行优化.

所谓优化就是配置一些参数,让jvm运行时使用这些参数,让jvm运行的程序更优。

1.2分析工具jvisualvm

我们只知道有JVM的存在,但它的运行对于我们来说感觉像是摸不着看不见的,所以我们需要借助工具来监控它的一个实时状态,就像Windows的性能监视器一样,JDK也有自己的可视化工具.Java提供了2个监视工具:

  1. D:\opensource\jdk1.8\bin\jconsole.exe
  2. D:\opensource\jdk1.8\bin\jvisualvm.exe

我们以管理员身份运行DOS ,输入jvisualvm,将Java VisualVM启动,输入jvisualvm,将Java VisualVM启动

 

 在这里我们可以看到

本地列表中有多个条目,而一眼也可以看到我们SpringBoot项目的main方法,直接双击,经过短时间的加载后,得到这样一个界面

这个是概述页面,可以得到很多信息,但对于我们分析JVM的运行还是没有什么帮助,所以我们切换到监视页

 监视页展示的就是实时的JVM信息

应该还是很直观的 , 现在安装插件,插件的安装属于VisualVM的一个重要功能,凭借插件我们可以将这个工具的功能变得更强大。

打开工具 -> 插件 -> 选择“可用插件”页 : 我们在这里安装一个Visual GC,方便我们看到内存回收以及各个分代的情况 . 打上勾之后点击安装,就是常规的next以及同意协议等 ,网络不是很稳定,有时候可能需要多尝试几次。可以在设置中修改插件中心地址

根据如下步骤修改地址:找到插件中心

VisualVM: Plugins Centers

 

找到对应的JDK版本:

Update Center documentation

复制插件地址:

安装插件:

然后再 可用插件中 找到 Visual GC

安装完成后我们将当前监控页关掉,再次打开,就可以看到Profiler后面多了一个Visual GC页。

 在这里我们可以看到JIT活动时间,类加载活动时间,GC活动时间以及各个分代的情况。

需要注意的是,当前课件使用的JDK版本为1.8,仍然自带了VisualVM,从1.9开始的版本是没有自带的,需要额外下载,下载的github地址:

VisualVM: Download

另外,如果开发工具使用的是Intellij IDEA的话,可以下载一个插件,VisualVM Launcher,通过插件启动可以直接到上述页面,不用在左边的条目中寻找自己的项目.

当然也有其他的工具,但这个在可预见的未来都会是主力发展的多合一故障处理工具.所以我们后面将会使用这个工具来分析我们的JVM运行情况,进而优化.而需要优化我们还需要对JVM的组成有进一步的了解.接下来我们来看一下JVM的组成

2.0 JVM的组成

从图上可以看到,大致分为以下组件:

  • 类加载器子系统
  • 运行时数据区 : 方法区 堆  虚拟机栈 本地方法栈 程序计数器
  • 执行引擎
  • 本地方法库

而本地库接口也就是用于调用本地方法的接口,在此我们不细说,主要关注的是上述的4个组件

2.1类加载器子系统

顾名思义,这是用于类加载的一个子系统.

2.1.1类加载的过程

类加载的过程包括了加载,验证,准备,解析和初始化这5个步骤

  • 加载:找到字节码文件,读取到内存中.类的加载方式分为隐式加载和显示加载两种隐式加载指的是程序在使用new关键词创建对象时,会隐式的调用类的加载器把对应的类加载到jvm中。显示加载指的是通过直接调用class.forName()方法来把所需的类加载到jvm中。
  • 验证:验证此字节码文件是不是真的是一个字节码文件,毕竟后缀名可以随便改,而内在的身份标识是不会变的.在确认是一个字节码文件后,还会检查一系列的是否可运行验证,元数据验证,字节码验证,符号引用验证等.Java虚拟机规范对此要求很严格,在Java 7的规范中,已经有130页的描述验证过程的内容.
  • 准备:为类中static修饰的变量分配内存空间并设置其初始值为0或null.可能会有人感觉奇怪,在类中定义一个static修饰的int,并赋值了123,为什么这里还是赋值0.因为这个int的123是在初始化阶段的时候才赋值的,这里只是先把内存分配好.但如果你的static修饰还加上了final,那么就会在准备阶段就会赋值.
  • 解析:解析阶段会将java代码中的符号引用替换为直接引用.比如引用的是一个类,我们在代码中只有全限定名来标识它,在这个阶段会找到这个类加载到内存中的地址.
  • 初始化:如刚才准备阶段所说的,这个阶段就是对变量的赋值的阶段.

如上过程都是在JVM执行的过程中自己完成的,我们无需干涉。

2.2.2类与类加载器

每一个类,都需要和它的类加载器一起确定其在JVM中的唯一性.换句话来说,不同类加载器加载的同一个字节码文件,得到的类都不相等.我们可以通过默认加载器去加载一个类,然后new一个对象,再通过自己定义的一个类加载器,去加载同一个字节码文件,拿前面得到的对象去instanceof,会得到的结果是false.

2.2.3双亲委派机制

JVM中内置的类加载器:

 类加载时使用了双亲委派模式:

载规则,优先使用爷爷加载,如果没有加载到再使用它爹加载,如果他爹也没有加载到,才到自己加载,如果自己也没有加载到才报ClassNotFountException。在这过程中只要上一级加载到了,下一级就不会加载了,这麽做的目的:

  1. 不让我们轻易覆盖系统提供功能
  2. 也要让我们扩展我们功能。

类加载器一般有4种,其中前3种是必然存在的

  1. 启动类加载器:加载<JAVA_HOME>\lib下的
  2. 扩展类加载器:加载<JAVA_HOME>\lib\ext下的
  3. 应用程序类加载器:加载Classpath下的
  4. 自定义类加载器

而双亲委派机制是如何运作的呢?

我们以应用程序类加载器举例,它在需要加载一个类的时候,不会直接去尝试加载,而是委托上级的扩展类加载器去加载,而扩展类加载器也是委托启动类加载器去加载.

启动类加载器在自己的搜索范围内没有找到这么一个类,表示自己无法加载,就再让扩展类加载器去加载,同样的,扩展类加载器在自己的搜索范围内找一遍,如果还是没有找到,就委托应用程序类加载器去加载.如果最终还是没找到,那就会直接抛出异常了.

而为什么要这么麻烦的从下到上,再从上到下呢?

这是为了安全着想,保证按照优先级加载.如果用户自己编写一个名为java.lang.Object的类,放到自己的Classpath中,没有这种优先级保证,应用程序类加载器就把这个当做Object加载到了内存中,从而会引发一片混乱.而凭借这种双亲委派机制,先一路向上委托,启动类加载器去找的时候,就把正确的Object加载到了内存中,后面再加载自行编写的Object的时候,是不会加载运行的.

结论:JDK自带的类是没法覆盖的,而引入的三方的JAR是可以自己定义相同的类来覆盖的。

案例测试:分别自定义类来覆盖JDK自带的String 类 , 和导入一个三方jar lang3包中的StringUtils 类,看是否能覆盖。

覆盖String

覆盖StringUtils :

 2.2运行时数据区

 

JDK1.8以后,方法区被元空间替代,没有方法区了,元空间直接使用本地内存

2.3.程序计数器

程序计数器是线程私有的,虽然名字叫计数器,但主要用途还是用来确定指令的执行顺序,比如循环,分支,跳转,异常捕获等.而JVM对于多线程的实现是通过轮流切换线程实现的,所以为了保证每个线程都能按正确顺序执行,将程序计数器作为线程私有.程序计数器是唯一一个JVM没有规定任何OOM的区块.(out of memory)

程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间,此外,程序计数器是Java虚拟机规定的唯一不会发生内存溢出的区域

2.4.Java虚拟机栈

Java虚拟机栈也是线程私有的,每个方法执行都会创建一个栈帧,局部变量就存放在栈帧中,还有一些其他的动态链接之类的.通常有两个错误会跟这个有关系,一个是StackOverFlowError,一个是OOM(OutOfMemoryError).前者是因为线程请求栈深度超出虚拟机所允许的范围,后者是动态扩展栈的大小的时候,申请不到足够的内存空间.而前者提到的栈深度,也就是刚才说到的每个方法会创建一个栈帧,栈帧从开始执行方法时压入Java虚拟机栈,执行完的时候弹出栈.当压入的栈帧太多了,就会报出这个StackOverflowError.

虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应Java代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个Java方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。

  1. 栈帧(方法执行形成栈帧):栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,线程私有。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程,栈帧随着方法调用而创建,随着方法结束而销毁
  2. 局部变量表(储存方法参数和局部变量):局部变量表(Local Variable Tabl
  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值