一. 内存模型详解
1. 内存模型
2. 元数据空间组成
堆 | 初始化的对象,成员变量 (那种非 static 的变量)所有的对象实例和数组都要在堆上分配 | 新生代 (1/3) | Eden (伊甸)区 | 用来分配新的对象内存 |
Survivor 区 | from 区用于GC时的复制. | |||
to 区 | ||||
老年代(2/3) | ||||
方法区/ 元空间 | 主要是存储类信息,常量池( static 常量和 static 变量)编译后的代码(字节码)等数据 | 永久代--->Metaspace(Java8 )Metaspace:与堆不相连的本地内存 | ||
栈(线程栈) | 栈的结构是栈帧组成的,每个方法在执行时就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针 | |||
本地方 法栈 | 主要为 Native 方法服务 | |||
程序 计数器 | 记录当前线程执行的行 |
1.2. 内存模型
2.堆工作过程
3. 堆工作过程
(1) 为什么需要两个Survivor区呢?
因为复制后Survivor from区虽然现在很整齐,没有碎片,当下一次进行回收时,Eden区和Survivor from区里都存在需要回收的对象,则Survivor from区也会出现碎片。
(2) 工作过程
1. 新创建的对象会被放在Eden区;对于大对象直接进入老年代,实际上是为了保证Eden区具有充足的空间可用的一种策略,采用-XX:PretenureSizeThreshold参数可以设置多大的对象可以直接进入老年代内存区域
2. 当Eden区中已使用的空间达到一定比例,会进行一次Minor GC操作,将Eden区进行回收,此时判断存活的对象会被复制进入Survivor from区(年龄加1)
3. from区快满的时候,会将仍然在使用的对象转移到to区(年龄加1);to区进行GC回收时候仍然在使用的对象再转移到from区(年龄加1)
4 对于长期存活的对象直接进入老年代,实际上时对Eden区到Survivor区过度的一种策略,是为了保证Eden区到Survivor区不会频繁的进行复制一直存活的对象且对Survivor区也能保证不会具有太多的一直占据的内存,采用-XX:MaxTenuringThreshold=数字 参数可以设置对象在经过多少次GC后会被放入老年代(年龄达到设置值,默认为15)。
5.当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区
二、 垃圾回收机制
1.如和判断一个对象是否存活?/ GC 的两种判定方法:
引用计数法: 指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就会回收但是 JVM 没有用这种方式,因为无法判定相互循环引用( A 引用 B,B 引用 A)的情况
可达性算法(引用链法):从一个被称为 GC Roots 的对象开始向下搜索,找到的对象标记为“非垃圾对象”,如果一个对象到 GCRoots 没有任何引用链相连时,则说明此对象不可用。
(1)虚拟机栈中引用的对象; (2)方法区类静态属性引用的对象; (3)方法区常量池引用的对象; (4)本地方法栈引用的对象
4) 被判定为回收的对象是否就一定被回收?
当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记:
(1)如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。
(2)GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
5) SafePoint
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC,
(1) 循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入safepoint)
(2) 方法返回前
(3) 调用方法的 call 之后
(4) 抛出异常的位置
6)java中垃圾收集方法
标记清除:标记完毕之后再清除,效率不高,会产生碎片
标记整理:标记完毕之后,让所有存活的对象向一端移动
复制策略:将原来存在的内存分为两个相等的区,使用一块进行新生代的内存分配,当要GC时,则将存活的对象复制进入另一块空闲的内存,然后将使用的内存进行清除,从而又有一个空闲区和一个使用区,并且不会有碎片问题。
分代收集:现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,可以使用标记-整理 或者 标记-清除。
3.GC 收集器有哪些? CMS 收集器与 G1 收集器的特点。
并行收集器:串行收集器使用一个单独的线程进行收集, GC 时服务有停顿时间
串行收集器:次要回收中使用多线程来执行
CMS收集器:是基于“标记—清除”算法实现的,经过多次标记才会被清除 ;
G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间上来看是基于“复制”算法实现的
4. 几种常用的内存调试工具
jstack 可以看当前栈的情况, jmap 查看内存, jhat 进行 dump 堆的信息
5. 类加载
1)类加载的几个过程:
(1)装载
查找并加载java类的二进制数据(.class文件)
(2)链接
a.验证:验证字节码的准确性,防止高手修改.class文件,确保加载类正确性
b.准备:将静态变量分配内存,并将其初始化为默认值
c.解析:将类中的符号引用转换成直接引用
(3)初始化
a.创建类实例(new)
b.访问某个类或接口的静态变量,并将类的静态变量赋予正确的初始值
c.调用类的静态方法
d.反射
e.初始化一个类的子类(首先初始化父类)
f.JVM启动时标明的启动类,文件名和类名相同的那个类
2)类加载器分类
(1)根类加载器(Bootstrap classLoader):负责加载lib下的核心类库
(2)扩展加载器(ExtClassLoader):负责加载lib目录下的ext的jar类包
(3)应用加载器(AppClassLoader):负责加载ClassPath路劲下的类包(自定义的类)
(4)自定义类加载器:继承ClassLoader,重写loadClass(),findClass(),一般是只需要重写findClass
3)类加载器双亲委派机制
(1.保证类的唯一性 2、沙箱安全机制)
a.先检查指定的类是否已经加载过了,若已经加载过,则直接返回加载的类
b.若没有加载,则判断有没有父类,有的话则调用父类加载器,或者调用根类加载器加载。
c.若父类加载器与根类加载器都没有找到指定的类,则调用自身的findClass来完成类加载
4)JVM怎样判断两个类是否相同
java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的
6.java 内存模型