JVM和Java并发


[toc]

硬件内存模型

JMM中的主内存、工作内存与JVM的Java堆、栈、方法区等并不是同一个层次的内存划分, 这两者基本上是没有关系的 。

Java内存模型:(Java Memory Model ,JMM)

答题要点:1.JMM内存可见性;2.JMM如何保证原子性、可见性、有序性

JMM 就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java序在各种平台下对内存的访问都能保证效果一致的机制及规范,实现Java能够“一次编写、导出运行”。

主要是为了规定线程和内存之间的一些关系

  • JMM系统存在一个主内存(Main Memory)

  • Java中所有变量都存储在主内中,主内存中的变量对于所有线程是共享的

  • 每个线程都有自己的工作内存(Working Memory),工作内存中保存的是主内存中某些变量的拷贝对变量进行操作之后,需要刷新主内存中的变量;

  • 每个线程对所有变量的操作都在自身的工作内存中进行,线程之间无法互相直接访问,变量智能通过主内存进行传递

    如何保证原子性、可见性、有序性

    JMM对除long、double外的基础数据类型的读写操作时原子性的,另外关键字Synchronized也可以提供原子性保证;可见性是通过Synchronized/volatile保证的;有序性通过volatile和一系列happens-before原则保证的。

Java虚拟机模型:JVM

内存结构

  • CLASS LOADER(类加载器):把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型;

  • RUNTIME DATA AREA(运行时数据区):就是常说的JVM管理的内存区域

    答题要点:1.各个部分的功能;2.哪些是线程共享,哪些是线程独占

    • 线程共享区

      • 方法区(JDK 1.8将方法去移动到了直接内存,叫元空间[Metaspace])

        也叫永久区;存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码、运行时常量池等数据;

      • Java堆

        存放实例对象和数组对象;是垃圾回收的主要区域,由于现在收集器基本采用的分代垃圾收集算法,所以Java堆还可以细分成新生代和老年代;Java堆在物理上可以不连续,但在逻辑上必须连续;在堆上分配内存的方法有:指针碰撞(堆内存规整[是否规整,由垃圾回收器是否压缩整理决定]的情况下,所有用过和空闲的内存中间有明确的分界线)和空闲列表(用空闲列表来记录内存的使用情况);Java堆空间不足会抛出OutOfMemoryError

    • 线程独占区(这3个区域随线程而生,随线程而灭,对于内存分配和回收具有确定性,所以不需要考虑回收的问题)

      • 虚拟机栈:Java方法运行的内存模型,线程私有,生命周期与线程相同;每个方法被执行的时候都会常见一个栈桢(Stack Frame)用于存储局部变量表、操作数栈、动态链接和方法出口等信息; 方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用; StackOverflowError(线程请求的栈深度大于JVM所允许的深度);OutOfMemoryError(如果虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存)。
      • 本地方法栈: 本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法; 。
      • 程序计数器:记录当前线程所执行到的字节码的行号,记录下一条要运行的指令,每个线程都有一个程序计数器,是唯一一个没有OutOfMemoryError情况的区域。
  • EXECUTION ENGINE(执行引擎):用于执行命令,不同的JVM内部实现不同,执行引擎在执行Java代码的时候有可能有解释执行和编译执行;

  • NATIVE INTERFACE(本地库接口):与本地库交互,调用native方法;当调用native方法时,就进入一个全新的并且不再受JVM限制的世界,很容易出现native OutOfMemory。

类加载器

类加载器机制

类加载器机制是指:代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型 。

  • 类的加载

    1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

    2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。

    3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

  • 类的连接

    类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。

    1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

    2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;静态变量a就会在准备阶段被赋默认值0

    对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中

    另外,静态常量会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。

    3、解析:将常量池内的符号引用换为直接引用。

  • 类的初始化

    真正开始执行定义的java代码; 初始化阶段是执行类构造器()方法的过程 ;当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化 。

都有哪些类加载器
  • 启动类加载器:负责加载%JAVA_HOME%\bin目录下的所有jar包,或者是-Xbootclasspath参数指定的路径
  • 扩展类加载器: 负责加载%JAVA_HOME%\bin\ext目录下的所有jar包,或者是java.ext.dirs参数指定的路径
  • 应用程序类加载器:负责加载用户类路径上所指定的类库,开发者可直接使用这个类加载器;

除了顶层的启动类加载器外,其他的类加载器都应该有自己的父类加载器

双亲委派模型

如果一个类加载器收到了类加载的请求,先把这个请求委派给父类加载器去完成(所以所有的类加载请求最终都会传递到顶层的启动类加载器中),只有当父类加载器反馈自己无法完成该类加载请求,子加载器才会尝试自己去加载。

自定义了一个java.lang.String会被加载吗?不会,

GC垃圾回收

哪些对象可以被回收
  • 引用计数器法

    堆内每个对象实例都有一个引用计数器;每当一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的,但是目前主流的虚拟机并没有选择这个算法来管理内存,原因是不能解决对象之间相互循环引用的问题相互循环引用的对方,它们的引用计数器都不为0,,也就不会被回收。

  • 可达性分析法

    通过一系列的称为“GC ROOT”的对象为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC ROOT没有任何引用链相连的话,则证明此对象是不可用的;

    可以用作GC ROOT的对象包括: a) 虚拟机栈中引用的对象(栈帧中的本地变量表); b) 方法区中类静态属性引用的对象; c) 方法区中常量引用的对象; d) 本地方法栈中JNI(Native方法)引用的对象。

无论是引用计数器还是可达性分析,判断对象是否存活都与“引用”有关系且与强引用有关系。在Java中,将引用可分为强引用、软引用、弱引用、虚引用。

  • 强引用:如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError,使程序异常终止;
  • 软引用:如果内存空间够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用,软引用可用来实现内存敏感的高速缓存。
  • 弱引用:比软引用更弱一些,弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用:在任何时候都可能被垃圾回收器回收,主要用于跟踪对象被垃圾回收器回收的活动

引用、引用都在引用对象被垃圾回收之后,JVM才把引用加入到与之关联的引用队列中;而引用对象在垃圾回收之前,JVM把引用加入到阈值关联的引用队列中。虚引用必须和引用队列联合使用,当垃圾回收器准备回收一个对象时,若发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。这样就可以通过判断引用队列中是否已经加入了虚引用来了解被引用对象是否将要被垃圾回收器回收

对象回收前的最后挣扎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

  • 第一次标记且会进行一次筛选

    对于可达性分析中不可达的对象将会进行第一次标记且会进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法,在finalize方法中没有重新与引用链关联关系的,就会被回收。如果此对象对应的类中没有覆盖finalize方法,或finalize方法已经被虚拟机调用过[finalize方法系统只会调用一次],则认为该对象没有必要执行finalize方法,对象直接被回收;

  • 第二次标记

    对于有必要执行finalize方法的对象,会被放置在一个F-Queue的队列中,并在稍后由一条由虚拟机自动建立的,低调度优先级的Finalizer线程去执行finalize方法,除非这个对象在执行finalize方法时与引用链建立关联[成功拯救自己],否则第二次标记成功的对象将被回收。

垃圾收集算法
  • 分代收集

    为什么要进行分代:大多数对象都是朝生夕灭的;越是熬过越多次垃圾收集过程的对象就越难以消亡,这样将那些朝生夕灭的对象放在一起,在回收时只需要关注如何保留少量存活而不去标记大量将要回收的对象,就能以较低代价回收到大量的空间;对于熬过多次垃圾收集的对象,将它们集中在一起,虚拟机便可以以较低频率来回收这个区域,这样就兼顾了垃圾回收的时间开销和内存的空间利用。在Java堆中划分不同的区域之后,垃圾收集器才能每次只回收其中一个或者某些部分的区域——从而有了“ Minor GC[ 对新生代进行回收,不会影响到年老代 ]”、“ Major GC”、“Full GC[ 对整个堆进行回收,包括新生代和老年代 ]”这样的回收类型,从而也有了标记-清除标记-复制标记-整理等针对性的垃圾收集算法。

    目前虚拟机使用的回收算法

  • 标记-清除

    首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记对象,该算法在存活对象较多的情况下极为高效,但是会产生大量碎片

  • 标记-复制(回收新生代-新生代中有大量的对象需要回收)

    针对整理-清除算法的缺点,复制算法将内存分为大小相同的两块,每次使用其中的一块。当这块内存使用完后,就将还存活的对象复制到另一半去,然后再把使用的空间一次清理掉。但是这样做成本较高(若存活的对象很多,复制的成本就很大,其次就是可用内存缩小为原来的一半,空间浪费大)。所以一般不会1:1划分边界,可以分成一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和1块Survivor空间,当会收时,就将Eden空间和Survivor空间中还存活的对象复制到另一块Survivor上,清理Eden空间和Survivor空间即可,一般的比例是8:1:1,每次浪费10%的空间,若存活的对象大于10%,就采取分配担保策略,多出来的对象直接进入老年代

  • 标记-整理

    标记过程与标记-清除一致,不直接对可回收对象进行回收,而是让所有存活的对象移动到一端,然后直接清除掉端边界之外的内存即可;

GC 优化
  • 将进入老年代的对象数量降到最低
  • 减少Full GC的执行时间
  • 优化JVM参数——堆和栈的大小,设置垃圾收集器的模式

性能调优

jvm参数

jvm技术趋势


Manba_girl: Mamba_girl
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Mamba_girl !
  目录