0. 概念
0.1 JVM的组成
JVM的组成大体上分九个:
1)类加载子系统:编译.Class文件
2)运行时内部区域:存储编译的内容
3)垃圾回收系统:对堆里面无用的对象进行清除。
4)执行引擎:负责执行虚拟机的字节码
1. Java内存区域详解
1.1 运行时数据内存
线程私有的:
- 程序计数器:用来存储下一条指令的地址,使得线程之间跳转有迹可循。
- 虚拟机栈:主管Java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。【调用方法,进栈。执行结束后出栈。】【不参与垃圾回收】
- 本地方法栈:管理本地方法的调用,不是java方法。
线程共享的: - 堆:只存在一个堆内存,所有的对象实例都应当在运行时配对在堆上。堆也是Java内存管理的核心区域。【新生区(新生代,Eden和Survivor)+老年区(老年代)+持久区】
- 方法区:方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息。
1.2 HotSpot虚拟机对象探秘
1.2.1 实例对象的创建过程(全文默写)
- Step1:类加载检查:
虚拟机遇到一条 new 指令时,查看这个对象锁代表的类是否已经被加载过;定位类的符号引用再进行查找。 - Step2:分配内存:
为新生对象分配内存,包括“指针碰撞(规整)”和“空闲列表(不规整)”两种。区别是堆内存是否规整,规整与否又与垃圾回收器选择有关(标记复制和整理是规整的,标记清除是不规整的)
1)虚拟机在分配内存如何保证线程安全方法
(1) CAS+失败重试:如果冲突,那么就不断重试,保证更新操作的原子性。
(2)TLAB:为每一个线程在Eden区分配一个固定内存,当该内存不够时,才使用CAS+失败重试机制。 - Step3:初始化零值:
将分配到的内存空间都初始化为零值(不包括对象头)【实例数据+内存填充】 - Step4:设置对象头基本数据填充
1)包括:哈希码,类的元数据信息,对象的GC分代年龄;锁状态信息;类型指针 - Step5:执行 init 方法:JVM对象已经创建成功了。但是对程序员自己些的变量复制进行基本操作。
1.2.2 Java对象的内存布局
在 JVM 中,Java对象保存在堆中时,由以下三部分组成:
- 对象头(object header):第一部分用于存储对象自身运行的数据(哈希码,GC分代年龄,锁状态标志),另一部分是类型指针,表明对象是那个类创建的。
- 实例数据(Instance Data):真正存储的有效数据。主要是存放类的数据信息,父类的信息,对象字段属性信息。
- 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。
补充
1)为什么需要对齐填充:让字段只出现在同一CPU的缓存行中,避免跨缓存行
2. JVM垃圾回收详解
2.1 垃圾回收对象
主要是堆区中的类无用对象,其次是方法区中的参数。
2.2 对象已经死亡
- 引用计数法(有一个引用就加一,取消一个引用就减一,当引用为0时清除)(存在相互引用问题)
- 可达性分析算法:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,如果没有引用链,那么对象是不可用的。(排除相互引用问题)
栈内存中引用的对象
2.3 引用的种类
- 强引用:必不可少的生活用品,垃圾回收器绝不会回收它
- 软引用:可有可无的生活用品
- 弱引用:只具有弱引用的对象拥有更短暂的生命周期,不管内存是否足够,都会回收。
- 虚引用:那么它就和没有任何引用一样,随时可能被垃圾回收
2.4 如何判断常量和类是无用的?
1. 无用的类:
1)该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2)加载该类的 ClassLoader 已经被回收。
3)该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2. 废弃的常量:
1)如果当前没有任何 String 对象引用该字符串常量的话
2.5 堆内存的结构
Java把堆内存分为三大部分:新生代,老年代和永久代。其中新生代进一步划分为Eden,S0和S1.
2.5 堆内存对象的分配与回收
- 创建对象会优先在Eden里创建,
- 进入老年代的方法?
如果是大对象,直接进入老年代;如果对象存活时间达到临界值,进入老年代。 - 垃圾回收的区域?
1)新生代区域发生的Minor GC:比较频繁,速度很快
2)老年代区域发生的Full GC:很少发生,在发生时伴随一次Minor GC。 - 为什么新生代有survivor?
为了避免存活过一次的对象直接进入老年代,在survivor - 为什么新生代有两个S?
当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的。只有一个的话,每次只能利用一般空间。
2.5 垃圾回收算法(理论)
- 标记清除算法:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象
- 标记复制算法:当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
- 标记-整理算法:首先标记出所有不需要回收的对象,让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法:根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,以根据各个年代的特点选择合适的垃圾收集算法。(新生代主要用标记复制,老生代主要用标记-清除”或“标记-整理)。
2.6 垃圾回收器(实践)
没有最好的垃圾回收器,只有在不同的环境下最好用的。
- Serial 收集器:最基本,最悠久,单线程
1)单线程:一条垃圾收集线程,暂停其他所有工作线程。
2)分代垃圾回收算法:新生代采用标记复制;老生代采用标记整理
3)优点:简单高效,没有线程交互的开销。客户端Client端的选择
- ParNew 收集器:Serial 收集器的多线程版本
1)对比:就是serial的多线程模式,其他一样(收集算法,回收策略)
2)优点:服务端Server端的选择
- Parallel Scavenge 收集器( JDK1.8 默认收集器之一): 与ParNew很像,多线程
1) 与ParNew区别 :关注点是吞吐量(性能),通过自适应调节策略达到最合适的调节时间和最大的吞吐量。
2)垃圾收集算法:新生代采用标记-复制算法,老年代采用标记-整理算法。 - Serial Old 收集器:Serial 收集器的老年代版本
1)与 Parallel Scavenge 收集器搭配使用
2)作为 CMS 收集器的后备方案。 - Parallel Old 收集器( JDK1.8 默认收集器之一):Parallel Scavenge 收集器的老年代版本
- CMS 收集器
1)特点:最短回收停顿时间为目标,用户体验感好!真正意义上的并发,可以和用户线程同时运行
2)收集算法:标记-清除”算法
(1)初始标记:暂停,记录与root相连对象
(2)并发标记:开启 GC 和用户线程
(3)重新标记
(4)并发清除
3)优缺点:真正的并发收集,低停顿;对CPU资源敏感;无法处理浮动垃圾;产生大量的空间碎片 - G1 收集器:面向服务器的垃圾收集器,高吞吐量和低停顿时间
1)并行与并发
2)与CMS区别:不需要其他收集器配合,不是标记清除算法,而是分代收集(新生代标记复制;老生代标记整理);将堆划分为若干个区间,避免进行全区域垃圾收集。
3)优点:可预测的停顿时间。
2.7 与垃圾回收时间有关的两个函数
1) System.gc()方法:请求Java的垃圾回收,只是请求,不是马上进行回收
2) finalize()方法:在进行垃圾回收之前调用的方法,如果执行方法后,对象还是没有被引用,则处于不可达状态,会被回收。
面试题目:
jvm垃圾回收:自动回收,是默认调用了什么方法?
finalize()方法(在进行垃圾回收之前调用的方法,如果执行方法后,对象还是没有被引用,则处于不可达状态,会被回收)
System.gc()方法:默认调用,之后等待回收
3. 类加载机制
- 加载:将class文件加载到内存
- 连接-验证:安全检查,确保加载的类符合 JVM 规范和安全。
- 连接-准备:为static变量在方法区中分配内存空间,设置变量的初始值
- 连接-解析:虚拟机将常量池内的符号引用替换为直接引用的过程
- 初始化:执行类构造器方法的 **clinit()**的过程(分为静态资源和非静态资源)
- 使用:使用所加载好的类
- 卸载:GC将无用对象从内存中卸载‘
clinit()与linit()的区别
1)init 用于初始化对象实例的构造方法,
2)clinit 用于初始化类对象本身 . 例如,当类加载并初始化时,在 中完成任何 static 类级别字段的初始化 .
4. 类加载器
- 总共有四类类加载器:(大中小)
1)BootstrapClassLoader(启动类加载器) :%JAVA_HOME%/lib,加载 Java 的核心库
2)ExtensionClassLoader(扩展类加载器) :%JRE_HOME%/lib/ext 目录下,加载 Java 的扩展库
3)AppClassLoader(应用程序类加载器) :面向我们用户的加载器,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类
4)用户自定义类加载器 - 作用:
通过类名来获取二进制字节流。主要分为四种加载器,启动类->扩展类->应用类->自定义类。我也知道双亲委派机制的好处,就是越基础的类交给越高级的加载器加载。 - 注意要点:子类加载器可以获取父类加载器加载的类,反之则不行。
5. 双亲委派机制
1)AppClassLoader的父类加载器为ExtClassLoader,
2) ExtClassLoader的父类加载器为 null,
3)null 并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader
5.1 什么是双亲委派机制
1)保证JVM的核心类和用户的类都能得到正常加载
2)向上委派检查:当类加载器加载一个类时,首先会向他的父类加载器进行委派,直到启动类加载器。
3)向下委派加载:当启动类加载器发现自己无法加载这个类时,会委派给他的子类加载器进行加载,如果直到最后一个子类还不能加载,就抛出ClassNotFound异常。
5.2 为什么需要双亲委派机制
1)为了防止内存中存在多份同样的字节码(安全),即避免类的重复加载。
2)如果没有双亲委派机制,同一个类可能就会被多个类加载器加载,如此类就可能会被识别为两个不同的类,相互赋值时问题就会出现(相同名字,被不同类加载会变为不同的类)
5.3 如何打破双亲委派机制
1)自定义类加载器,重写loadClass方法(loadClass方法实现了双亲委派机制)
5.4 自定义类加载器
需要继承ClassLoad
5.5 为什么需要多个类加载器
- 首先,是为了区分同名的类:假定存在一个应用服务器,上面部署着许多独立的应用,同时他们拥有许多同名却不同版本的类库。试想,这时候 jvm 该怎么加载这些类同时能尽可能的避免掉类加载时对同名类的差异检测呢?当然是不同的应用都拥有自己独立的类加载器了。
- 其次,是为了更方便的加强类的能力:类加载器可以在 load class 时对 class 进行重写和覆盖,在此期间就可以对类进行功能性的增强。比如添加面向切面编程时用到的动态代理,以及 debug 等原理。怎么样达到仅修改一个类库而不对其他类库产生影响的效果呢?一个比较方便的模式就是每个类库都可以使用独立的类加载器
- 小结:
jvm 需要有不同的类加载器,因为它一方面允许你在一个 jvm 里运行不同的应用程序,另一方面方便你独立的对不同类库进行运行时增强。