JVM虚拟机
目录
一,内存区域划分
一个运行起来的Java进程,就是一个JVM虚拟机,就需要从操作系统申请一大块内存。这块内存空间会被划分成以下几个部分:
- 方法区:又叫元数据区,用来存储类对象(?.class文件加载到内存)。
- 堆:这里存储的内容,就是代码中 new 的对象,是内存中占据空间最大的区域。
- 栈:用来存储方法之间的调用关系。主要存储方法的入口,方法返回的位置,方法的形参,返回值,局部变量...
- 程序计数器:主要用来存放 "地址" ,表示下一条要执行的指令的地址,指令是以二进制的形式存储在方法区中,并保存在对应的类对象中。
- 本地方法栈:指的是使用native关键字修饰的方法,该方法是通过C++代码实现,这块区域是用来存储JVM内部的C++代码的调用关系。
注:在JVM进程中,堆和与元数据区只有一份,而虚拟机栈和程序计数器则是每一个线程中都有一份。
常见的面试题:问下面代码中,变量 n, a, t 分别处于内存中的哪个区域?
class Demo{
public int n = 100;
public static int a = 1;
void test(){
Demo t = new Demo();
}
}
- n 是成员变量,是存储在new的对象的区域中,上面讲到new的对象都存储在堆上,所以变量n存储在堆上。
- a 是静态变量,而静态变量又称类属性,也就是说它是包含在类对象中的,所以变量a存储在方法区中。
- t 是局部变量,局部变量是存储在栈中的。
总结:一个变量存储在哪里,与变量是否是引用类型,是否是基本类型无关,与变量的形态是密切相关的。局部变量存储在栈上;成员变量存储在堆上;静态变量存储在方法区中。
二,类加载过程
2.1 类加载的基本流程
java代码会被编译成 .class文件,Java程序想要运行起来,就需要让JVM读取到这些 .class 文件,并且把里面的内容构造成类对象,保存到方法区中。这个类加载过程,主要分为5个步骤:
1)加载
找到.class文件,打开文件,读取文件内容,往往代码中会给定类的 "全限定类名",例如:java.util.ArraysList,java.lang.*,JVM会根据这个类名,在一些指定的目录范围内查找。
2)验证
.class文件是一个二进制的格式,(某些字节都是有特定含义的),需要验证当前JVM读到的格式是否符合要求。当前读到的.class文件的格式必须和官方提供的格式一样,下图是官方的.class文件格式。
3) 准备
给类对象分配内存空间,这里只是分配内存空间,没有初始化,此时空间上内存的数值仍然是全0.
4)解析
针对类对象中的包含的字符串常量进行处理,进行一些初始化操作。java代码中用到的字符转常量在编译后,也会进入到.class文件中。
比如:final String str = "abc",与此同时,.class文件的二进制指令也会有一个引用str被创建出来,又因为引用本质上存储的是一个地址,而在文件中,没有地址这样的概念,所以在.class文件中,str的初始化语句,就会被设置成一个 "文件的偏移量",通过偏移量就能找到 "abc" 所在的位置,当这个类真的被加载到内存中时,再把这个偏移量替换为真正的地址。
5)初始化
针对类对象进行初始化,把类对象中需要的各个属性设置好,初始化static成员,执行静态代码块,如果有父类还要加载以下父类
2.2 双亲委派模型
双亲委派模型是属于类加载中"加载"这一环节,负责根据全限定类名查找 .class文件。
在讲双亲委派模型之前,先了解一下类加载器,类加载器是JVM中的一个模块,是专门用来进行类加载操作的,JVM中内置了三个类加载器:1. BootStrap ClassLoader? 2. Extension ClassLoader? 3. Application ClassLoader?
查找 .class文件的过程:
- 给定一个类的全限定类名,比如:java.lang.String
- 从Application ClassLoader 作为入口,开始执行查找逻辑
- Application ClassLoader (负责搜索项目当前目录和第三方库对应的目录),不会立即去扫描自己负责的目录,而是把查找的任务,交给它的父亲,Extension ClassLoader
- Extension ClassLoader (负责JDK中的一些扩展的库对应的目录),不会立即去扫描自己负责的目录,而是把查找的任务,交给它的父亲,Bootstrap?ClassLoader
- Bootstrap?ClassLoader (负责的是标志库的目录),不会立即去扫描自己负责的目录,而是把查找的任务,交给它的父亲,但是它没有父亲,所以只能自己扫描目录,如果找到对应的.class文件,就打开读取该文件....,如果没找到,就把任务交给孩子
- 也就是Extension ClassLoader,如果Extension ClassLoader找到了,就打开读取该文件....,如果没找到,就把任务交给孩子
- 也就是Bootstrap?ClassLoader,如果Bootstrap ClassLoader找到了,就打开读取该文件....,如果没找到,就说明没有该文件,抛出ClassNotFoundExceptioon
?该流程的目的就是为了保证优先查找标准库中的类,其次是扩展库,最后是自己写的和第三方库
三,垃圾回收机制 - GC
在C和C++中,通常使用malloc或者new来申请内存空间,但是在使用后,也需要手动调用free或者delete来释放内存,这十分考验程序员的素养,而相比之下,Java提供了一种方案——垃圾回收机制(GC),让JVM自动判定某个内存是否不再使用,如果真的不在使用,JVM会自动把该内存给回收掉,这大大降低了程序员的负担。
GC机制回收的目标,其实是内存中的对象,对于Java来说,就是new出来的对象,也就是堆上的对象;局部变量是跟随栈帧的生命周期走的,方法执行结束,栈帧销毁,内存自然就释放了;静态变量的生命周期是整个程序,它是始终存在的,这意味着静态变量是不需要释放的。综上所述,真正需要GC释放的就是堆上的对象。
GC的实现需要两大步骤:1. 找到垃圾? ? ?2. 释放垃圾
3.1 找到垃圾
在GC圈中,有两套主流方案:
1)引用计数 【Python,PHP使用】
对于new出来的对象,再单独安排一块空间,来存储一个计数器,用来记录有多少引用指向new出来的对象,一旦引用计数为0,就说明这个对象不可能被访问到,此时该对象就可以视为垃圾。
?为什么Java不使用该方法呢?因为该方法存在两个缺陷:
- 比较浪费内存:计数器最少也要两个字节,但是有可能创建的对象很小,就2个字节,也就是说计数器占了50%的内存,这样的话,就会浪费内存
- 会存在 "循环引用" 问题:
2)可达性分析 【Java使用】
该方法本质上是时间换空间,有一个/一组线程,周期性的扫描代码中所有对象,把所有能访问到的对象标记为 "可达",反之,未被标记的对象就是垃圾。
该方法是出发点有很多,不仅仅是所有局部变量,还有常量池中引用的对象,方法区中静态引用类型的对象.....,这些统称为GCRoots.
注意,这里的可达性分析都是周期性进行的,所以它比较消耗系统资源,开销比较大。
3.2 回收垃圾
三种思路:
1. 标记清除:比较简单粗暴,把找到的对象,直接释放,但是这个方案非常不好,因为这样会产生很多的内存碎片,释放资源的目的是为了让别的代码能够申请,而申请的内存都是连续的,比如:申请一个2MB的空间
2. 复制算法:通过复制的方式,把有效的对象放到一起,再统一释放剩下的空间。
3. 标记整理
?
实际上,JVM采取的释放思路,是上述基础思路的结合,以此来扬长避短。
- 伊甸区存放刚new的新的对象,根据经验规律,从对象诞生到第一轮可达性分析,这个过程中大部分对象都会成为垃圾。
- 伊甸区 => 幸存区,使用复制算法,每一轮GC扫描后,都要把有效对象拷贝到幸存区的一边,伊甸区就可以整体释放。
- 幸存区分为两份,大小相同,每次GC扫描幸存区的时候,就会把有效对象拷贝到幸存区的另一边。
- 当这个对象已经在幸存区存活过很多轮GC扫描之后,JVM就认为这个对象短时间内是释放不掉了,就会把这个对象拷贝到老年代
- 进入老年代的对象,虽然也会被GC扫描,但是JVM认为这个对象短时间内是释放不掉,所以GC扫描的频率就会比新生代低很多。这也是为了减少GC扫描的开销。
我们将上述思想称为:分代回收
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!