一、jvm内存模型
有程序计数器,java虚拟机栈,本地方法栈,堆,方法区,直接内存。
其中,线程私有的:程序计数器,虚拟机栈,本地方法栈;
线程共享的:堆,方法区,直接内存。
程序计数器:
主要有两个作用:
a:字节码解释器通过程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理。
b:在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候知道该线程
上次运行到哪儿了。
注意,程序计数器是唯一不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,
随着线程的结束的消亡。
Java虚拟机栈:
线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧都会存放局部变量表、操纵数栈、动态链接、方法出口信息。
局部变量表主要存放了编译器可知的各种基本数据类型和对象引用。
Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError.
StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的
最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError:若Java虚拟机栈的内存大小运行动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,
此时抛出OutOfMemoryError异常。
本地方法栈:
和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,
而本地方法栈则为虚拟机使用到的Natice方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。
其他的栈帧和抛出的异常同Java虚拟机栈。
堆:
Java堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,
因此该区域经常发生垃圾回收机制。
jvm堆内存主要由新生代,老年代,永久代构成。
在JDK1.8中已处理整个永久代,取而代之的是一个叫元空间(Metespace)的区域(永久代使用的是JVM的堆内存空间,
而元空间使用的是物理内存,直接收到本机的物理内存限制)。
方法区:
方法区与Java堆一样,是各个线程共享的内存区域。它用来存储已被虚拟机加载到内存的类信息,常量,静态常量,即时编译器编译后的字节码信息。
运行时常量池:
常量池用于存放编译器生成的各种字面量和符号引用。
在JDK1.7及以后的JVM已经将运行时常量池从方法区中移了出来,在Java堆中开辟了新的一块内存区域存放运行时常量池。
从逻辑上将,运行时常量池属于方法区的一部分,实际上存放在Java堆内存中,这里容易有误解。
二、垃圾回收机制
1,标记-清除算法
这是垃圾收集算法中最基础的,它的思想就是标记有哪些要被回收的对象,然后统一回收。
方法简单,但是有两个主要问题:
a:效率不高,标记和清除的效率都很低;
b:会产生大量不连续的内存碎片,导致以后分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
2,复制算法
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中一块,当一块内存用完时,将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,再将第二块上的对象复制到第一块。但是这种方式的内存代价太高,每次基本上都要浪费一半的内存。
于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1 三部分,较大那份内存叫Eden区,其余两块较小的内存区叫Survior区,每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区域上,然后清除Eden区,如果此时存活的对象太多,以至于Survior不够时,会将这些对象通过分配担保机制复制到老年代中(Java堆分为新生代和老年代)
3,标记-整理算法
该算法主要为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法效率的问题。
它的不同之处就是,清除对象的时候将可回收对象移到一端,然后清除掉端边界以为的对象,这样就不会产生内存碎片。
4,分代收集
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生命周期,将堆分为新生代和老年代。在新生代中,由于对象生命期短,每次回收都会有大量对象死去,那么这是就采用复制算法;在老年代里的对象存活率较高,没有额外的空间进行分配担保,所有可用使用标记-整理或者标记-清除算法。
三、何判断一个对象应该被回收(重点掌握)
1,引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,将计数器+1,引用失效时,计时器就-1.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是将会被垃圾回收。
但有一个很大的缺陷,就是无法解决循坏引用问题。也就是对象A引用对象B,对象B引用对象A,此时A,B对象的引用计数器都不为零,也就不会被垃圾回收,所以主流的虚拟机都没有采用这种算法。
2,可达性算法(引用链法)
该算法的思想是:从一个被称为GC Roots的对象开始往下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此事对象不可用。
在Java中可用作为GC Roots的对象有以下几种:
- 虚拟栈中引用的对象
- 方法区静态属性引用的对象
- 方法区常量池引用的对象
- 本地方法栈JNI引用的对象
四、类加载过程
Java类加载需要经历以下7个过程:
1,加载
在这个阶段,将完成三件事情:
- 通过一个类的全限定名获取该类的二进制流
- 将该二进制流中的静态存储结构转化为方法区运行时数据结构
- 在内存中生成该类的Class对象,作为该类的数据访问入口。
2,验证
验证的目的是为了确保Clas文件的字节流中的信息不会危害到虚拟机。
3,准备
准备阶段是为类的静态变量分配内存并将初始化为默认值,这些内存都将在方法区中进行分配,准备阶段不分配类中的实例变量的内存,实例变量都将会在对象实例化时随着对象一起分配在Java堆中。
4,解析
该阶段主要完成符号引用到直接引用的转换动作,解析动作不一定在初始化动作完成之前,也有可能在初始化之后。
5,初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可用通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码。
6,使用
7,卸载
五、类加载器有哪些
实现通过类的全限定名获取该类的二进制字节流的代码块较类加载器。
主要有以下四种类加载:
- 启动类加载器(Bootstrap ClassLoader):用来jiazaiJava核心类库,无法被Java程序直接引用。
- 扩展类加载器(extesions ClassLoader):它用来加载Java的扩展库,Java虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载Java类。
- 系统类加载器(system ClassLoader):它根据Java引用的类路径(CLASSPATH)来加载Java类,一般来说,Java应用的类都是由它来完成加载的,可以通过ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过集成java.lang.ClassLoader类的方式实现。
六、什么是双亲委派加载
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。