浅谈JVM

一、JVM入门
1.JVM的常见问题

请谈谈你对JVM的理解?java8的虚拟机有什么更新?

什么是OOM?什么是StackOverflowError?有哪些方法分析?

JVM的常用参数调优你知道哪些?

谈谈JVM中,对类加载器你的认识?

2.JVM的作用

想要运行一个Java代码,需要具备JRE环境。而JRE中,包括Java虚拟机及Java的核心类库。Java程序员通常安装的JDK,则已经包括了JRE,还附带了常用的开发和诊断工具。

Java虚拟机会将字节码,即class文件加载到JVM中。由JVM进行解释和执行

在这里插入图片描述

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

3.JVM的体系结构

在这里插入图片描述

Java 虚拟机将运行时内存区域划分为五个部分,分别为方法区、PC 寄存器、Java 方法栈和本地方法栈。

执行 Java 代码首先需要使用类加载器将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。

方法区和堆为线程共享,是垃圾回收的重点照顾区域,栈空间为线程私有,基本不会出现垃圾回收。

Java 虚拟机将栈细分为面向 Java 方法的 Java 方法栈,面向本地方法(用 C++ 写的 native 方法)的本地方法栈,以及存放各个线程执行位置的 PC 寄存器(程序计数器)。

在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧(栈的一片区域),用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

二、类加载器
1.简介

类加载器,即ClassLoader,它负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

在这里插入图片描述

2.类加载器分类

虚拟机自带的类加载器:

  • 启动类加载器(Bootstrap):主要负责加载jre中的最为基础、最为重要的类。如$JAVA_HOME/jre/lib/rt.jar等,以及由虚拟机参数 -Xbootclasspath 指定的类。由于它由C++代码实现,没有对应的java对象,因此在java中,尝试获取此类时,只能使用null来指代。
  • 扩展类加载器(Extension),由Java代码实现,用于加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类,以及由系统变量 java.ext.dirs 指定的类。如$JAVA_HOME/jre/lib/ext/*.jar。
  • 应用程序类加载器(AppClassLoader),由Java代码实现, 它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。用户自定义类
  • 用户自定义的加载器:Java.lang.ClassLoader的子类,用户可以定制类的加载方式。例如可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。

在这里插入图片描述

3.双亲委派机制

双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

优势:

  • 避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 防止java核心api中定义类型不会被用户恶意替换和篡改,从而引发错误。
三、JVM的内存模型

在这里插入图片描述

1. Execution Engine

执行引擎负责解释命令,提交操作系统执行。在 HotSpot 里面,将字节码翻译为机器码的翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译,即将一个方法中包含的所有字节码编译成机器码后再执行。 前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。

2.Native Method Stack

定义了很多调用本地操作系统的方法,也称之为本地方法接口。

本地方法栈(让操作系统执行):

native方法 ->压到本地方法栈里面 ->向操作系统发送指令,交给执行引擎解释命令->调用本地方法接口->会用到本地方法库

Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraie(本地方法库)。

3.PC寄存器(程序计数器)

​ 每个线程都有一个程序计数器,是线程私有的,就是一个指针,连接栈中的方法,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

​ PC寄存器主要负责计数和调度。它可以看作是当前线程所执行的字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

4.方法区

​ 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。

**静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中。**实例变量存在堆内存中,和方法区无关。

5.栈
5.1栈介绍

​ 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就结束,生命周期和线程一致,是线程私有的。

8种基本类型的变量+对象的引用变量+实例方法都是在栈内存中分配

byte short int long float double char boolean

StackOverflowError异常:在栈区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度

OutOfMemoryError异常:如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存

5.2栈帧

​ 一个线程的每个方法在调用时都会在栈上划分一块区域,用于存储方法所需要的变量等信息,这块区域称之为栈帧(stack frame)。栈由多个栈帧构成

5.3 栈运行原理

​ 栈中的数据都是以栈帧(Stack Frame)为载体存在。在栈中,方法的调用顺序遵循“先进后出/后进先出原则。

在这里插入图片描述

栈帧中主要保存3 类数据:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;

  • 栈操作(Operand Stack):记录出栈、入栈的操作;

  • 栈帧数据(Frame Data):包括类文件、方法等等。

5.4 堆栈方法区的交互关系

在这里插入图片描述

6.堆
6.1逻辑设计

堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆内存的大小是可以调节的,初始化堆内存是物理内存的1/64 最大时1/4

所有的对象实例以及数组都要在堆上分配

如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”(Garbage Collected Heap)

堆内存逻辑上分为三部分

​ Young Generation Space 新生区 Young/New

​ Tenure generation space 养老区 Old/ Tenure

​ Permanent Space 永久区(1.8后为元空间) Perm

在这里插入图片描述

垃圾回收机制:

minor GC:回收伊甸园区和幸存from区

Full GC:全局GC​

当伊甸区的内存不足时候,触发轻量级的垃圾回收机制manager GC

伊甸园区在垃圾回收后幸存者会移动到幸存to区

幸存from区,如果没有经过15次垃圾回收就进入to区,如果超过了15次就进入养老区

哪个幸存区是空的,哪个就是幸存to区

重量级垃圾回收:主要是堆对养老区进行垃圾回收

如果针对养老区进行垃圾回收失败会出现OOM错误

如果new的新对象,占的内存比较大,伊甸园区放不下,但是养老区可以放下,就放入养老区,都放不下直接OOM

6.2物理设计

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )

新生代又被划分为三个区域:Eden、From Survivor、To Survivor

from和to区域是两块大小相等并且可以互换角色的空间。绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄(默认15)后,则进入老年代。
在这里插入图片描述

6.3永久区

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

JDK1.8之后就没有了永久代,被元空间代替。永久代是方法区的一个实现,1.7版本之前是永久代,1.8之后就是元空间

四、JVM参数
参数名含义
-XX:+PrintGC每次触发GC的时候打印相关日志
-XX:+PrintGCDetails更详细的GC日志
-Xms堆初始值(默认为物理内存的1/64)
-Xmx堆最大可用值(默认为物理内存的1/4)
-Xmn新生代堆初始值
-XX:SurvivorRatio用来设置新生代中eden空间和from/to空间的比例,默认为8
-XX:NewRatio配置新生代与老年代占比,默认1:2
-Xss每个线程的栈大小,默认为1M,此值不能设置过大,否则会减少线程并发数。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制

1.OutOfMemoryError

错误原因: java.lang.OutOfMemoryError: Java heap space 堆内存溢出

解决办法:调大堆内存大小

2.StackOverflowError

错误原因: java.lang.StackOverflowError表示为栈溢出,一般产生于递归调用。

解决办法:设置线程最大调用深度,默认是1m

-Xss5m 设置最大调用深度

五、GC
1.GC定义

JVM中的**Garbage Collection,**简称GC,它会不定时去堆内存中清理不可达对象。

2.GC的分类

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)

新生代GC(minor GC):只针对新生代区域的GC。

老年代GC(major GC or Full GC):针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC。


Minor GC触发机制:当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor满不会引发GC。

Full GC触发机制:当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代

3.GC的工作特点

在GC工作中,通过某种算法来对JVM中的内存区域进行检测,对检测到的不可达对象,进行垃圾回收。

理论上GC过程中会频繁收集Young区,很少收集Old区,基本不动Perm区(元空间/方法区)。

4.标记不可达对象
4.1引用计数法

引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到循环指向的存在。给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的

缺点:很难解决对象之间相互循环引用的问题

public class MyObject {
	public Object ref;
	public String name;
	public static void main(String[] args) {
		MyObject myObject1 = new MyObject();
		MyObject myObject2 = new MyObject();
		myObject1.ref=myObject2;
		myObject2.ref=myObject1;
		myObject1=null;
		myObject2=null;
	}
}

将myObject1和myObject2赋值为null后,虚拟机依然无法回收,因为他们还相互指向和依赖

4.2GC ROOTS算法

可达性分析(GC ROOTS算法)

简单理解,可以理解为看有没有堆外指向堆内的引用。

5.垃圾回收的三种方式
5.1 清除

第一种是清除(sweep),即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

清除这种回收方式的原理及其简单,但是有两个缺点。

一是会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。

另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。

在这里插入图片描述

5.2 压缩

第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。

压缩之后,每块内存的地址都要变化导致性能开销较大

在这里插入图片描述

5.3 复制

第三种则是复制(copy),即把内存区域分为两等分,分别用两个指针 from和 to来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。

在这里插入图片描述

5.4 总结

​ 回收死亡对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。当然,现代的垃圾回收器往往会综合上述几种回收方式,综合它们优点的同时规避它们的缺点。

6.垃圾回收算法
6.1标记复制(Mark-Copying)算法

​ 当我们调用 new 指令时,它会在 Eden 区中划出一块作为存储对象的内存。当 Eden 区的空间耗尽时,这个时候 Java 虚拟机便会触发一次 Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到 Survivor 区。

​ 新生代共有两个 Survivor 区,我们分别用 from 和 to 来指代。其中 to 指向的 Survivior 区是空的。当发生 Minor GC 时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还是空的。

​ Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。万一存活对象数量比较多,那么to域的内存可能不够存放,这个时候会借助老年代的空间。

​ 因此Minor GC使用的则是标记-复制算法。将 Survivor 区中的老存活对象晋升到老年代,然后将剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中。理想情况下,Eden 区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记 - 复制算法的效果极好。

在这里插入图片描述

复制必交换,谁空谁为to

6.2标记清除(Mark-Sweep)算法

老年代一般是由标记清除或者是标记清除与标记压缩的混合实现。

标记清除算法一般应用于老年代,因为老年代的对象生命周期比较长。该算法先对所有可访问的对象,做个标记再遍历堆,把未被标记的对象回收(标记活的)。

缺点:

​ ①回收时,应用需要挂起,也就是stop the world,导致用户体验非常差劲,如果没有挂起操作,当标记操作完成的时候,此时有新生区来的对象,但是还被引用,但是因为到了回收的时候没有被标记而被回收,产生错误。

​ ②由于需要遍历全堆对象,效率比较低(递归与全堆对象遍历)。

​ ③造成内存碎片化

6.3标记压缩(Mark–Compact)算法

在这里插入图片描述

标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化。

优点:解决内存碎片化问题。也消除了复制算法当中,内存减半的高额代价

缺点:效率低,压缩阶段,由于移动了可用对象,需要去更新引用。

6.4标记清除压缩(Mark-Sweep-Compact)算法

标记清除压缩(Mark-Sweep-Compact)算法是标记清除算法和标记压缩算法的结合算法。其原理和标记清除算法一致,只不过会在多次GC后,进行一次Compact操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值