一:基本概念
JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
二:程序的运行过程
- 代码翻译为class文件:Java 源文件—->编译器—->字节码文件
- 通过类加载器装载类:字节码文件—->JVM的解释器—->机器码
- 执行类
- 解释执行
- 编译执行:客户端编译、服务端编译
当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
三:JVM和线程
JVM 允许一个应用并发执行多个线程,Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。
- 当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。
- 当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。
- Java 线程结束,释放原生线程和 Java 线程的所有资源。
- 操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上
系统线程:
线程 | 内容 |
---|---|
虚拟机线程 VMthread | 等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。 |
周期性任务线程 | 负责定时器事件(也就是中断),用来调度周期性操作的执行。 |
GC 线程 | 支持 JVM 中不同的垃圾回收活动 |
编译器线程 | 将字节码动态编译成本地平台相关的机器码 |
信号分发线程 | 接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
四:Java的内存区域
- 线程私有数据区域:生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot JVM 内, 每个线程都与操作系统的本地线程直接映射)
- 线程共享区域:随虚拟机的启动/关闭而创建/销毁。
- 直接内存:并不是JVM运行时数据区的一部分, 但也会被频繁的使用(NIO)
1. 程序计数器(线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器
- 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)
- 正在执行Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
2. 虚拟机栈(线程私有)
描述java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。通过-Xss控制栈的大小
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
3. 本地方法区(线程私有)
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 JVM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
4. 堆(线程共享)
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。在虚拟机启动时创建。物理上不连续、逻辑上连续,使用 -Xms和 -Xmx 控制堆的大小
由于现代 JVM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分:
- 新生代
- Eden 区
- From Survivor 区
- To Survivor 区
- 老年代
5. 方法区/永久代(线程共享)
即永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器
永久代的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小。
有一些应用会动态生成或调用类,如Hibernate,此时需要设置较大的空间来存放类
运行时常量池(Runtime Constant Pool)
方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
五:Java的内存模型JMM
JMM (Java Memory Model)是 Java 内存模型,JMM 定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节,并提供了内置解决方案(hapen-before 原则)及其外部可使用的同步手段(synchronized/volatie 等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性
1. 访问变量的方式
- JMM 规定了所有的变量都存储在主内存(Main Memory)中
- 每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile 变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
2. hapens-before 原则
- 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
- 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatie 规则 volatie 变量的写,先发生于读,这保证了 volatie 变量的可见性,简单的理解就是,volatie 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
- 线程启动规则 线程的 star()方法先于它的每一个动作,即如果线程 A在执行线程 B 的 star 方法之前修改了共享变量的值,那么当线程 B 执行 star方法时,线程 A 对共享变量的修改对线程 B 可见
- 传递性 A 先于 B ,B 先于 C 那么 A 必然先于 C
- 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程 B 终止之前,修改了共享变量,线程 A 从线程 B 的 join 方法成功返回后,线程 B 对共享变量的修改将对线程 A 可见。
- 线程中断规则 对线程 interupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interupted()方法检测线程是否中断
- 对象终结规则 对象的构造函数执行,结束先于 finalize()方法
3. JMM 特点
- 在现代计算机平台中保证程序可以正确性的执行,但是不同的平台实现是不同的
- 编译器中生成的指令顺序,可以与源代码中的顺序不同
- 编译器可以把变量保存在寄存器中,而不是内存中
- 处理器可以采用乱序或并行等方式执行指令
- 缓存可能改变将写入变量提交到主内存的次序
- 保存在处理器本地缓存的值,对其他处理器不可见
六:JVM的运行时内存
1. 新生代 New
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
- 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC
- Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收
- Servivor From :上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
- Servivor To:保留了一次 MinorGC 过程中的幸存者。
MinorGC的过程(复制->清空->互换)
采用复制算法
- eden、Servivor From 复制到 Servivor To,年龄+1
- 动态对象年龄判定:如果在 Survior 空间中相同年龄所有对象大小总和大于 Survior 空间的一半,(比如说Survior 空间大小为 1M,而有两个年龄为 1 的对象大小和是大于 512K 的),那么年龄大于等于该年龄的对象都可以直接进入到老年代。
- 如果Servivor To 不够位置,则存活对象复制到老年代区
- 在Eden 区中所有存活的对象,都会被复制到“Servivor To”
- 在“Servivor From”区中,仍存活的对象会根据他们的年龄值来决定去向。
- 如果有对象的年龄以及达到了老年的标准(默认15,通过虚拟机参数 -XX:MaxTenuringThreshold设置),则复制到老年代区。
- 否则,复制到“Servivor To”,同时把这些对象的年龄+1。
- 清空 eden、Servivor From
- Servivor To 和 Servivor From 互换,原 Servivor To 成为下一次 GC 时的 Servivor From区
在进行 MinorGC 前,虚拟机会查看 HandlePromotinFailure 设置值是否为 True,即是否允许担保失败(会检查虚拟机老年代剩余空间的大小与平均晋升到老年代空间的大小,如果大于说明“可能”是安全的)
- 为 True时可以容忍内存分配失败: 那么只进行一次 MinorGC,如果此时刻发现进入到老年代的新对象的大小是大于老年代的剩余空间,说明担保失败了,只能进行一次 FulGC 清除老年代的剩余空间。
- 为false时不可以容许:触发 MinorGC 就会同时触发 Ful GC,哪怕老年代还有很多
内存
2. 老年代 Tenured
主要存放应用程序中生命周期长的内存对象。(虚拟机的参数 -XX:PretenureSizeThreshold 可以指定大于这个值的对象直接在老年代分配)
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
MajorGC 的过程
采用标记清除算法
- 扫描一次所有老年代,标记出存活的对象
- 回收没有标记的对象。
MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
3. 永久代 Perm
内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域。
永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。HotSpot 提供-Xnoclassgc 进行控制
它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
清理方式
- 常量池中的常量:没有引用了就可以被回收
- 无用的类:
- 类的所有实例都已经被回收
- 加载类的 ClassLoader 已经被回收
- 类对象的 Class 对象没有被引用(即没有通过反射引用该类的地方)
Java8的元数据区
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
七:垃圾回收算法
1. 判断垃圾
JAVA 四种引用类型:
Java引用类型 | 作用 |
---|---|
强引用 | 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。 |
软引用 | 需要SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。 |
弱引用 | 需要WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。 |
虚引用 | 需要PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用,主要作用是跟踪对象被垃圾回收的状态。 |
(1)引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。
简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
(2)可达性分析
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
GCRoots:
- 虚拟机栈引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
2. 垃圾回收
(1)标记清除算法(Mark-Sweep)
分为两个阶段,标注和清除。
- 标记阶段标记出所有需要回收的对象
- 清除阶段回收被标记的对象所占用的空间
特点:
- 内存碎片化严重
- 后续可能发生大对象不能找到可利用空间的问题,提前触发垃圾回收
(2)复制算法(copying)
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
特点:
- 实现简单,内存效率高,不易产生碎片
- 可用内存被压缩到了原本的一半。且存活对象增多的话效率会大大降低
(3)标记整理算法(Mark-Compact)
结合了以上两个算法
- 标记阶段和 Mark-Sweep 算法相同
- 标记后不是清理对象,而是将存活对象移向内存的一端
- 清除端边界外的对象
(4)分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的区域,根据不同区域选择不同的算法
- 一般情况下将 GC 堆划分为老生代和新生代
- 老生代的特点是每次垃圾回收时只有少量对象需要被回收,采用标记-整理算法
- 新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,即要复制的操作比较少,采用复制算法
- 处于方法区的永生代,它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
(5)分区收集算法
将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
(6)并行算法与并发算法
并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。
- 并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代
- 并行算法吞吐量相对较高
八:GC垃圾收集器
1. Serial 垃圾收集器(单线程、复制算法)
Serial是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾
收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率
Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
2. ParNew 垃圾收集器(Serial+多线程)
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
3. Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器
它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务。
主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
4. Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:
- 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
- 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:
新生代 Parallel Scavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:
5. Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:
6. CMS 收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
- 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
7. G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
九:类加载机制
1. 过程
(1)加载
- 通过一个类的全限名来获取定义此类的字节流,实现这个代码模块就是类加载器
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
(2)验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证
- 是否以魔数 0xCAFEBAE 开头
- 主次版本号是否在当前虚拟机处理范围之内
- 常量池中的常量是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANTUtf8info 型的常量中是否有不符合 UTF8 编码的数据
- Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息
- 元数据验证
- 这个类是否有父类
- 这个类的父类是否继承了不准许被继承的类
- 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法
- 类中的字段方法是否与父类产生矛盾
- 字节码验证
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的
- 符号引用验证
- 符号引用中通过字符串描述的全限定名是否找到相应的类
- 在指定的类中是否存在符合方法的字段描述符以及简单名称说描述的方法和字段
- 符号引用中的类、字段、方法的访问性是否被当前类访问
(3)准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
//在准备阶段过后v的初始值为0,程序被编译后,存放于类构造器<client>方法之中的指令再将v赋值为8080
public static int v = 8080;
//编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据 ConstantValue 属性将v赋值为8080
public static final int v = 8080;
(4)解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
- 符号引用:符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的(符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中)
- 直接引用:直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
(5)初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了这个阶段,才开始真正执行类中定义的 Java 程序代码。
< client >方法
初始化阶段是执行类构造器< client >方法的过程。
< client >方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子< client >方法执行之前,父类的< client >方法已经执行完毕,
如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成
< client >()方法。
特殊情况
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
(6)举例
参考博客:Java基础-对象的内存分配与初始化(一定要明白的干货)
public class Demo_Student {
public static void main(String[] args) {
Student s = new Student();
s.show();
}
}
class Student {
private String name = ”张三”;
private int age = 23;
public Student() {
name = “李四”;
age = 24;
}
public void show() {
System.out.println(“我叫:”+name+”,今年”+age+”岁”);
}
}
-
程序运行时,会将 Demo_Student 类加载进内存的方法区
-
随后,其主方法main入栈。
-
紧接着发现了new Student(),所以又将Student加载进内存
- 首先查看类的符号引用,看是否已经在常量池中,在说明已经加载过了,不在
的话需要进行类的加载,验证,准备,解析,初始化的过程 - 不存在的话,则需要加载类Student到方法区
- 首先查看类的符号引用,看是否已经在常量池中,在说明已经加载过了,不在
-
然后在栈内存分配一块空间(Student s),声明Student的引用。
-
new Studetn() 在线程的私有空间去分配空间,如果空间不足则在堆内存开辟空间。进行默认初始化和显示初始化
-
有构造代码块就先执行构造代码块,如果没有则忽略
-
调用构造方法,系统默认调用。构造方法进栈,对对象进行初始化
-
初始化完成后,弹栈。此时对象已经创建完毕。将其地址值赋值给变量s
-
调用show方法时,show进栈,其内部有个隐藏的this引用,根据该引用找到堆内存实体,并打印相应内容
-
随后main方法也执行完毕,弹栈,程序执行完毕
2. 类加载器
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库
JVM 通过双亲委派模型进行类的加载,当然也可以通过继承 java.lang.ClassLoader实现自定义的类加载器
双亲委派模型
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
特点:不管是哪个加载器加载XX类,最终都是委托给顶层的启动类加载器进行加载。保证了使用不同的类加载器最终得到的都是同样一个XX对象。
破坏双亲委派模型
- 在 JDK1.2 之前,用户去继承 jav.lang.ClasLoader 的唯一目的就是为了重写 loadClass方法,由于用户自己重写了 loadClass,那么也就是用户自己去自定义加载类,造成破坏
- JDBC,JDNI 等的 SPI 的加载都是父类的加载器去请求子类的加载器去加载类
- OSGI 的热部署就是自定义类加载器机制的实现;
动态模型系统OSGI (Open Service Gateway Initiative)
是 Java 动态化模块化系统的一系列规范。
-
动态改变构造:OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGi 技术提供一种面向服务的架构,它能使这些组件动态地发现对方。
-
模块化编程与热插拔:基于 OSGi 的程序很可能可以实现模块级的热插拔功能,当程序升级更新时,可以只停用、重新安装然后启动程序的其中一部分
特点:在提供强大功能同时,也引入了额外的复杂度,它不遵守类加载的双亲委托模型
十:内存泄露
1. 内存溢出 OutOfMemoryError
- 堆上没有内存可完成实例分配,并且堆无法扩展
- 方法区(及其常量池)无法满足内存分配需求
- 虚拟机栈(本地方法栈)扩展时无法申请到足够内存
时机:并不是内存被耗空的时候才抛出
- JVM98%的时间都花费在内存回收
- 每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。
2. 内存泄露
- 代码原因:程序动态分配了内存,但在程序结束时没有释放这部分内存,导致部分内存不可用
- java本身:当被分配的对象可达,但是已经没有作用了
现象:
- 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s(存活对象多增加了复制量,导致时间延长)
- FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC(内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,导致频繁垃圾回收)
- 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放(年轻代的内存无法被回收,越来越多地被Copy到年老代)
区别:
- 内存泄露是导致内存溢出的原因之一
- 内存泄露可以通过完善代码避免,内存溢出可以通过调整配置尽量减少,但是无法避免
性能监测分析工具:
- MemoryAnalyzer:功能丰富的Java堆转储文件分析工具
- EclipseMAT:开源的Java内存分析工具,可以查找大块内存和其使用者
- JProbe
- JProfile
- Optimizeit Profiler
jstat
jmap
避免:
- 尽早释放无用对象的引用
- 使用引用变量时,让引用变量在退出活动域后自动设置为null,暗示垃圾收集器可以收集对象,防止内存泄露
- 避免使用String,会独立占用一块内存,应该使用StringBuffer
3. 调优
调优思路:
- 线程池:解决用户响应时间长的问题
- 连接池
- JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
- 程序算法:改进程序逻辑算法提高性能
(1)线程池
大多数JVM6上的应用采用的线程池都是JDK自带的线程池,线程池的设计思路是,任务应该放到Queue中,当Queue放不下时再考虑用新线程处理,如果Queue满且无法派生新线程,就拒绝该任务。
设计导致“先放等执行”、“放不下再执行”、“拒绝不等待”。所以,根据不同的Queue参数,要提高吞吐量不能一味地增大maximumPoolSize。
因此,必须对线程池进行一定的封装:
- 以SynchronousQueue作为参数,使maximumPoolSize发挥作用,以防止线程被无限制的分配,同时可以通过提高maximumPoolSize来提高系统吞吐量
- 自定义一个RejectedExecutionHandler,当线程数超过maximumPoolSize时进行处理,处理方式为隔一段时间检查线程池是否可以执行新Task,如果可以把拒绝的Task重新放入到线程池,检查的时间依赖keepAliveTime的大小。
(2)连接池(org.apache.commons.dbcp.BasicDataSource)
当访问量大时,通过JMX观察到很多Tomcat线程都阻塞在BasicDataSource使用的Apache ObjectPool的锁上,因为BasicDataSource连接池的最大连接数设置的太小
- Mysql默认支持100个链接,所以每个连接池的配置要根据集群中的机器数进行
- initialSize:一直打开的连接数。BasicDataSource会关闭所有超期的连接,然后再打开initialSize数量的连接,这个特性与minEvictableIdleTimeMillis,timeBetweenEvictionRunsMillis一起保证了所有超期的initialSize连接都会被重新连接,从而避免了Mysql长时间无动作会断掉连接的问题。
- minEvictableIdleTimeMillis:该参数设置每个连接的空闲时间,超过这个时间连接将被关闭
- timeBetweenEvictionRunsMillis:后台线程的运行周期,用来检测过期连接
- maxActive:最大能分配的连接数
- maxIdle:最大空闲数,当连接使用完毕后发现连接数大于maxIdle,连接将被直接关闭。只有initialSize < x < maxIdle的连接将被定期检测是否超期。这个参数主要用来在峰值访问时提高吞吐量。
(3)JVM参数
目标:
- GC的时间足够的小
- GC的次数足够的少
- 发生Full GC的周期足够的长
要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡
- 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值
- 年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小
- 在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集
- 线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
年轻代和年老代设置多大才算合理?
- 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
- 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:
- 本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理
- 通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间
如何减少 GC 的次数?
- 对象不用时最好显示置为 NUL: 一般而言,为 NUL 的对象都会被作为垃圾处理,所以将不用的对象置为NUL,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。
- 尽量少使用 System,gc():此函数建议 JVM 进行主 GC,会增加主 GC 的频率,增加了间接性停顿的次数。
- 尽量少使用静态变量:静态变量属于全局变量,不会被 GC 回收,他们会一直占用内存
- 尽量使用 StringBufer,而不使用 String 来累加字符串
- 分散对象创建或删除的时间:集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在这种情况下只能进行主 GC 以回收内存,从而增加主 GC 的频率。
- 尽量少用 finalize 函数:它会加大 GC 的工作量。
- 如果有需要使用经常用到的图片,可以使用软引用类型,将图片保存在内存中
- 能用基本类型入int 就不用对象 Integer
- 增大-Xmx 的值
十一:JVM 参数设置
1. 基本参数
- -Xmx350m:设置 JVM 最大堆内存为 350M。
- -Xms350m:设置 JVM 初始堆内存为 350M。此值可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。
- -Xs128k:设置每个线程的栈大小。JDK5.0 以后每个线程栈大小为 1M,之前每个线程栈大小为 256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 30~50 左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
- -Xmn2g:设置年轻代大小为 2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到 JVM 垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的 3/8。
- -X:NewSize=1024m:设置年轻代初始值为 1024M。
- -X:MaxNewSize=1024m:设置年轻代最大值为 1024M。
- -X:PermSize=256m:设置持久代初始值为 256M。
- -X:MaxPermSize=256m:设置持久代最大值为 256M。
- -X:NewRatio=4:设置年轻代(包括 1 个 Eden 和 2 个 Survior 区)与年老代的比值。表示年轻代比年老代为 1:4。
- -X:SurviorRatio=4:设置年轻代中 Eden 区与 Survior 区的比值。表示 2 个 Survior 区(JVM 堆内存年轻代中默认有 2 个大小相等的 Survior 区)与 1 个 Eden 区的比值为 2:4,即 1 个 Survior 区占整个年轻代大小的 1/6。
- -X:MaxTenuringThreshold=7:表示一个对象如果在 Survior 区(救助空间)移动了 7次还没有被垃圾回收就进入年老代。如果设置为 0 的话,则年轻代对象不经过 Survior 区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survior 区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少 Ful GC 的频率,这样做可以在某种程度上提高服务稳定性。
- -X:PretnureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
2. 垃圾收集相关参数