详述Java中JVM那些不为认知的秘密

各位大佬光临寒舍,希望各位能赏脸给个三连,谢谢各位大佬了!!! 

目录

1.JVM是什么

2.JVM内存模型

1.本地方法栈(Native Method Stack)

2.程序计数器(Program Counters Register)

3.虚拟机栈 (VM Stack)

4.堆区(Heap) 

5.元数据区(metaspace)

6.判断变量在什么内存中的位置

7.小结

3.JVM的类加载过程

1.加载

2.验证

3.准备

4.解析

5.初始化

6.双亲委派模型

ApplicationClassLoader  ==》

==》ExtentionClassLoader ==》

==》BootstarpClassLoader

图片描述查找过程

4.GC(垃圾回收)

1.判断垃圾

1.引用计数

2.可达性分析 

2.垃圾回收方式

1.标记清除

2.复制算法 

3.标记整理

小结

5.Java中GC的讲解

伊甸区 

幸存区

老年代

小结

6.总结


1.JVM是什么

JVM(Java Virtual Machine)也就是Java虚拟机。它就类似于一个子电脑在我们的Windows或是Linux环境中运行,它可以与我们的系统直接进行交互。

2.JVM内存模型

知道了JVM是什么,那我们来了解一下JVM的内存模型长成啥样。如下图所示:

那让我们来逐一认识一下这五大内存区域吧!

1.本地方法栈(Native Method Stack)

它是为了专门执行本地方法而设立的栈,本地方法就是非Java语言,用的是C或C++等语言写的方法。这些代码通过JNI接口和Java交互。一般在Java中,带native修饰的就是本地方法。

2.程序计数器(Program Counters Register)

它的作用是存储线程下一条指令,确保了指令正确的运行顺序,每一个线程都拥有自己的程序计数器。而每个方法的指令在元数据区中,以二进制存储到类对象中。一般程序计数器占据的空间很小。

3.虚拟机栈 (VM Stack)

它的作用是维护被调用的方法,维护了方法和方法之间的调用关系。每一个元素就是一个栈帧,每个栈帧就是一个方法,它里面包含了方法的入口,方法的返回位置,方法的形参,返回值,局部变量等。每一个线程也都拥有一个虚拟机栈。所以才说线程是调度执行的基本单位(我在前面介绍线程和进程关系的博客中有写点击跳转),看吧,世界线回溯了。

4.堆区(Heap) 

它就是我们JVM中的老大哥,占据着JVM内存的最大区域,也是GC回收的主要区域,我们在博客后面的GC内容中会详细介绍里面的内存分布。在这里我们只需要知道他存放的是我们创建出来的实例对象以及数组。

5.元数据区(metaspace)

元数据区主要存储一些静态的数据,如静态变量(它所引用的new出来的对象还是在堆区的),方法的字节码,常量池数据。总的来说存的就是类对象,也就是.class文件加载到内存之后的内容。元数据区在JDK1.8之前叫做方法区,在1.8及之后叫做元数据区。区别就是取代了永久代,虽然GC扫描的间隔久,但也是减少了了内存泄漏的可能。

6.判断变量在什么内存中的位置

有时候我们可能需要知道一个变量在内存的什么位置,这时候我们只需要判断这个变量是什么变量即可。静态变量(包含在类对象中)在元数据区,成员变量在堆区,局部变量在虚拟机栈区。我们new出来的实例也是在堆中。

7.小结

一个进程拥有一个JVM的所有内存资源,而一个线程只会有独自的程序计数器和虚拟机栈。所以线程的堆区内存是共享的,这也可以解释为什么线程可以直接取到成员变量。这就充分说明了进程是资源分配的基本单位,线程是调度执行的基本单位。

3.JVM的类加载过程

类加载分为5步,加载,验证,准备,解析,初始化。那让我们来看看这五步分别是在做什么工作吧。

1.加载

加载简单的来讲就是根据全限定类名(如:java.lang.String)找到编译好的类的.class文件,将其读取到元数据区中。这里查找时涉及到一个“双亲委派模型”,这个我们在后面讲,我们先把类加载的步骤讲完。

2.验证

这里是对文件格式,元数据,字节码和引用符号的验证。如图是需要一一对应的字节码文件:

这是为了保证虚拟机的安全,防止对虚拟机造成的伤害。

3.准备

准备主要是为类中的静态变量开辟内存空间并且赋予默认值,如果这个值被final修饰,则这个变量在准备阶段就会被赋为指定的值。

4.解析

解析是把符号引用转化为直接引用。那这句话是什么意思呢?我们分开理解,其中符号引用就是指某个常量,如字符串在常量池中的偏移量,这里的常量池并不是指元数据区中的常量池,而是在文件中的常量池,因为在编译.class文件时,这个文件还没有被加载到内存中,所以当某个变量要引用这个常量时就只能给它一个相对的地址,也就是偏移量。在解析时就是把这个符号转变为直接引用(在内存中的位置)。

5.初始化

初始化就是要把各个属性所对应的值或引用都一一变为指定值。还要执行静态代码块,以及加载一下父类等。

6.双亲委派模型

双亲委派模型就是JVM查找类的一种方法模型。它保证了查找到类的唯一性,避免了自定义代码篡改Java的核心库。当然程序员也可以自己写加载器来实现加载。Java提供了3个类加载器,分别为ApplicationClassLoader(应用程序类加载器),ExtentionClassLoader(拓展类加载器),BootstarpClassLoader(启动类加载器)。它们分别加载,自己写的项目以及第三方库,拓展库,Java标准库。那现在让我们来讲述一下这个模型到底是怎样工作的吧!

ApplicationClassLoader  ==》

类的查找回先从ApplicationClassLoader开始,所以刚开始把查找任务交给了ApplicationClassLoader,但是类加载器很懒,它会把任务传递给自己的父亲,而ApplicationClassLoader就是ExtentionClassLoader。

==》ExtentionClassLoader ==》

ExtentionClassLoader接收到任务,它发现自己好像也有爹,这时候它就把任务给自己的爹BootstarpClassLoader。

==》BootstarpClassLoader

这时候BootstarpClassLoader接到任务之后也想找爹,但是它没爹了。这时候它只好老老实实地执行任务了。它会在Java标准库中查找是否有这个类,如果找到了,那就加载。就是因为标准库优先级最高,所以才让Java在加载类时不会因为自定义的代码而篡改Java的核心库,导致程序运行故障。如果标准库中没找到那么它就会把任务交还给ExtentionClassLoader,如果它没找到,就交给ApplicationClassLoader,如果它也没找到,那就会报错找不到这个包。如图:

但是一般在使用idea时编译期间就会报错的,所以这个 错误很少见。

图片描述查找过程

我们用图片来简单描述一下双亲委派模型的流程:

本质上就是一个递归 。

4.GC(垃圾回收)

JVM会对Java中没有用的垃圾进行回收,在这里主要针对的就是我们new出来的对象,因为在虚拟机栈区中的局部变量跟随栈的销毁而销毁,内存是自然释放的,static修饰的静态变量,本身就是类的属性,贯串程序的生命周期,它也不需要释放。所以真正需要释放的就是堆上的内存,当然在元数据区内没有被用到的类,其中常量池中没有被用到的常量等也会被清理,只不过周期较长。我们这里重点讲堆区的垃圾回收。在了解垃圾回收之前,我们要先了解怎么判断是否为垃圾。

1.判断垃圾

1.引用计数

在一个变量所指向的内存之前开辟一块空间,表示这块空间有多少个变量指向它。当指向它的变量为0时,这块空间就会被当作垃圾。这种方式很简单,但是一个变量会消耗更多的内存,对于大内存还好说,但是对于像byte这种只有一个字节的,引用计数前面所多占的内存也就显得很多了。并且它还会出现互相引用的情况,如图:

所以Java没有使用这种方法。

2.可达性分析 

可达性分析也就是判断这个变量可以不可以访问到,如果不能访问到,那么就标记为垃圾。可达性分析的入口有很多,可以是局部变量也可以是静态变量常量区的变量等,这些统称为GCRoots。这种方法隔一段时间就得扫描一次。是一种用时间换空间的措施。当然也不会出现互相引用不被当做垃圾的情况。

那么知道了怎么标记垃圾,我们来看看怎么回收垃圾。

2.垃圾回收方式

1.标记清除

直接对标记的垃圾进行清除。这种方法虽然很高效,但是会产生内存碎片化的问题。如图:

2.复制算法 

复制算法是开辟一块和原内存一样大小的内存,然后每一次清除数据的时候都把原来的数据有序复制到开辟的另一半的内存中 ,解决了内存碎片化问题,因为只需要复制那些有用的数据,所以它很高效。并且分配内存时也十分高效,因为它是没有碎片空间的。但是它也有缺点:它的空间利用率很低,只有百分之五十,并且当对象存活率很高时,它的效率就不是那么高了。

3.标记整理

标记整理算法也就是将标记好的垃圾作为空位也解决了内存碎片化问题,然后将后面的数据依次往前移动,就像顺序表中删除非末尾的数据一样。它的优点是当对象存活率高时,它的效率和内存使用率就会相对于复制算法高。但是它的缺点也很明显,它在对象存活率低的情况下效率很低,毕竟它要对对象标记之后还要对其进行整理。

小结

复制算法和标记整理各有优缺点,标记清除算法实在太挫了几乎没有语言会用。那我们的Java是用什么算法的呢?请看下一小节的讲解。

5.Java中GC的讲解

Java并不是单单使用了复制算法或是标记整理,它根据经验把这两种算法都用上了。至于怎么全都用上呢?因为主要回收的是堆区,所以我们这里只讲堆区回收的原理。Java把堆区内存分为两个大区,分别为新生代和老年代。新生代中又分为两个区域,分别是伊甸区和幸存区(这两个词出自于圣经,有兴趣的哥们儿可以去看看这两个词的含义)。如图:

伊甸区 

它里面放的是GC第一轮扫描还未扫描到的对象。根据经验,这里的对象存活率是最低的,所以经过GC一轮扫描之后,里面非垃圾数据就会被转移到幸存区,当然垃圾会被清除掉。这里GC的扫描频率也是最高的。

幸存区

就像上面说的,它里面放的是GC第一次扫描幸存下来的对象,根据经验这个区域的对象存活率也不是很高,所以这个区域采用复制算法。当经过多次GC扫描还存在的对象会被移动到老年代。此处GC的扫描频率相对伊甸区的速度较低一些。

老年代

老年代里的对象一般认为是基本不会改变的,所以此处使用标记整理算法,这样效率也是最高的,当然就算是基本不会改变也是有改变机率的,所以GC也会扫描,但是这个扫描频率是很低的。

小结

Java帮程序员想的很周到,其实不仅仅是堆区,元数据区也会进行GC扫描,当然扫描频率也是很低的。并且在需要回收的数据很多时可能会出现STW(Stop the World)问题,整个程序都会卡顿。特别是老年代,需要处理的对象多,算法复杂度高,更容易出现这种问题。并且GC算法也有坏处,那就是对象不会立即被释放,因为GC扫描是有一定间隔的,并且GC运行时会占用很大的系统资源。

6.总结

JVM的原理最主要的就是上述内容,当然虽然是最主要的但是也只是冰山一角。如果真的想要了解可以去看看OpenJDK的源码,或者可以看看《深入理解Java虚拟机》这本书。

制作不易,望各位大佬赏个脸,给个三连吧!!谢谢各位大佬了!!!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值