1 JVM 内存结构
2 类加载器
- 启动类加载器(bootstrap)
主要加载jvm 自身需要的类,采用c++ 语言实现,是虚拟机自身的一部分。负责加载JAVAHOME /lib 路径下的jar 包,文件名必须要虚拟机能够识别,否则就算将jar 包添加到该目录下也不会进行加载。出于安全考虑,bootstrap 启动类加载器只加载java、javax 、sun 等开头的类。 - 拓展类加载器(extension)
java 语言实现、负责加载JAVAHOME /lib /ext 目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。 - 应用程序类加载器
java 语言实现,负责加载classpath 路径下的类库。
双亲委派模式
沙箱安全机制
沙箱安全机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制,假设你要写一个java.lang.String 的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.
破坏双亲委派模型
应为双亲委派模型是1.2 才出现的所有早于该版本的一些代码未遵循改模式。
因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
类加载过程
类是第一次使用时动态加载的,而不是一次加载所有类。因为一次加载,哪会占用很多内存。
总体分为三个阶段
加载、连接、初始化
-
加载
将不用来源的java 字节码文件加载到方法区并封装到class 对象中
1.通过类的全限定名获取定义该类的二进制字节流。
2.将该字节流的静态存储结构转换为方法区的运行时存储结构。
3.在内存中生成一个代表该类的Class 对象,作为方法区该类各种数据入口 -
验证
确保class 字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全 -
准备
为类变量赋初始即零值。使用方法区的内存。
实例变量不会在阶段分配内存,他会在对象实例化的时候一起分配到堆内存中,实例化不属于类加载的一个过程。
-
解析
将常量池符号引用替换为直接引用的过程 -
初始化
1.初始化阶段就是执行类构造器方 clinit() 的过程
2.此方法不需要定义,是javac 编译器自动收集类中的所有变量的赋值动作和静态代码快中的语句合并而来的。
3.构造器方法中指令语句在源文件中出现的顺序执行
4.clinit() 不同于构造器
5.若该类有父类,jvm 会确保子类的clinit() 执行前,父类的clinit() 先执行
6.虚拟机必须保证一个类的clinit() 在多线程的情况下加锁。
程序计数器(PC 寄存器)
以线程私有的一块较小的空间,用来记录所有线程所执行的字节码行号指示器 ,java 虚拟机中唯一一个没有OOM 情况的地方
本地方法栈
这部分主要与虚拟机用到native 方法相关,线程私有
JAVA虚拟机栈
1.线程私有,声明周期和线程相同
2.如何线程请求栈的深度大于虚拟机所允许的深度,抛出StackOverflowError,当无法申请到足够内存时也会抛出 OOM 异常
3.java 虚拟机栈描述的是java 方法执行的内存模型,每个方法执行时会创建一个栈帧。我们只关注stack 栈内存,就是虚拟机中局部变量表部分。
栈帧(Stack Frame)
栈帧是虚拟机进行方法执行和方法调用的数据结构。是虚拟机运行时数据区中的 java 虚拟机栈的栈元素。
栈帧存储了方法的局部变量表 、操作数栈、动态链接和方法返回地址等信息。
每一个方法从调用开始到完成的过程,都对应着一个栈帧在虚拟机里面入栈到出栈的操作。
结构图:
在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧,与该栈帧先关联的方法称为当前方法。
局部变量表
1.局部变量表是一组变量值存储空间,用来存放方法参数和方法内部定义的局部变量。
2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址)
很多人说:基本数据和对象引用存储在栈中。
当然这种说法虽然是正确的,但是很不严谨,只能说这种说法针对的是局部变量。
局部变量存储在局部变量表中,随着线程而生,线程而灭。并且线程间数据不共享。
但是,如果是成员变量,或者定义在方法外对象的引用,它们存储在堆中。
因为在堆中,是线程共享数据的,并且栈帧里的命名就已经清楚的划分了界限 : 局部变量表!
变量槽
局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。
对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
Slot复用
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,
也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),
那这个变量对应的Slot就可以交给其他变量使用。
优点 : 节省栈帧空间。
缺点 : 影响到系统的垃圾收集行为。
(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)
实例对象引用
堆
1.所有线程共享的一块区域,虚拟机启动的时候创建,虚拟机管理内存中最大的一块。
此区域唯一的目的就是存放 对象实例和数组,几乎所有的对象实例和数据都在这里分配(栈上也可以内存分配)
2. 垃圾收集器管理的主要区域
3. 存在内存泄露(指程序在申请内存后,无法释放已申请的内存空间)和内存溢出(程序在申请内存时,没有足够的内存空间供其使用)问题
方法区
1.和堆一样线程共享,用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2.也成为永久代
3.JDK 1.7+ 运行时常量池已移走
4.JDK 1.8 已经没有了方法区,改为元空间
5.存在OOM 异常
运行时常量
1.jdk 1.7 移到堆中
直接内存
JVM垃圾回收
-
JVM 内存分配与回收
-
堆空间的基本结构
-
上图所示的 eden 区、s0(“From”) 区、s1(“To”) 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1(“To”),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
来设置。经过这次GC后,Eden区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到年老代中。 -
-
大对象直接进入老年代:数组、字符串…为了避免对象分配空间时由于分配担保机制带来的复制而降低效率
-
长期存活的对象:经过多次MinorGC 年龄增加到一定的数值(默认是15),就会进入老年代
2.判断对象是否死亡
-
-
引用计数器
- 每当有一个地方引用就+1,引用失效就-1,任何时候计数器为0的对象就是不可能被使用
- 不能解决循环依赖问题
-
可达性分析算法
- 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的
-
引用
- 强引用:实际中使用的引用,必不可少。垃圾回收不会回收
- 软引用:可有可无,内存足够的时候不会回收,内存不足的时候就会回收,然后将引用加入引用队列(可加速JVM对来及内存回收的速度)
- 软引用:可有可无,不管内存够不够都会回收,然后将引用加入引用队列
- 虚引用:和引用队列联合使用,不能决定对象的生命周期
-
不可达对象并非 “非死不可”
- 对象真正死亡需要两次标记过程
- 第一次进行筛选判断对象是否有必要finalize ,当对象没有覆盖finalize方法,或finalize 已经被虚拟机调用过,则认为没有必要执行
- 被判断需要执行的对象会被放在一个队列中进行二次标记,除非该对象和引用连上的任何一个对象建立联系,否则就会被回收
-
判断常量是废弃常量
- 如果没有任何对象引用该常量,就说明是一个废弃常量
-
判断类是一个无用类
-
该类的所有实例都被回收,java 堆中不存在任何该类的任何实例
-
加载该类的classLoader 已经被回收
-
该类对应的java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-
-
3.垃圾回收算法
-
标记-清除算法
-
存在问题:效率问题,空间问题(会产生不连续碎片)
-
-
-
复制算法
- 为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收
-
标记-整理算法
- 根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法
- 在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
-
面试
1.详细介绍JVM 内存模型
根据JVM 规范,JVM 内存共分为 方法区、堆、虚拟机栈、本地方法栈、程序计数器 五个部分。
JDK 1.7 对方法区进行了修改,将运行时常量移动到堆中。
JDK 1.8 将方法区移除,改成元空间
元空间不再java 虚拟机中,而是使用直接内存
2 java内存模型
3 内存泄露和内存溢出
内存泄露 (指程序在申请内存后,无法释放已申请的内存空间)
内存溢出 (程序在申请内存时,没有足够的内存空间供其使用)
4 线程栈
JVM 规范每个java 线程拥有自己独立JVM 栈,也就是java方法的调用栈,当方法调用的时候,会生成一个栈帧。栈帧里面保存了局部变量表,操作数栈,动态链接、方法返回地址等信息。线程运行过程中,只有一个栈帧处于活跃状态称为“当前活跃栈帧”,当前活动栈帧是虚拟机栈的栈顶元素。
5 jvm 年轻代到年老代过程的判断条件
- 年轻代回收次数超过15次还未回收掉的对象。
- 如果对象的大小大于Eden 去的一半会直接分配到年老代,如果old 放不下就进行major GC,如果小于Eden 区的一半但是没有空间则尽心minor GC
- minor GC 后 survivor 放不下则放入 old