java学习总结——图文并茂讲述jvm内存原理和调优

一、前言

最近无意间看了个腾讯课堂的java-jvm调优视频,觉得学的深有感触,所以做相关学习知识总结。

二、了解java

我们都知道java具有一处编译处处执行的特点。如下所示
在这里插入图片描述
编写的java文件,采取javac编译后,我可以在Windows端运行,也可以在linux环境下运行。就好比我们平时开发web需要在Windows下测试运行,部署服务器则在centos上运行一样。

三、了解jvm

jvm作为java代码字节码文件的解析操作。其包含以下几个区域。
在这里插入图片描述
jvm java虚拟机中的运行时内存区中有很多的组成,其中包含很多专业名词,需要了解这些名词含义的可以参考博客《Java虚拟机(JVM)你只要看这一篇就够了!

3.1、栈

1、保存局部变量名称。
2、线程栈/虚拟机栈。
3、每个线程独有。
4、FILO(first in last out) 先进的元素后出

3.1.1、如何理解每个线程独有栈空间?

两个线程,程序在运行时,会分别给两个线程分配各自独立的栈内存空间。

  • 写个小小的javademo
public class TestDemo {
	public static void main(String[] args) {
		TestDemo td = new TestDemo();
		td.xiangjiao();
	}
	
	public int xiangjiao(){
		int a = 0;
		int b = 1;
		return a+b;
	}
}

当程序运行时,main作为主线程,jvm会给其分配一个栈内存区域。
当有其他线程时,会给其分配一个其他的栈内存区域。
每个线程单独分配栈内存,管理各自的局部变量信息。
在这里插入图片描述

3.1.2、栈的内部数据结构有哪些?

  • 1、栈帧

每个方法的局部变量只在本方法内有效。
main方法:局部变量 td
xiangjiao方法:局部变量 a b

何为栈帧?

jvm为了区分每个方法内局部变量的作用域范围的内存区域,每个方法在运行的时候,都会给这个方法分配一块独立的栈帧内存区域。

在这里插入图片描述
何为先进后出?
在这里插入图片描述
栈帧内部除了放置局部变量外,还存在什么?

3.2、程序计数器

每个线程独有。
存储当前线程正在运行或马上要运行的jvm指令码对应的行号。

3.2.1、什么是jvm指令码对应的行号?

我们使用如下指令,反编译机器码为jvm指令码。

javap -c TestDemo.clss > TestDemo.txt
在这里插入图片描述
此时的4则表示我即将要运行jvm指令的行号。

3.2.2、为什么说每个线程独有计数器?

在给线程分配各自的内存空间时,会给每个线程都分配一个程序计数器
在这里插入图片描述

3.2.3、为什么需要给每个线程栈分配计数器?

《java并发编程的艺术》中针对上下位切换有这么一个说法:

在这里插入图片描述

所以jvm给线程分配程序计数器的根本原因在于,cpu分片处理各种线程操作的数据,当切换别的线程后再切回来,如果没有程序计数器,则会导致cpu无法知道这个线程执行到了哪里和需要从哪开始继续执行。

3.3、方法区

JVM 方法区

1.8之前称为 永久代持久代
1.8开始称为元空间。
主要保存常量(包含常量池)、静态变量、类元信息。

例如下列代码:

class User{
	
}
public class TestDemo {
	public static int a = 666;
	public static User user = new User();
}

int a 在内存中保存数据;
由于new User()是对象,所以会先在堆中创建对象,在方法区中创建变量user保存堆内存地址信息。
在这里插入图片描述
之前也说到方法区中也存有类元信息,所以堆也会指向方法区。
在这里插入图片描述

3.3.1、为什么会存在堆指向方法区呢?

类元信息:类名、修饰符、类中方法、指令码等。
类的加载,由类加载器加载 xxx.class文件于方法区中。
实例化对象在堆中,但实例化的那个对象类元信息却在方法区中
在这里插入图片描述
这里的方法区中为什么存在一个user变量指向堆内存中的实例化对象,原因是此时的user变量名称是一个static修饰的!

3.3.2、方法区在jdk1.8前后变更了什么?

Java学习笔记9—类静态成员变量的存储位置及JVM的内存划分

区分 jdk1.8前 jdk1.8后
组成 永久代实现,主要存放类的信息、常量池、方法数据、方法代码等 取消了永久代,提出了元空间
位置 常量池、静态成员变量等迁移到了堆中;元空间不在虚拟机内存中,而是放在本地内存中

图解:
在这里插入图片描述

3.4、本地方法栈

java中native关键字修饰的方法。
每个线程专属。(线程栈)
在这里插入图片描述

3.5、堆

存放实例化对象信息。
是垃圾管理器的主要工作区域。

其中,堆内存空间可以细分为以下几种:
在这里插入图片描述

每次产生实例化对象操作(new User())都会在Eden区中产生。

3.5.1、当Eden区内存都占用了会怎么样?

会由字节码执行引擎单独开启一个线程做minor GC操作。

3.5.2、什么是垃圾?

无任何指针指向的对象实例。

3.5.3、GC操作是如何从堆中找到需要搜集的垃圾的?

在这里插入图片描述

将GC Roots 根 作为起点,从这些节点 开始向下搜索引用对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
1、如在栈中找到可以作为GC Roots 根 的变量(栈中的本地变量、方法区中的静态变量)。
2、再通过找到的GC Roots 根 (如:变量)的指针,找到对应堆中的对象。
3、判断对象是否存在其他引用,如果存在引用其他对象,则继续搜索。
4、直到找到对象不再引用任何其他对象。
5、上述在GC Roots 根 的引用链条上的都是非垃圾对象(栈和堆中存在指针关联),不在链条上的就是垃圾对象(栈和堆中不存在指针关联)。
在这里插入图片描述

3.5.4、为什么说不在链条上的就是垃圾对象?

一段代码执行完成后,会销毁栈指向堆的指针,此时的堆中的实例化对象就不会存在GC Roots 根 对应的引用链上。
在这里插入图片描述

3.5.5、什么是GC Roots 根

GC Roots 根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。


Minor GC操作执行完毕后,被判定为非垃圾的对象,会保存至From Survivor空间中,同时对这个对象(对象头内存区)信息中增加一个分代年龄(经历过一次GC操作,则会将分代年龄 +1 操作)。
在这里插入图片描述
当再经历一次GC操作后,此对象依旧为非垃圾对象,则会继续将对象由From转移至To内存区中。
在这里插入图片描述
如果GC又执行一次了,此时的对象依旧存在(为非垃圾对象),则会由To转移至From中并将分代年龄+1操作,以此类推。。。
在这里插入图片描述

3.5.6、在From至To之间,一个非垃圾对象会无限来回(复制算法)吗?

不会。
两者复制算法为15次,即一个对象的分代年龄达到15,则会由新生代转移至老年代

非要分代年龄达到15,才会从新生代移至老年代吗?

From Survivor空间、To Survivor空间 中,
如果非垃圾对象装满了,又有新的对象进来(或者from和to内存空间内装不下一个新对象),
此时From Survivor空间和To Survivor空间中所有的非垃圾对象将全部转移至老年代。

信息参考:
JVM内存堆布局图解分析
jvm的新生代、老年代、永久代关系

3.5.7、什么样的对象会保存至老年代?

1、分代年龄达到15次的对象。
2、from和to放不下新对象时(survivor区),会将其中的对象移至老年代。
3、线程池、缓存对象。
4、Spring框架管理的bean。
5、一个Survivor区中的对象如果等于或超过Survivor区的总体容量大小的50%,会直接移至老年代。

前面的年轻代内存回收时Minor GC,那老年代呢?

Full GC

jvm自带内存信息查看器:

public class TestDemo2 {
	byte[] init = new byte[1024*100];//100M空间
	public static void main(String[] args) throws InterruptedException {
		ArrayList<TestDemo2> tests = new ArrayList<>();
		while (true) {
			tests.add(new TestDemo2());
			Thread.sleep(10);
		}
	}
}

jvm工具运行:

jvisualvm

启动界面后,选择指定的程序查看
在这里插入图片描述
注:

如果没有Visual GC查看器,可以去 工具–插件 找到对应的 Visual GC,下载安装插件即可。

3.5.8、什么时候触发GC操作

在这里插入图片描述
查看上述内存使用情况,发现:
1、Eden 区中,占用满了后,会进行一次Minor GC 操作。
在这里插入图片描述
2、老年代堆内存满了,也会触发一次GC操作。
在这里插入图片描述

3.5.9、完整内存模型

在这里插入图片描述

四、性能优化

minor GC 负责新生代(Eden区)垃圾搜集。
Full GC 负责所有堆内存和方法区垃圾搜集。

4.1、新的概念:STW(stop the world)

jvm在做垃圾回收(minor GC 和 Full GC)执行过程中,会暂时将jvm的应用线程暂停。

4.2、为什么执行GC会暂停应用线程(STW)?

1、当GC操作正在执行时,会搜索GC Roots 根 信息,
2、此时如果在执行GC操作时,不暂停线程
3、则会导致GC统计的非垃圾链对象,在GC去扫描识别其他的链信息时,之前的非垃圾链对象成为了垃圾链对象
3、GC继续统计前面的对象信息,将后面非垃圾链对象统计,此时后面已统计的信息中可能存在已经转化为了垃圾链对象。
4、致使GC搜集引用链操作做了无用功(做了不彻底的工作)。

所以在进行GC操作时,会暂停应用线程的执行。
在这里插入图片描述

4.3、STW的后果

由于会暂停应用线程的执行,会导致业务处理造成短暂的延迟卡顿。

4.4、如何才能更好的实现jvm的性能调优?

GC执行次数要少。
或者GC垃圾搜集时间要短。

我们在上面有几点说明:

1、3.5.7、什么样的对象会保存至老年代?
2、3.5.8、什么时候触发GC操作?

其次,正常新生代和老年代内存默认占用比例分别为堆内存的 1/3 和 2/3。
同时我们具体分配新生代和老年代内存量时,需要考虑到高访问量时,类的大小和多少时间会存满Eden区,造成minor GC操作。

关于类的大小计算,可以参考资料:
java如何获取一个对象的大小
两种计算Java对象大小的方法
一个Java对象到底占用多大内存?

性能调优最基本的操作就是设定项目在堆内存中能尽可能的存在新生代,并能依靠 minor GC处理进行垃圾清理,而不是采取Full GC处理(所有堆内存和方法区中的垃圾)。

设定堆内存参数,参考博客:《JVM中的堆以及调优参数

参数 描述
-Xms 设定堆内存初始的大小(新生代+老年代)
-Xmx 设定堆内存的最大分配大小
-Xmn 设定堆内存中新生代的大小
-Xss 设定每个线程分配的线程栈内存大小
-XX: 参考:https://www.cnblogs.com/lionjulyy/p/9951247.html

举个栗子:

java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize= 256M 
-XX:MaxMetaspaceSize=256M -jar xxxx.jar

表示
1、设定初始堆内存和最大堆内存大小为3G。
2、设定堆内存中新生代(eden+Surivior)大小为2G。
3、设定每个线程栈的栈内存大小为1M。
4、设定方法区的初始内存和最大内存为256M。

猜你喜欢

转载自blog.csdn.net/qq_38322527/article/details/103313875