JVM虚拟机


JVM内存模式

JVM内存模式(指内存分区)主要是指运行时的数据区,包括5个部分:
v2-48c28fbd914babc267b289b8ee059ff5_720w

  • 栈(方法栈):线程私有,线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息,调用方法时执行入栈,方法返回时执行出栈。
  • 本地方法栈:与方法栈的作用类似,也是用来保存线程执行方法时的信息,只不过方法栈服务于Java方法,而本地方法栈服务是为虚拟机调用调用Native方法服务。
  • 程序计数器:用来记录当前线程所执行的字节码位置,字节码解析器通过改变计数器的值来选取下一条需要执行的字节码指令;每个线程工作时都有一个独立的计数器。
  • 堆:作为虚拟机中内存最大的一块,被所有线程共享,目的时为了存放对象实例,几乎所有的对象实例都在这里分配;当堆内存没有可用空间时,会抛出OOM异常(内存溢出);根据对象存活周期不同,虚拟机把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。
  • 方法区:用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

JMM内存模型

JMM内存模型是一种虚拟机规范,与常说的JVM内存模式(型)不同,JVM内存模式指的是JVM的内存分区。

JMM内存可见性

v2-7552b6be776a5d65dcdd1eeb65c4a90a_720w

Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),主要目标是定义程序中变量的访问规则:
所有的共享变量都存储在主内存中共享,每个线程都有自己的工作内存,工作内存中保存的是主内存中变量的副本,线程堆变量的读写等操作都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
在对线程进行数据交互时,线程A给一个共享变量1赋值后,由线程B来读取这个值,线程A修改完变量时修改在自己的工作内存中,线程B不可见,只有从线程A的工作内存写回主内存之后,线程B再从主内存读取到自己工作内存才能完成进一步操作。由于指令重排序的存在,上述过程可能会被打乱,因此JMM需要提供原子性、可见性、有序性的保证。

JMM保证

v2-07461119fe1a929c78e3d1ac15bc2dc1_720w

原子性

JMM保证对除long和double类型外基本数据类型的读写操作时原子性的。另外,关键字synchronized也可以提供原子性保证。

可见性

JMM可见性的保证,一个是通过synchronized,另一个是通过volatile。volatile强制变量的赋值会同步刷新回主内存,强制变量的读取会从主内存重新加载,保证不同的线程能够看到该变量的最新值。

有序性

JMM有序性保证,主要是通过volatile和happens-before原则。volatile的另一个作用就是阻止指令重排序,这样就可以保证变量读写的有序性。

happens-before原则主要有:

  • 程序顺序规则:在一个线程内一段代码的执行结果是有序的。就是说,还是会指令重排序,但随便怎么排,结果是按照我们代码顺序生成的不会改变。
  • 锁规则:无论是在单线程还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果。
  • colatile变量规则:如果一个线程先写一个volatile变量,然后另一个线程读取这个变量,那么这个写操作的结果一定对读的这个线程可见。
  • 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B是可见的。
  • 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果对于线程A是可见的。
  • 线程中断规则、传递规则、对象终结规则。

类加载机制

类的加载是指将编译好的Class类文件中的字节码读入内存中,将其放在方法区并创建对应的Class对象。类的加载分为加载、链接、初始化、,其中链接又包括验证、准备、解析三个步骤。
v2-4dd11842aeb5cc150e789027290016d6_720w

  1. 加载是文件到内存的过程。通过类的完全限定名查找此类字节码文件,并利用字节码文件创建一个Class对象。
  2. 验证是对类文件内容验证。目的在于确保Class文件符合当前虚拟机要求,不会危害虚拟机自身安全,主要包括四种:文件格式验证、元数据验证、字节码验证、符号引用验证。
  3. 准备是进行内存分配。为类中static修饰的变量分配内存,并且设置初始值(初始值是0或null,而不是代码中设置的具体值,且不包含final修饰的静态变量,final修饰的静态变量在编译时就会分配)。
  4. 解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程,直接引用就是直接指向目标的指针、相对偏移量等。
  5. 初始化主要完成静态代码块的执行与静态变量的赋值,如果被加载类的父类没有初始化,则先对父类进行初始化。只有对类主动使用时,才会进行初始化,初始化的触发条件包括在创建类的实例时、访问类的静态方法或静态变量时、Class.forName()反射类时,某个子类被初始化时。

注意:由Java虚拟机自带的类加载器(引导类加载器、扩展类加载器、系统类加载器)加载的类在虚拟机的整个生命周期中时不会被卸载的,只有用户自定义的类加载器所加载的类才会被卸载。

双亲委派模式

Java中类的加载使用双亲委派模式,即一个类加载器在加载类的时候,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,父类加载器能够完成类加载就成功返回,无法完成加载时,子加载器才会尝试自己去加载。

垃圾回收算法(GC)

标记清除算法

最基础的垃圾回收算法,主要分为标记阶段和清除阶段,标记阶段的任务是标记出所有需要被回收的对象,清除阶段则是回收被标记的对象所占用的空间。这个算法最严重的问题是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

复制算法

复制算法是将内存按容量划分为大小相等的两个区域,每次只使用其中一块,当使用中的这块内存用完时,就将还存活着的对象复制到另一块上面,然后再把已经使用的内存空间一次清除。这个算法虽然可以解决内存碎片问题,但使得能够使用的内存空间缩减到原来的一半,而且存活的对象越多,算法效率越低。

标记整理算法

先对需要清理的对象进行标记,完成标记后将存活对象都向一端移动,然后清理掉端边界以外的对象。

分代算法

其核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,一般情况下将堆内存划分为老年代和新生代。老年代的特点是每次垃圾回收时只有少量对象需要被回收,所以采用标记整理算法;而新生代则是每次垃圾回收时都有大量对象需要被回收,所以采用复制算法。