文章来源:微信阅读--Offer来了
Java类加载过程(*)
Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。
1)Loading(载入)
JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并在堆中创建生成一个代表该类的 java.lang.Class 对象。
2)Verification(验证)
JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。
- 文件格式的验证:确保二进制字节流格式符合class文件的格式规范。
- 元数据验证:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的对数据类型做出验证,
- 符号引用验证:
3)Preparation(准备)
JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
4)Resolution(解析)
该阶段将常量池中的符号引用转化为直接引用。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。
5)Initialization(初始化)
该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
使用Class.forName加载指定的类时,可以通过initialize参数设置是否需要对类进行初始化
子类初始化过程:
1)父类的静态属性
2)父类的静态代码块
3)子类的静态属性
4)子类的静态代码块
5)父类的非静态属性
6)父类的非静态代码块
7)父类构造方法
8)子类非静态属性
9)子类非静态代码块
10)子类构造方法
Java 类加载器可以分为三种。
1)启动类加载器(Bootstrap Class-Loader),加载Java_HOME/lib 包下面的 jar 文件,比如说常见的 rt.jar。
2)扩展类加载器(Extension or Ext Class-Loader),加载Java_HOME/lib/ext 包下面的 jar 文件。
3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。
4)自定义加载器(User ClassLoader):继承java.lang.ClassLoader
双亲委派模型:双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。核心是保障类的唯一性和安全性
跨平台原理
Java源文件(.java文件) ----编译----> 通用的Java字节码(.class文件) ----JVM翻译---->平台相关的机器码 ---调用本地方法库---> 执行相应方法
每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,虚拟机识别平台并将字节码转换成机器码,这也是Java能够跨平台的原因。
JVM内存模型
1. 线程私有:
- 栈(Stack): 基本数据类型变量,对象的引用,局部变量
- 程序计数器:指令地址,线程启动时创造一个PC寄存器
- 本地方法区:保存本地(native)方法的地址
2. 线程共有:
- 堆(Heap,是完全二叉树):类的对象,new开辟,GC回收。
逻辑上分成新生代(1/3)+老年代(2/3)+永久代(极少区域),
- 方法区(Method Area):类信息,静态变量,常量,成员方法、包含运行时常量池和字符串常量池,
3. 直接内存(堆外内存):
JDK的NIO模块提供的基于Channel与Buffer的I/O操作方式
在高并发应用场景下被广泛使用(Netty、Flink、HBase、Hadoop都有用到堆外内存)
GC的内存区域
堆+方法区
JVM用Java堆的永久代来实现方法区,便于垃圾收集器用管理堆的方式管理方法区的回收
GC过程
新生代的MinorGC:
新生代 = Eden (8/10) + SurvivorFrom(1/10) + SurvivorTo(1/10)
Eden区:Java新创建的对象首先会被存放在Eden区,大对象则直接将其分配到老年代。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收。
ServivorTo区:保留上一次MinorGC时的幸存者。
ServivorFrom区:复制算法的过渡区,将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者
新生代的GC过程叫作MinorGC,采用复制算法实现,具体过程如下。
(1)把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由XX:MaxTenuringThreshold设置,默认为15),则将其复制到老年代,同时把这些对象的年龄加1;如果ServivorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(大小为2KB~128KB的对象属于大对象,例如通过XX:PretenureSizeThreshold=2097152设置大对象为2MB,1024×1024×2Byte=2097152Byte=2MB),则也直接将其复制到老年代。
(2)清空Eden区和ServivorFrom区中的对象。
(3)将ServivorTo区和ServivorFrom区互换,原来的ServivorTo区成为下一次GC时的ServivorFrom区。
老年代的MajorGC:
主要存放有长生命周期(年龄大于15)的对象和大对象
JVM会进行一次MinorGC,再进行MajorGC,MajorGC标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out OfMemory异常。
永久代:
内存的永久保存区域,主要存放Class和Meta(元数据)的信息。GC不会在程序运行期间对永久代的内存进行清理,会因加载的Class文件过多时会抛出Out Of Memory异常。
Java 8中永久代已经被元数据区(也叫作元空间)取代。元空间直接使用操作系统的本地内存,不再受JVM的最大可用内存(MaxPermSize)空间决定,由操作系统的实际可用内存空间决定。
GC的对象判定
(判断一个对象是否存活,是否该回收)
1. 引用计数:每个对象有一个引用计数属性,计数为0时可以回收。无法解决对象相互循环引用问题
2. 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象要经过至少两次标记才能判定其是否可以被回收。
GC算法
1、 标记-清除算法(Mark-Sweep):为每个对象存储一个标记位,在标记阶段,计数为0时记录死亡;清除阶段,该阶段对死亡的对象进行清除,。效率比较低(递归与全堆对象遍历)、碎片多。
2、 标记-压缩法(Mark-Compact):在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;清除阶段,将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。减少碎片,但复制操作耗时。
3.、复制算法(Copying):将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。长时间存活的对象来回复制耗时,适用于小对象、生存时间短的区域。
4. 分代收集(Generational Collecting):
新生代:对象生存期短,每次回收都会有大量对象死去,采用复制算法。
老年代:对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
垃圾收集器
内存回收算法的具体实现
1、 Serial收集器:串行单线程,新生代复制算法、老年代标记-压缩
2、 ParNew收集器:并行版本Serial收集器
3、 Parallel Scavenge收集器:通过自适应调节策略提高系统吞吐量,三个重要参数:控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,控制吞吐量大小的-XX:GCTimeRatio参数和控制自适应调节策略开启与否的UseAdaptiveSizePolicy参数
4、 CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
(1)初始标记:只标记和GC Roots直接关联的对象,速度很快,需要暂停所有工作线程。
(2)并发标记:和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程。
(3)重新标记:在并发标记过程中用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正确性,需要对其重新标记并暂停工作线程。
(4)并发清除:和用户线程一起工作,执行清除GC Roots不可达对象的任务,不需要暂停工作线程。
5、 G1收集器: 前沿成果,标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。
CMS收集器使用“标记-清除”算法,是老年代的收集器,以最小的停顿时间为目标的收集器。
G1收集器使用的是“标记-整理”算法,范围是老年代和新生代,进行了空间整合,降低了内存空间碎片。
对象的引用类型
一切皆对象,对象的操作是通过该对象的引用(Reference)实现的,
Java中的引用类型有4种,分别为强引用、软引用、弱引用和虚引用
(1)强引用:最常见。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成Java内存泄漏(Memory Link)的主要原因。
(2)软引用:软引用通过SoftReference类实现。如果一个对象只有软引用,则在系统内存空间不足时该对象将被回收。
(3)弱引用:弱引用通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。
(4)虚引用:虚引用通过PhantomReference类实现,虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。