面试老大难 —— JVM调优到底是个什么东西?

从最熟悉的地方开始

我们先写一个Java类,如图:

public class Java3y {

    private String name;

    private int age;

    public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

然后我们再写一个测试类

public class Java3yTest {

    public static void main(String[] args) {
        
        Java3y java3y=new Java3y();
		java3y.setName("Java3y");
		System.out.println(java3y+"|"+java3y.getName());

    }
}

然后我们不用IDE等开发工具运行,我们使用javac编译,java来运行(使用IDE运行就是上述俩步骤的结合)

执行命令后我们会看到如图

然后我们调用执行即可得到如下内容

我们来捋一下他的逻辑过程:我们通过javac.exe编译器(位于jdk的bin目录下)将.java文件编译为.class文件,然后这些.class文件交给JVM来解析运行(JVM是运行在操作系统之上的根据不同的JDK可以适配所有类型操作系统)从而可以跨平台运行。那么.class文件是如何加载到jvm的呢?我们在下面将会提到。

JVM(Java Virtual Machine)

Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。Java虚拟机不是真实的物理机,它没有寄存器,所以指令集是使用Java栈来存储中间数据,这样做的目的就是为了保持Java虚拟机的指令集尽量的紧凑,同时也便于JAVA虚拟机在那些只有很少通用寄存器的平台上实现。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。Java语言编译程序只需生成能在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

Java虚拟机的组成:(A图含工作逻辑关系但有些组件没写出来,俩图结合起来看)

             

Class Loader(类加载器):类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

Execution Engine(执行引擎) :负责解释命令(它负责执行虚拟机的字节码),提交操作系统执行

Native Interface (本地接口):Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

Runtime Data Area (运行数据区):

  • Method Area(方法区):用于保存加载的类信息、运行时常量信息、静态变量、即时编译器编译后代码。(方法区和堆是线程之间共享的内存区域,而虚拟机栈、本地方法栈、程序计数器则是线程私有的区域,就是说每个线程都有自己的这个区域)
  • 直接内存:java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
  • Java Stack( 栈):每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,java栈中保存着局部变量、方法参数、方法返回地址等
  • Native Method Stack( 本地方法栈):java栈用于方法的调用,而本地方法栈则用于本地方法的调用
  • Java (堆):java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的
  • PC Register( 程序计数器):寄存器也是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined。通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成
  • 垃圾回收系统:垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。和C/C++不同,java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。

JVM是运行在操作系统之上的,他与硬件没有直接的交互。

类的加载过程:

按我们程序来走,我们的Java3yTest.class文件会被AppClassLoader加载器(因为ExtClassLoader和BootStrap加载器都不会加载它[双亲委派模型])加载到JVM中。随后发现了要使用Java3y这个类,我们的Java3y.class文件会被AppClassLoader加载器(因为ExtClassLoader和BootStrap加载器都不会加载它[双亲委派模型])加载到JVM中。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存

总结:

  • 1、通过java.exe运行Java3yTest.class,随后被加载到JVM中,元空间存储着类的信息(包括类的名称、方法信息、字段信息..)。
  • 2、然后JVM找到Java3yTest的主函数入口(main),为main函数创建栈帧,开始执行main函数
  • 3、main函数的第一条命令是Java3y java3y = new Java3y();就是让JVM创建一个Java3y对象,但是这时候方法区中没有Java3y类的信息,所以JVM马上加载Java3y类,把Java3y类的类型信息放到方法区中(元空间)
  • 4、加载完Java3y类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Java3y实例分配内存, 然后调用构造函数初始化Java3y实例,这个Java3y实例持有着指向方法区的Java3y类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用
  • 5、当使用java3y.setName("Java3y");的时候,JVM根据java3y引用找到Java3y对象,然后根据Java3y对象持有的引用定位到方法区中Java3y类的类型信息的方法表,获得setName()函数的字节码的地址
  • 6、为setName()函数创建栈帧,开始运行setName()函数

引用:JAVA面试经常会被问题 JVM调优? - 知乎

栈中主要保存3类数据

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
  • 栈操作(Operand Stack):记录出栈、入栈的操作;
  • 栈帧数据(Frame Data):包括类文件、方法等。

运行逻辑:

栈帧:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。

举个例子:当一个方法A被调用时就产生一个栈帧2,并被压入到栈中,A方法调用了B方法,于是产生栈帧1也被压入到栈,栈帧1处于栈顶的位置,栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,线程结束,栈释放。

堆的划分:根据对象存活的周期不同,把堆内存划分为几块,一般分为新生区、养老区和永久存储区。(以下几张图结合起来看)

     

在JDK1.8版本及以后版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -Xmn堆中新生代初始及最大大小(NewSize和MaxNewSize为其细化)。
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:PermSize设置永久代最小空间大小。
  • -XX:MaxPermSize设置永久代最大空间大小。
  • -Xss设置每个线程的堆栈大小。

 

Minor GC:只针对新生代区域的GC,这种GC算法采用的是复制算法(Copying)

Full GC(又叫Major GC):会对整个堆进行整理,包括Young、Tenured和Perm。因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。这种GC算法采用的是标记-整理算法

新生区:是类的诞生、成长、消亡的区域。新生区又分为伊甸(dian)区(Eden Space)和幸存者区(Survivor Space)。幸存区又分为0区和1区(由FromSpace和ToSpace组成),Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。所有的类都是再伊甸区被new出来。当伊甸区的空间用完时,程序需要新创建对象,则JVM垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将其中不再被其他对象引用的对象进行销毁,然后将剩余对象移动到幸存0区,若幸存0区也满了,再对幸存区进行垃圾回收,剩余对象移动到1区,若幸存1区也满了,再移动到养老区,若养老区也满了,JVM垃圾回收器对养老区进行垃圾回收(Full GC),若进行垃圾回收(Full GC)后依然不够创建新对象,则抛出OOM异常。即:java.lang.OutOfMemoryError:Java heap space(总结:对象创建完成之后会先试图放入新生代;如果新生代经过回收之后也放不开,则直接试图将该对象放入老生代。老生代如果也放不开,则会出现错误 — OutOfMemoryError。)

养老区:用于保存从新生区筛选出来的JAVA对象

永久区:用于存放JDK滋生所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

GC回收算法:

GC垃圾回收主要应用范围:

JVM中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生随线程而灭。栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。它们的内存分配和回收都具有确定性。因此,GC垃圾回收主要集中在堆和方法区(元空间)(堆占用内存空间最大),在程序运行期间,这部分内存的分配和使用都是动态的。现代收集器基本都采用分代收集算法

1.首先,JVM回收的是垃圾,垃圾就是我们程序中已经是不需要的了。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”

判断方法:

  • 引用计数法-->这种难以解决对象之间的循环引用的问题
  • 可达性分析算法-->主流的JVM采用的是这种方式

引用计数法

通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。

当我们编写以下代码时,abc这个字符串对象的引用计数值为1.

String p = new String("abc")

而当我们去除abc字符串对象的引用时,则abc字符串对象的引用计数减1

p = null

可达性分析算法

通过一系列被称为「GCRoots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GCRoots没有任何引用链相连,就说明该对象不可达,即可以被回收。

什么是对象可达?

public class MyObject {
	private String objectName;//对象名
	private MyObject refrence;//依赖对象

	public MyObject(String objectName) {
		this.objectName = objectName;
	}

	public MyObject(String objectName, MyObject refrence) {
		this.objectName = objectName;
		this.refrence = refrence;
	}

	public static void main(String[] args) {
		MyObject a = new MyObject("a");
		MyObject b = new MyObject("b");
		MyObject c = new MyObject("c");
		a.refrence = b;
		b.refrence = c;

		new MyObject("d", new MyObject("e"));
	}
}

假设a是GC Roots的话,那么b、c就是可达的,d、e是不可达的。

什么是GCRoots?GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象,可以作为GC Roots的对象可以分为两大类:全局对象和执行上下文。

现在已经可以判断哪些对象已经“死去”了,我们现在要对这些“死去”的对象进行回收,回收也有好几种算法:

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代收集算法

引用:面试官,不要再问我“Java GC垃圾回收机制”了 - 程序新视界

标记清除算法

标记清除(Mark-Sweep)算法,包含“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

 

主要缺点:一个是效率问题,标记和清除过程的效率都不高;另外是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制(Copying)算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,就将还存活着的对象复制到另外一块上,然后清理掉前一块。

缺点:将内存缩小为一半,性价比低,持续复制长生存期的对象则导致效率低下。

JVM堆中新生代便采用复制算法。

在GC回收过程中,当Eden区满时,还存活的对象会被复制到其中一个Survivor区;当回收时,会将Eden和使用的Survivor区还存活的对象,复制到另外一个Survivor区,然后对Eden和用过的Survivor区进行清理。

如果另外一个Survivor区没有足够的内存存储时,则会进入老年代。

这里针对哪些对象会进入老年代有这样的机制:对象每经历一次复制,年龄加1,达到晋升年龄阈值后,转移到老年代。

标记整理算法

标记整理(Mark-Compact)算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

这种算法不既不用浪费50%的内存,也解决了复制算法在对象存活率较高时的效率低下问题。

分代收集算法

分代收集算法,基本思路:将Java的堆内存逻辑上分成两块,新生代和老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略。而在新生代中大多数对象都是瞬间对象,只有少量对象存活,复制较少对象即可完成清理,因此采用复制算法。而针对老年代中的对象,存活率较高,又没有额外的担保内存,因此采用标记整理算法。

JVM调优

JVM调优核心为调整年轻代、年老大的内存空间大小及使用GC发生器的类型等,常见虚拟机监控工具分为命令行工具和可视化工具,其中命令行工具(位置在jdk的bin目录下)主要有jps、jstat、jinfo、jmap、jhat、jstack;可视化工具有JConsole和VisualVM

1.jps

虚拟机进程状况查询工具,可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称(main函数对应的类)和进程的本地虚拟机唯一ID(简称LVMID)

初始状态查询:jps -l(还有-q、-v、-m,暂时不展开介绍了)

然后我启动IDEA运行一个项目后,再次查询,可以看到主类为TestApplication且其对应ID为12916(主要看这个信息即可) 

 

2.jstat

用于监控虚拟机各种运行状态信息的命令行工具,可以显示本地或远程进程中的类装载、内存、垃圾收集、JIT编译等运行数据

每250ms查询一次进程12916(这个id是通过jps查询得到的)的垃圾回收状况,一共查询5次

  • S0C:第一个幸存区的大小(单位:KB,下同)
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • EC:伊甸园区的大小
  • EU:伊甸园区的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

查询各分区使用情况

  • S0 代表Survivor0区使用空间百分比
  • S1 代表Survivor1区使用空间百分比
  • E (Eden)代表 Eden 区使用空间百分比
  • O(Old)代表老年代使用空间百分比
  • P(Permanent)代表永久代使用空间百分比
  • YGC(Young GC)代表Minor GC次数
  • YGCT(Young GC Time)代表Minor GC耗时
  • FGC(Full GC)代表Full GC次数
  • GCT(GC Time)代表Minor和Full GC共计耗时

3.jinfo

查看和调整虚拟机各项参数

4.jmap 

用于生成堆转储快照(这里注意一下权限问题,我在C盘下失败了)

5.jhat

用于分析jmap生成的快照,当提示Server is Ready后,在浏览器输入localhost:7000即可查看

查看内存中对象的容量 

使用类似sql语法对内存中的对象进行查询统计 

 

6.jstack

生成虚拟机当前时刻的线程快照,线程快照是当前虚拟机每一条线程正在执行的方法堆栈的集合,生成快照可以定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等

如图,可以看到线程状态时Runnable,如果状态是waiting,则说明发生了停顿

7.visualvm

使用java自带工具jvisualvm.exe,在cmd下运行jconsole即可打开

安装Visual GC插件,选择工具-》插件-》可用插件,然后点击安装

然后我们重启VisualVM,可以看到多了Visual GC的选项,这样我们就可以看到进程的堆的分配情况了

8.jconsole

使用java自带工具jconsole.exe,在cmd下运行jconsole即可打开

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值