Java 虚拟机
概念: JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
运行过程:
Java 源文件 --> 编译器 --> 字节码文件 --> JVM解释器 --> 机器码
Java 的跨平台性: 每一种平台的解释器不同, 但实现的虚拟机是相同的, 当一个程序开始运行, 虚拟机就开始实例化了, 多个程序启动就会存在多个虚拟机实例.
线程
JVM允许一个应用并发执行多个线程, hotspot Jvm 中的java线程和原生操作系统线程有直接映射关系. 但本地存储,缓冲区分配,同步对象,栈,程序计数器等准备号了, 就会创建一个操作系统原生线程.
JVM后台线程:
- 虚拟机线程: 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM位于安全点。这些操作的类型有:stop-theworld 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
- 周期性任务线程: 负责定时器时间, 用来调度周期性操作执行
- GC线程: 在运行时将字节码动态编译成本地平台相关的机器码
- 信号分发线程: 接受发送到jvm的信号,并调用适当的jvm方法处理
线程和进程的区别: (1) 进程是操作系统资源分配和调度的基本单位, 线程是CPU调度和分派的基本单位; (2) 进程是线程的容器, 线程是进程的一个实体, 线程不能独立执行, 一个进程至少有一个线程; (3) 进程在执行过程中拥有独立的内存单元, 而一个进程的多个线程共享内存;
JVM 内存模型
线程私有区域: 虚拟机栈和本地方法栈、程序计数器
线程共享区域: 方法区 和 堆
- 程序计数器: 是当前线程所执行的虚拟机字节码的地址的指示器, 如果是本地方法, 则为空; 内存区域唯一一个没有规定OOM的区域.
- 虚拟机栈: 描述java方法执行的内存模型, 每个方法在执行时都会创建一个栈帧, 用于存储局部变量表,操作数栈,动态链接,返回地址等信息.
- 本地方法栈: 为 Native 方法服务, 在 HotSpot JVM 中, 与虚拟机栈合二为一.
- 方法区: 用于存储被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等.
- 运行时常量池: 方法区的一部分, Class 文件中处理类的版本,字段,方法,接口描述信息外, 还有一项常量池. 用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 堆:
- 新生代: 用来存放新生的对象, 1/3的堆空间, 由于频繁创建新对象, 所以会频繁触发 Minor GC
- Eden: 新对象出生地, 大对象则直接分配到老年代. Eden区内存不够时就触发 GC.
- ServivorFrom: 上一次的幸存者, 这次被扫描
- ServivorTo: 保留这次 Minor GC过程中的幸存者
- 老年代: 主要存放程序中声明周期较长的内存对象, 或大对象.
- 永久代: 内存的永久保存区, 主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理. 容易抛出OOM异常.
- 新生代: 用来存放新生的对象, 1/3的堆空间, 由于频繁创建新对象, 所以会频繁触发 Minor GC
java8与元数据: 在java8中, 永久代被移除, 使用元数据区(元空间)代替. 元空间使用本地内存. 类的元数据放入native memeory中, 字符串和类的静态变量放入java堆中.
元数据: 描述数据的数据, 例如在java代码中吗添加到方法/字段/类/包上的额外信息, 如注解
注: minor GC: 新生代的垃圾回收, major Gc 老年代的垃圾回收, full GC 同时回收新生代/老年代/永久代
新生代的垃圾回收
- eden/servivorFrom 复制到 servivorTo, 年龄+1, 到达老年标准则复制到老年代(年龄15)
- 清空 eden/servivorFrom
- servivorFrom和servivorTo 互换, (只是身份互换)
老年代的垃圾回收
老年代对象比较稳定, 在major GC之前, 一般都先进行了一次minor GC, 新进的对象发现空间不够才触发.
使用标记清除算法
, 先扫描所有老年代, 标出存活的对象, 然后回收无标记的对象.
垃圾回收
确定垃圾的方式
- 引用计数法: 一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。存在循环引用的问题.
- 可达性分析:通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
垃圾回收算法:
- 标记清除算法: 标记阶段标记出所有需要回收的对象, 清除阶段回收被标记的对象所占用的空间. 缺点: 导致大量内存碎片
- 复制算法: 按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉. 缺点:内存利用率低, 却存活对象较多时, 复制效率也低.
- 标记整理算法: 标记阶段和标记清除算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
- 分代收集算法: 根据对象存活的不同生命周期将内催划分为不同的域, 老年代每次只有少量对象需回收, 新生代每次有大量垃圾回收. 不同区域采用不同的算法.
java四种引用类型
- 强引用: 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到, JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
- 软引用: 需要用
SoftReference
类实现, 对于只有软饮用的对象, 只有系统内存不足时才会被回收, 通常用在内存敏感的程序中. - 弱引用:
WeakReference
类实现, 只要垃圾回收机制一运行, 不管jvm空间是否足够, 总会被回收. - 虚引用:
PhantomReference
类实现, 必须和引用队列联合使用, 主要用于跟踪对象被垃圾回收的状态.
GC 垃圾收集器
新生代收集器: serial, ParNew, Parallel Scavenge, G1
老年代收集器: CMS, Serial Old, Parallel Old, G1
-
Serial 垃圾收集器, (单线程, 复制算法)
需要暂停所有其他的工作线程, 简单高效
运行在client模式下默认的新生代垃圾收集器 -
ParNew 垃圾收集器, (Serial + 多线程)
Serial 的多线程版本, 默认和cpu数目相同的线程, 使用-XX:ParallelGCThreads限定 -
Parallel Scavenge 收集器, (多线程复制算法, 高效)
重点关注程序达到一个可控制的吞吐量(=运行代码时间/(运行代码时间+垃圾收集时间)) , 自适应调节策略 -
Serial Old 收集器, (单线程标记整理算法)
运行在client默认的java虚拟机老年代垃圾收集器 -
CMS 收集器 (多线程标记清除算法)
concurrent mark sweep 收集器是老年代垃圾收集器, 主要目标是获取最短垃圾回收停顿时间. 并发标记/清除好使最长,所以总体上看CMS是和用户线程一起并发执行.- 初始标记: 只标记GC Roots能直接关联的对象, 速度很快, 仍需暂停所有工作线程
- 并发标记: 进行GC Roots 跟踪的过程, 和用户线程一起工作
- 重新标记: 为修正并发标记期间引用户程序继续运行导致标记产生的变动, 仍需停止所有工作线程
- 并发清除: 清除GC Roots 不可达对象, 和用户线程一起工作.
-
G1 收集器
- 基于标记-整理算法, 不产生内存碎片
- 可以非常精确控制停顿时间, 在不牺牲吞吐量的前提下, 实现低停顿垃圾回收.
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
JVM 类加载机制
jvm 类的加载机制分为五个部分: 加载, 验证, 准备, 解析, 初始化
- 加载: 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的入口.
- 验证: 确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求.
- 准备: 准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间.
赋初值, 只是0, false等, 真正赋值是在编译后, 类构造器方法中 - 解析: 指虚拟机将常量池中的符号引用替换为直接引用的过程。
- 符号引用: 引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
- 直接引用: 是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。引用的目标必定已经在内存中存在。
- 初始化: 初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码.
不会执行类的初始化的情况:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触
发定义常量所在的类。 - 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
类加载器
- 启动类加载器, Bootstrap ClassLoader
负责加载JAVA_HOME\lib目录中或 -Xbootclasspath 参数指定路径中被虚拟机认可(rt.jar)的类 - 扩展类加载器, Extension ClassLoader
JAVA_HOME\lib 目录中, 或java.ext.dirs系统变量指定路径中的类库 - 应用程序类加载器, Application ClassLoader
负责加载用户路径上的类库
双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
比如对于rt.jar中的java.lang.Object, 不管那个加载器加载这个类, 最终都委托给启动类加载器, 保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。