JVM定义及作用:

JVM是java Virtual Machine (java 虚拟机),是一个虚拟计算机,也是java程序是爱西安跨平台的基础。其作用是加载Java程序,将字节码翻译成机器码再交给CPU执行的虚拟计算机。

JVM的主要组成:

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)

工作流程:

首先程序在执行前将java代码(.java)转变成字节码(.class),JVM通过;类加载器将字节码加载到内存中,由于字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层的机器码,再交给CPU执行,CPU执行过程中需要调用本地库接口来完成整个程序的运行。

JVM的内存布局:




javax虚拟机 java虚拟机怎么使用_javax虚拟机


不同的虚拟机存在不同,但都会遵循java虚拟机规范。java 8虚拟机规范规定,内存布局包括以下几个区域:

  • 程序计数器(Program Counter Register)
  • java虚拟机栈(java Virtual Machine Stacks)
  • 本地方法栈(Native Method Stack)
  • java堆(java Heap)
  • 方法区(Method Area)

1.程序计数器

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是native方法,该计数器的值为undefined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

线程私有。由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。

2.java虚拟机栈

每个java方法在执行的同时会创建一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。从方法调用直至执行完成的过程,对应着一个栈帧在java虚拟机栈中入栈和出栈的过程。线程私有,生命周期与线程相同。

该区域有两种异常:

  • StackOverflowError:当线程请求的栈深度超过最大值
  • OutOfMemoryError:虚拟机栈扩展到无法申请足够的内存时

3.本地方法栈

类似于java虚拟机栈,区别是本地方法栈服务于本地方法。

4.java堆

java堆是java虚拟机中内存最大的一块。在虚拟机启动时创建,被所有线程共享。

存放对象实例。垃圾收集器主要管理的就是java堆。

现在的垃圾收集器基本采用分代收集算法,主要思想是针对不同类型的对象采取不同的垃圾回收算法,java堆可以分为两块:

  • 新生代(Yong Generation)
  • 老年代(Old Generation)

动态扩展内存失败会抛出OutOfMemoryError异常。

5.方法区

被所有线程共享,用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等信息。

动态扩展内存失败会抛出OutOfMemoryError异常。

运行时常量池是方法区的一部分。保存Class文件中的符号引用、翻译出来的直接引用。

垃圾收集

垃圾收集主要针对java堆和方法区。

1.判断一个对象是否可被回收

  • 引用计数算法:给对象添加引用计数器,增加引用+1,引用失效-1。引用计数为0的对象可以被回收。(容易出现循环引用问题)
  • 可达性分析算法:以GC Roots为起点进行搜索,可达的对象是存活的,不可达的对象可被回收。GC Roots 一般包括:
  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中JNI(java本地接口)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

2.java的4中引用方式

  • 强引用(Strong Reference):被强引用关联的对象不会被回收。 Object obj=new Object();
  • 软引用(Soft Reference):被软引用关联的对象只有在内存不够的情况下被回收。
  • 弱引用(Weak Reference):被弱引用关联的对象只能活到下一次垃圾回收发生之前。
  • 虚引用(Phantom Reference):为对象设置虚引用的目的是在这个对象被回收时收到一个系统通知。

3.垃圾回收算法

  • 标记-清除算法(Mark-Sweep):首先标记处所有需要回收的对象,之后统一回收被标记的对象。效率低,标记清除后会产生大量的不连续的内存碎片。
  • 复制算法(Copying)-新生代:将可用内存划分成大小相等的两块,每次只使用一块。当这块内存用完,将存活的对象复制到另一块,饭后将使用过的内存空间一次清理。
  • 现在的商业虚拟机都采用这种复制算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间(8:1:1),每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
  • 标记-整理算法(Mark-Compact)-老年代:让所有存活的对象向一端移动,然后直接清理掉边界外的内存。
  • 分代收集算法:根据对象的存货周期,将内存划分为几块。一般将java堆划分为新生代和老年代。
  • 新生代:复制算法
  • 老年代:标记-清除算法 / 标记-整理算法

垃圾收集器


javax虚拟机 java虚拟机怎么使用_老年代_02


  • Serial 收集器:这是一个以串行的方式单线程收集器。它只会使用一个CPU或一条收集线程去完成收集工作,并且在进行垃圾回收是必须暂停其他所有工作线程直到收集结束。
  • ParNew收集器:Serial收集器的多线程版本。
  • 并行:Parallel指多条垃圾收集线程并行工作,此时用户线程处于等待状态。
  • 并发:Concurrent指用户线程和垃圾回收线程同时执行,用户进程在执行,而垃圾回收线程在另一个CPU上执行。
  • Parallel Scavenge收集器:并行的多线程收集器,使用复制算法。吞吐量优先收集器。
  • GC自适应调整策略:不需要手动指定新生代的大小及比例、晋升老年代的年龄等参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间、最大的吞吐量。
  • Serial Old收集器:使用标记-整理算法
  • Parallel Old收集器:使用标记-整理算法
  • CMS(Concurrent Mark Sweep)收集器:是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。
  • 初始标记(CMS initial mark):标记GC Roots能直接关联到的对象,停顿
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing ,不用停顿
  • 重新标记(CMS remark):修正并发标记期间的变动部分,停顿
  • 并发清除(CMS concurrent sweep):不需要停顿
  • G1收集器:面向服务端的垃圾回收器。并行与并发、分代回收、空间整合、可预测停顿、G1将堆划分成多个大小相等的独立区域(Region)。
  • 初始标记(initial mark)
  • 并发标记(concurrent mark)
  • 最终标记(final mark)
  • 筛选回收(live data counting and evacuation)

Minor GC 和Full GC:

  • Minor GC:回收新生代,新生代对象存活时间短,因此Minor GC会频繁执行,速度快。
  • Full GC:回收老年代和新生代,老年代对象存活时间长,因此Full GC很少执行,速度慢。

Full GC的触发条件

  1. 调用System.gc():不建议使用
  2. 老年代空间不足
  3. 空间分配担保失败
  4. JDK1.7及之前的永久代空间不足
  5. Concurrent Mode Failure (CMS GC工程中老年代空间不足)

内存分配策略

  1. 对象优先在Eden分配
  2. 大对象直接进入老年代
  3. 长期存活的对象进入老年代
  4. 动态对象年龄判定:在Survivor中相同年龄所有对象总和大于Survivor空间的一半,则年龄大于等于该年龄的对象直接进入老年代。
  5. 空间分配担保:在发生Minor GC之前,虚拟机先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立,Minor GC确认安全;如果不成立,虚拟机查看HandlePromotionFailure的值是否允许担保失败,如果允许继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于尝试进行Minor GC;如果小于或不允许冒险,就进行Full GC。


javax虚拟机 java虚拟机怎么使用_方法区_03


类加载器

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。


javax虚拟机 java虚拟机怎么使用_javax虚拟机_04


类加载过程包括加载、验证、准备、解析、初始化这5个阶段。

1.加载

  • 通过类的完全限定名称获取定义该类的二进制字节流。
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
  • 在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口。

2.验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会威海虚拟机的安全。

3.准备

  • 类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
  • 实例变量不会在该阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。

4.解析

虚拟机将常量池中的符号引用替换为直接引用。

5.初始化

开始执行类中定义的java程序代码。初始化阶段是虚拟机执行类构造器<clinit>()方法的过程。


javax虚拟机 java虚拟机怎么使用_java虚拟机_05


双亲委派模型

如果一个类加载器收到一个类加载的请求,首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

java内存模型


javax虚拟机 java虚拟机怎么使用_老年代_06


并发注意操作的原子性、可见性、有序性。

  • 原子性:基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。
  • 可见性:是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。
  • 有序性:线程内表现为串行的语义,“指令重排”现象和“工作内存与主内存同步延迟”现象。Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。

volatile

关键字volatile是java虚拟机提供的最轻量级的同步机制。

happens-before(先行发生原则)

这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系

java线程

线程状态转换

  • 新建(new):创建后尚未启动。调用start()方法开始运行。
  • 运行(Runnable):包括了操作系统线程状态中的Running和Ready,正在执行或等待CPU分配时间。
  • 无限期等待(Waiting):这种状态下的线程不会被CPU分配时间, 等待其他线程显示唤醒。以下方法会使线程进入无限期等待状态:
  • 没有设置Timeout参数的Object.wait()方法;
  • 没有设置Timeout参数的Thread.join()方法;
  • LookSupport.park()方法。
  • 限期等待(Timed Waiting):在一定时间后由系统自动唤醒。以下方法会使线程进入限期等待状态:
  • Thread.sleep()方法;
  • 设置了Timeout参数的Object.wait()方法;
  • 设置了Timeout参数的Thread.join()方法;
  • LookSupport.parkNanos()方法
  • LookSupport.parkUntil()方法。
  • 阻塞(Blocked):阻塞状态是在等待获取一个排他锁,这个事件会在另一个线程放弃这个锁的同时发生。在程序等待进入同步区域的时候,线程进入阻塞状态。
  • 结束(Terminated):已终止线程的线程状态。


javax虚拟机 java虚拟机怎么使用_方法区_07


参考资料:

https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md#%E4%B8%80%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9Fgithub.com

Java虚拟机(JVM)你只要看这一篇就够了!

javax虚拟机 java虚拟机怎么使用_java_08