10、JVM知识点

226 阅读10分钟

java内存区域划分

image.png

image.png

1. 程序计数器

  • 当前线程所执行的字节码行号指示器
  • 每个线程都有独立的程序计数器
  • 如果执行的是java方法,记录字节码指令地址。如果执行Native方法,则为空(Undefined)

2. 虚拟机栈

  • 线程私有,生命周期与线程相同
  • 描述java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量,方法出口,操作数栈等信息。每个方法调用对应一个栈帧在虚拟机栈中入栈到出栈的过程
  • 存放基本数据类型(8种)和对象引用类型(地址的指针或者对象的句柄)
  • 请求栈深度大于虚拟机允许深度时,抛出StackOverflowError异常
  • 如果动态扩展仍无法申请足够的内存,抛出OutOfMemoryError异常

3.本地方法栈

  • 作用和虚拟机栈一样
  • 区别为:本地方法栈服务虚拟机使用到的Native方法

4. 堆

  • 虚拟机管理的内存最大的一块
  • 被所有线程共享的区域
  • 所有对象的实例在此分片内存
  • 可细分为多个代

5.方法区

  • 所有线程共享的区域
  • 存储类信息,常量,静态变量
  • 在HotSpot虚拟机上也称永久代
  • 垃圾收集行为很少出现在这个区域,因为可回收的内存很少

6.运行时常量池

  • 是方法区的一部分
  • 用于存放编译器生成的各种字面量和符号引用

7.直接内存

  • 不包括在JVM内存区域中,不受JVM参数影响
  • JVM使用缓冲区时,会在该区区域分配内存
  • 配置时注意给该区域预留空间,而不是把所有内存都分给JVM
  • -XX:MaxDirectMemorySize指定,不指定默认与堆最大值一样(-Xmx)

如何计算对象已死

1.1 引用计数器算法
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。

引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。

1.2 可达性分析算法
可达性分析算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径称为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。

GC Roots对象包括:

虚拟机栈(栈帧中的本地变量表)中的引用的对象。

方法区域中类的静态属性引用的对象。

方法区域中常量引用的对象。

本地方法栈中JNI(Native方法)的引用的对象。

上面只是标记了对象是否可以被回收,实际上在java中首先会标记下对象,会调用对象里面的protected void finalize()这个方法,这个时候对象还有救,只要在这个方法把该对象和引用链对接上,其实可以逃脱被回收

回收的区域
新生代
永久代
新生代,这个很容易理解,一般来说永久带是最不好回收的。永久代主要回收以下部分内容:

废弃常量
无用的类

垃圾回收算法

标记—清除算法
标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。

标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。

复制算法
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。这种方式听上去确实是非常不错的方案,但是总的来说对内存的消耗十分高。

复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1),也就是常提到的一块Eden(80%)和两块Survivor(20%)。当然也会存在10%不够用的情况,这个后面在进行梳理,会有一个补偿机制,也就是分配担保。

标记—整理算法

复制收集算法会存在一种极端情况,就是对象都没死。这种情况会在老年代有几率的出现,所以根据老年代的特点提出了标记—整理算法。 标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,如下图所示:

image.png

分代收集
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用标记—复制算法,老年代采用标记—整理算法。

垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。

Jdk和Jre和JVM的区别

  • Jdk包括了Jre和Jvm,Jre包括了Jvm

  • Jdk是我们编写代码使用的开发工具包

  • Jre 是Java的运行时环境,他大部分都是C和C++语言编写的,他是我们在编译java时所需要的基础的类库

  • Jvm俗称Java虚拟机,他是java运行环境的一部分,它虚构出来的一台计算机,在通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序

JVM问题排查步骤

1、通过top命令找到消耗cpu高的进程id号pid

2、根据pid找到消耗cpu资源比较高的线程id

3、对当前的线程做jstack,输出前进程的所有堆栈信息

4、将第2步中得到的线程id转换成16进制进得到结果

5、根据相应的线程id在堆栈信息中找到相关的内容

6、解读对应的堆栈信息,定位代码位置并排查问题原因

  1. 通过jps命令查看Java进程「基础」信息(进程号、主类)。这个命令很常用的就是用来看当前服务器有多少Java进程在运行,它们的进程号和加载主类是啥

  2. 通过jstat命令查看Java进程「统计类」相关的信息(类加载、编译相关信息统计,各个内存区域GC概况和统计)。这个命令很常用于看GC的情况

  3. 通过jinfo命令来查看和调整Java进程的「运行参数」。

  4. 通过jmap命令来查看Java进程的「内存信息」。这个命令很常用于把JVM内存信息dump到文件,然后再用MAT( Memory Analyzer tool 内存解析工具)把文件进行分析

  5. 通过jstack命令来查看JVM「线程信息」。这个命令用常用语排查死锁相关的问题

  6. 还有近期比较热门的Arthas(阿里开源的诊断工具),涵盖了上面很多命令的功能且自带图形化界面。这也是我这边常用的排查和分析工具

JVM调优

一般调优JVM我们认为会有几种指标可以参考:『吞吐量』、『停顿时间』和『垃圾回收频率』

基于这些指标,我们就有可能需要调整:

  1. 内存区域大小以及相关策略(比如整块堆内存占多少、新生代占多少、老年代占多少、Survivor占多少、晋升老年代的条件等等)

比如(-Xmx:设置堆的最大值、-Xms:设置堆的初始值、-Xmn:表示年轻代的大小、-XX:SurvivorRatio:伊甸区和幸存区的比例等等)

(按经验来说:IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些)

  1. 垃圾回收器(选择合适的垃圾回收器,以及各个垃圾回收器的各种调优参数)

比如(-XX:+UseG1GC:指定 JVM 使用的垃圾回收器为 G1、-XX:MaxGCPauseMillis:设置目标停顿时间、-XX:InitiatingHeapOccupancyPercent:当整个堆内存使用达到一定比例,全局并发标记阶段 就会被启动等等)

类加载的执行过程

  • 加载:根据查找路径找到相应的class文件然后导入;

  • 验证:检查加载的class文件的正确性;

  • 准备:给类中的静态变量分配内存空间;

  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;

  • 初始化:对静态变量和静态代码块执行初始化工作。

四种类加载器

1、启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。

2、扩展类加载器(extensions class loader):它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

3、系统类加载器(system class loader):它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。

4、用户自定义类加载器,通过继承java.lang.ClassLoader类的方式实现。

双亲委派模型:
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。