1、JVM内存模型
1.1 程序计数器(Program Counter Register)
- 记录每个线程执行位置,线程私有的。
1.2 java虚拟机栈(VM Stack)
- 生命周期同线程同步,线程创建的同时创建一个虚拟机栈,其中存放栈帧,方法调用对应压栈出栈的过程。
- 栈帧分为几个区域:局部变量表、操作数栈、动态链接、方法出口等。
- 线程私有的。
1.3 本地方法栈(Native Method Stack)
- 功能类似java虚拟机栈,调用本地方法服务(native)是使用,线程私有。
1.4 堆(Heap)
- 在虚拟机启动时创建,用于对象创建实例(new Object)时分配内存空间。
- 分为新生代(Young)、老生代(Old),GC垃圾回收主要目标区。
- 非线程私有,线程共享。
1.5 方法区
- 存放虚拟机加载的类元数据信息(元空间)、常量、静态变量等。
- 也可被划分为堆的一个逻辑部分,习惯叫永久代(Perm),很少发生垃圾回收,但常量池回收、类型卸载等可能导致垃圾回收。
- 非线程私有,线程共享。
- Java8中已经没有方法区,取而代之的是元空间(Metaspace)。
1.6 直接内存
- 非JVM规范的定义。
- NIO中的ByteBuffer对象的方法allocateDirect(int capacity),可以使用Native函数库直接分配堆外内存,然后通过Java堆里面的DirectByteBuffer对象引用进行操作。
2、GC垃圾回收
2.1 Java中对象引用类型
- 强引用:Java中普遍的引用关系,如Object o = new Object()。
- 软引用:SoftReference类实现软引用,只在内存不足时,被GC处理。
- 弱引用:WeekReference类实现弱引用,无论内存是否充足,下一次GC时处理。
- 虚引用:PhantomReference类实现,无法通过虚引用获取实例对象,只用于GC时收到系统通知。
2.2 标记阶段算法
2.2.1、引用计数算法(Reference Counting Collector)
- 每个对象都有一个计数器,对象每被引用一次,计数器+1,引用失效时,计数器-1,计数器为0时标记回收。
- 优点:执行简单,判断效率高。
- 缺点:无法检测出循环引用关系。
2.2.2、根搜索法(Tracing Collector)
- 从所有GC Roots对象向下递归查找引用链,不可达对象标记回收。
- 宣告一个对象死亡,至少要经过2次标记。
- 第一次标记后,通过一个低优先级的线程执行对象的finalize()方法,这是最后一次逃脱机会(使对象重新关联到引用链)。
2.3 垃圾回收算法
2.3.1 标记-清除算法
- 分为标记和清除2阶段,通过根搜索法先标记,后统一回收空间。
- 优点:无需进行内存移动,仅对死亡对象进行处理。
- 缺点:(1)效率低,需要额外的空闲列表来记录所有的空闲地址和大小。(2)产生大量内存碎片。
2.3.2 标记-整理算法
- 标记阶段和“标记-清除”算法相同,回收阶段把所有的对象移动到同一端,然后把范围外的空间回收。
- 优点:整理后没有内存碎片,分配新对象空间简单高效。
- 缺点:整理过程中移动对象导致GC占用太多时间。
2.3.3 复制算法
- 把内存分为大小相等的两部分,当A块内存用完,把所有存活对象复制到B块,然后直接回收A块内存。
- 优点:1)标记和复制可同时进行;2)无内存碎片;
- 缺点:浪费一半内存。
- 适合短生存期对象(如:新生代)
2.3.4 分代收集
- 现代虚拟机大多采用这种方式,根据对象生产周期,将堆分成新生代和老生代,新生代对象生存周期端,采用复制算法,老生代使用标记整理 或 标记清除。
2.3.5 Adaptive算法(Adaptive Collector)
- 监测当前堆的使用情况,自动选择合适的算法。
2.4 Java垃圾回收器(GC)
2.4.1 串行垃圾回收器(Serial Garbage Collector)
- 使用单线程执行垃圾回收,同时冻结所有的应用线程(Stop the World,STW)。
- 不适合服务器环境,适合单CPU、暂停时间不敏感、简单的命令行程序。
- 通过-XX:+UseSerialGC可以使用串行垃圾回收器。
2.4.2 并行垃圾回收器(Parallel Garbage Collector)
- 使用多线程执行垃圾回收,同时冻结所有的应用线程。JVM默认使用。
- 优点:使用多线程扫描和压缩堆。
- 缺点:在minor和full GC时都会暂停应用。
- 适合服务器环境,适合多CPU、暂停时间敏感。
- 可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
2.4.3 并发标记扫描垃圾回收器(CMS Garbage Collector)
- 使用多线程,标记并回收内存,特定情况冻结应用线程:
1) 标记的对象在Tenured区。 2)垃圾回收的同时,堆内存数据被并发的改变。 - 相比并行GC,并发使用更多的CPU来确保吞吐量。
- CMS最大的问题是会产生严重的碎片化,只有在出发FullGC时才会进行碎片整理。
- 对比并行GC,会占用更多的CPU。
- 通过-XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
2.4.4 G1收集器
- 内存大于4G时,G1优先考虑使用,它是JDK7u4引入的,G1把堆分成1M到32M的多个区域,使用多个后台线程来扫描,优先扫描最多垃圾的区域(垃圾优先Garbage First),在处理的同时会进行碎片整理。
- 如果在后台线程完成扫描前堆内存耗光,才会进行STW收集。
2.5 堆内存分配
- 对象在堆上分配,主要在Eden上分配,如启用线程本地分配缓存机制,在TLAB上先分配,少数情况也直接分配在老年代中。
- 当Eden满时,发起一次MinorGC(新生代GC),把Eden和S1复制到S2,然后清理Eden和S1(S1和S2是相对的),如果S2不足以存放存活对象,则提前复制到老年代。
- 大对象直接进入老年代,可通过-XX:PretenureSizeThreshold配置,大对象需要连续空间分配,过多的大对象导致频繁GC,应避免。
- 长期存活对象进入老年代,经过多次MinorGC增加年龄,可通过-XX:MaxTenuringThreshold配置,默认为15岁。
- JDK8中,方法区(永久代)被删除,取而代之的是类元数据区(Class Metadata)
1)类元数据直接在本地内存分配,不在JVM中,理论上只受物理内存大小限制,也可通过-XX:MaxMetaspaceSize参数设置,默认无限制。
2)部分数据转移到heap中,如字面量、静态变量
3、类的加载过程
定义:JVM把class文件读取到内存中,经过校验、解析转换、初始化,在方法区和堆上面分配内存,形成可以被JVM使用的类型的过程。
3.1 加载
- 执行动作:
1)类加载器通过类的全路径限定名读取类的二进制字节流。
2)将字节流代表的类结构转化到 JVM方法区 中。
3)在 JVM堆 上生成代表该类的java.lang.Class实例。 - 类加载器
可以使用jvm自带加载器,也可以自定义加载器,不同的加载器从不同地方读取字节流:jar包、class文件、网络流等。
同一个类加载器加载后的同源类,才是同类,instanceof 返回 true. - 类加载的双亲委派模型
类加载器优先使用父加载器来加载,如果没有才自己加载。这样可以保持java类型的一致性,优先使用JDK的类加载器 - 4种类加载器
1)启动类加载器(JVM bootstrap Loader),JVM的一部分,负责JAVA_HOME/lib下的类的加载
2)扩展类加载器(Extension Loader),负责JAVA_HOME/lib/ext和java.ext.dir目录下类加载。
3)应用系统类加载器(Application System Loader),负责加载用户类路径下的类库,如果没有使用自定义的加载器,这就是默认的类加载器了。
4)自定义加载器
3.2 验证
- 加载和验证是交叉执行的,验证内容主要包括:
1)文件格式:符合class文件格式,才能在 方法区 分配内存。
2)元数据:符合java语言规范。
3)字节码:数据流和控制流分析验证。
4)符号引用:符号引用转换为直接引用,对自身以外引用的可访问性验证。
3.3 准备
- 在方法区给static类变量分配内存,并 零值初始化。
3.4 解析
- 常量池内的 符号引用 替换为 直接引用。
3.5 初始化
- 真正开始执行java代码,static静态变量赋值为代码设定的值。
- 只有以下5中主动引用,才会触发初始化的发生,其他类的引用称为被动引用,不会触发初始化:
1)new 对象。
2)访问static字段和static方法。
3)使用反射调用未初始化过的类。
4)初始化子类,需优先初始化父类。
5)启动main方法所在的类时,jvm优先初始化该类。 - 继承情况下的初始化顺序
1)父类的static变量和块,按出现顺序
2)子类的static变量和块,按出现顺序
3)父类构造函数
4)子类构造函数
4、Java对象生命周期
4.1 创建
- 分配空间,构造对象
- 先父类后子类,对static成员初始化。
- 父类成员变量按顺序初始化,递归调用父类构造方法。
- 子类成员变量按顺序初始化,调用子类构造方法。
4.2 应用
- 至少被一个强引用,并且在作用域内。
4.3 不可见
- 程序不再持有对象的强引用,但这些引用还存在着,一般具体是程序的执行超出作用域了。
4.4 不可达
- 不在被任何强引用
- 可能还被某些JVM线程,或jni所有,这些特殊的强引用成为GC Root,容易导致内存泄漏,无法被回收。
4.5 回收
- 对象不可达,GC回收阶段。
- 如果重写了finazlie()方法,可能导致对象复活,尽量不要重写此方法。
4.6 终结
- 对象执行完finazlie()方法后,仍处于不可达状态,进入终结阶段,等待GC回收。
4.7 空间重新分配
- 空间重新被分配,对象彻底消失。