jvm组成
借用Java虚拟机(JVM)面试题的图来看jvm
jvm由两个子系统两个组件组成。两个子系统分别是类加载子系统,执行引擎。两个组件是运行时数据区域,本地接口。
类加载子系统:根据给定的类名,加载进运行时数据区域的方法区中
执行引擎:执行classes的指令
本地接口:与其他编程语言交互
运行时数据区域:jvm的内存
作用:编译器把java代码变成字节码,类加载器去加载字节码到内存中,即到运行时数据区域的方法区中,然后字节码通过执行引擎变成底层的指令集,指令集交给cpu的过程中要调用到本地接口来实现整个程序的功能。
jvm内存区域
java虚拟机再执行java程序的时候会把他管理的内存划分成不同的区域,有线程私有的,有线程共享的。这里借用公众号JavaGuide的图片
可以看到虚拟机栈,本地方法栈,程序计数器是线程私有的。堆,方法区,直接内存是线程共享。下面一个个介绍
程序计数器
程序计数器占一块较小内存,字节码解释器工作时通过改变程序计数器的值来选取下一条要执行的字节码指令,同时每个线程有自己的程序计数器,意味着线程之间互不影响,切换线程后可以恢复到上次运行的位置。
程序计数器是唯一一个不会出现OOM的内存区域,因为他的生命周期随着线程创建而创建,结束而死亡。
虚拟机栈
虚拟机栈是线程私有的,生命周期和线程相同。描述的java方法执行的内存模型,每次方法调用的数据都是通过栈来传递。
java内存大概分为栈内存和堆内存,栈指的是虚拟机栈中的局部变量表,主要存放编译器可知的各种数据类型以及对象引用。
java虚拟机栈会出现两种错误StackOverFlowError与OutOfMemoryError。
本地方法栈
基本与虚拟机栈类似,虚拟机栈为虚拟机执行java方法,本地方法栈为虚拟机使用的native方法服务。
堆
堆是虚拟机中占内存最大的一块,负责存放对象实例,几乎所有对象实例和数组都在这分配内存。如果某些方法中的对象引用没有被返回或者未被外面使用,就会分配内存在栈上。
java堆是垃圾收集器管理的主要区域,由于现在都采用分代垃圾收集算法,所以堆细分成新生代,老年代。
可以看到jvm分为堆内存和非堆内存,非堆内存就是永久代,又称方法区。堆内存存放的是对象,同时垃圾收集器就是判断处理这些对象的。非堆内存存放的是永久代,放的是程序运行时长期存在的对象,如类的方法,常量。
JDK8的时候废除了永久代,然后在直接内存里面弄了个元空间,都是方法区的实现。
方法区
方法区是各线程共享的一个区域,存储类的常量,静态变量等。方法区与永久代的关系引用文献
《Java 虚拟机规范》只是规定了有⽅法区这么个概念和它的作⽤,并没有规定如何去实现它。那么,在不同的 JVM 上⽅法区的实现肯定是不同的了。 ⽅法区和永久代的关系很像Java 中接⼝和类的关系,类实现了接⼝,⽽永久代就是 HotSpot 虚拟机对虚拟机规范中⽅法区的⼀种实现⽅式。 也就是说,永久代是 HotSpot 的概念,⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久代这⼀说法。
为什么要用元空间来代替永久代,永久代有jvm设置的固定大小,不能调整,所以经常发生空间溢出的错误,而元空间用的是直接内存,就是你机器可用内存的限制,出现错误的概率比较小。
运行时常量池
运行时常量池在方法区里面,一开始运行时常量池逻辑包括字符串常量池在永久代里面,jdk7后就把字符串常量池移到了堆中,运行池常量池则还在方法区,不过方法区从永久代变成了元空间。
堆和栈的区别
- 栈是线程之间私有的,堆是所有线程共享的。
- 栈的存取速度比堆快,
java类加载的过程
分为三个步骤,加载,连接,初始化。其中连接可以分为验证,准备,解析。
加载:将class文件加载进内存,并创建一个class对象。类的加载有类加载器来完成。
验证:确保加载类的信息符合规范,无安全问题。
准备:为类的静态Field分配内存,设置初始值(不是代码设置的初始值,是java虚拟机的默认初始值)
解析:将类的二进制数据中的符号引用替换成直接引用
初始化:对类的变量初始化,对static修饰的变量或者代码块进行初始化。
类加载器有 启动类加载器,扩展类加载器,系统类加载器,用户自定义类加载器
类加载的机制
双亲委托机制:首先,每个加载器都有对应的父加载器,除了启动类加载器。
类加载器收到加载的请求,不会自己立马加载,而是去把请求转给父类,如果父类还有父类,就继续转。当转到启动类加载器(即没有父类了),判断启动类加载器有没有加载过,有就加载成功,不能就回退给启动类加载器的子类,尝试是否被加载过,不能就继续回退,一直到第一个类加载器自己加载为止。
优点:防止重复加载一个类,保证数据安全。
java对象的创建过程
分为类加载检查,分配内存,把内存区域初始化零值,设置对象头,执行init方法。
类加载检查:检查new后面的参数是否能在常量池中定位到这个类的符号引用,检查是否被加载过,没有就按步骤去加载类。
分配内存:为新的对象分配内存,内存大小在第一步就已经知道了。分配方式有“指针碰撞”和“空闲列表”两种。
初始化零值:虚拟机将分配到的内存都初始化为零值。(注意:不包括对象头)
设置对象头:把一些必要信息存放到对象头中。
执行init方法:执行完new之后,已经生成了一个可用的对象了,然后要按照程序员的意愿执行init方法,就是把它设置成任意值这种,人为的初始化。
对象的访问方式
访问方式由虚拟机来实现,一般由两种,句柄访问,直接指针。
内存分配
堆内存分为新生代,老年代。新生代又分为eden区,survivor from(s0),survivor to(s1),eden区域最大。一般对象会在eden区分配,当eden没有足够空间去分配,就发起一次Minor GC(新生代垃圾收集)。大对象(需要大量连续内存空间)直接进入老年代,长期存活的对象进入老年代。每个对象都有一个age计数器,对象在survivor区每经过一次Minor GC就增加一岁,如果到了默认值(一般15),就会变成老年代。
判断对象死亡的两种方法
堆进行回收要判断对象是否已经死亡(不再被任何途径使用的对象)。
引用计数法:每个对象添加一个引用计数器,有被引用就+1,失效就-1。如果计数器为0就是不可能再被使用的了。
可达性分析算法:把名字是GC Roots的对象作为起点,通过这些起点开始往下搜索,节点走过的路径就是引用链,如果一个对象到GC Roots没有任何引用链,就代表对象不可用了,可以被回收了
图片出自JavaGuide的复习资料
垃圾收集算法
- 标记-清理算法
先标记出所有不需要回收的对象,然后把没有标记的对象都给回收了。有效率问题和回收后存在大量不连续的碎片问题。 - 复制算法
先把内存分为两块相同大小的内存块,每次使用都只在一个上面使用,当这块内存使用完后,回收掉不需要的对象,然后把剩下的对象复制到另一个内存上面,再把这个内存块全部清理掉,下次再使用。 - 标记-整理算法
先进行标记不需要回收的对象,然后把所有对象移动到另一端,然后直接清理掉边界以外的内存。 - 分代收集算法
根据存活周期分为不同代的对象,如新生代,老年代一样。根据每个年代的特点执行相对应的算法。如新生代使用复制算法,老年代使用标记清理,标记-整理算法。这样子就可以提高gc的效率。
四个引用
引用计数法和可达性分析算法都是判断对象是否被引用。引用又可以分为强引用,软引用,弱引用,虚引用。
强引用:大部分引用都是强引用,垃圾回收器决定不会回收强引用,即使抛出OOM错误,终止程序都不会随意回收强引用对象。可能会导致内存泄露。
在安卓中一般就是new一个对象就是强引用了。
软引用:软引用如果内存足够就不管他,如果内存不足了,就会开始回收他。只要没被回收就可以使用。
从网络上获取照片并显示时,用软引用缓存下来。下次再去网络上获取的时候就可以先判断该图片有没有被缓存,有的话就显示。
弱引用:如果垃圾回收器发现了弱引用的对象,不管内存是否充足,都会进行回收。但是垃圾回收机制优先级低,所以一般不会立马回收掉。
安卓中也可以缓存一些数据,防止内存泄露。
虚引用:如果一个对象有虚引用,和没有引用一样。任何时候都可能被回收。虚引用主要用于跟踪对象被垃圾回收的活动。