JVM学习(一) 整体回顾

JVM整体回顾

JVM架构图如下

 

JAVA虚拟机是装在操作系统之上的一个应用软件【平台性质】,作用是:将class格式的字节码编译成可执行的机器码。从而,class格式和物理机无关。也就是所谓的java和平台无关。做到跨平台。

从上图可以看出,JVM核心知识架构

一、类加载子系统相关

(一) 类加载

类加载

  1.根据全路径名获取二进制的字节流

  2.将类的静态存储结构转换成运行时数据结构

  3.生成一个Java.lang.class类型的对象。作为内存使用类的一个接口。

类加载器

  1.定义

  2.3种预定于类加载器

    1. 启动类加载器(根类加载器)。加载路径JAVA_HOME/lib
    2. 扩展类加载器。加载路径JAVA_HOME/lib/ext
    3. 应用类加载器。加载路径ClassPath下的所指定的类库。开发者直接使用。默认的类加载器
双亲委派机制

  1.原理

  2.优势

    1. 避免类的重复加载
    2. 保证程序安全,避免核心API被修改。【就算自动以核心API,根据双亲委派机制,最终还是会加载启动类加载器加载的类库,而不会加载自定义的方法】

(二) 链接

验证

  验证字节码是否满足规范,元数据格式,文件格式等

准备

  静态变量分配内存并初始化值。

  1.如果final修饰,则准备阶段的初始值就是程序中的初始值。

  2.如果没有被final修改,则静态变量的初始值给对应数据类型的初始值。比如0.是给内存对象分配内存的阶段。

解析

  将虚拟机中常量池的部分符号引用(类名、方法名、属性名转换成直接引用

(三) 初始化

  执行java中的构造方法,给属性变量赋值

二 、内存管理机制和垃圾回收器

(一) 内存管理机制

运行时内存区域

  线程级别(程序计数器、JAVA虚拟机栈、本地方法栈)和非线程级别(方法区和堆)【线程共享】

(1).程序计数器

  记录指令码执行的位置,在线程切换回来后能继续执行。

(2).JAVA虚拟机栈

  线程方法执行的方法调用。一个方法对应一个栈帧。包括局部变量表操作数栈动态链接方法返回地址

(3).本地方法栈

  和JAVA虚拟机栈功能相同。区别在于执行本地方法(类库)时使用。

(4).方法区

  用来存储被虚拟机加载的类的信息。包含类常量、静态方法、即时编译器编译后的代码等。

(5).

  Java虚拟机启动时就创建,目的是存储创建的实例对象。

(6).运行时常量池

  方法区的一部分。用于存放字面量符号引用

(7).直接内存

  不是java虚拟机的一部分,运行时候可能会使用到。是计算机的直接内存。NIO技术的时候,通常会被使用到。

2  内存对象操作

(1).内存对象创建

  内存对象的创建流程基本上分两步:其一 判断类是否已经被加载到方法区。如果没有加载到方法区,先将类加载到方法区。其二 在新生代的Eden区创建对象【验证、准备、解析和初始化】。

(2).内存对象的结构

  内存对象的结构,即布局(格式),包含三部分:

  1. 对象头【GC代年龄、哈希码,锁信息等】
  2. 实例数据【真实对象数据】
  3. 对齐填充数据。

(3).内存对象的调用

  1. 直接引用

  Java虚拟机栈的局部变量表中的变量引用是对象的地址。就是直接引用。直接通过地址访问对象。

  1. 句柄引用

  JAVA虚拟机栈的局部变量表的变量引用是句柄,是间接引用。访问对象需要通过句柄到句柄池中找到句柄对应的地址。然后再访问改地址,就会访问到对象。

常见的内存溢出

(1).JAVA堆溢出

  堆内存溢出会提示Java heap space。核心是区分内存溢出还是内存泄露

  1. 内存泄露

  可以根据GCRoot引用链分析,分析在哪个环节,需要对对象的引用及时释放,引用了已经不需要的对象。从而解决泄露的问题。

  1. 内存溢出

  通过调整JVM堆内存-xmx -xms 来设置。通过分析哪些对象的生命周期可以缩短(对象还不能释放,但是对象的部分属性值不需要继续使用,可以释放)。目的是及时的释放内存。避免内存溢出。

(2).栈溢出

  JAVA虚拟机栈和本地方法栈溢出栈溢出会提示”stack over flow”提示这个错误,

  两种可能:其一 栈深度不够方法调用使用(可能性比较小-通常支持2000)。其二 栈申请扩展空间,没有足够的空间提供。

  分析:【常见的原因是线程数量太多,JAVA栈是线程级别的,线程太多会造成栈太多。每个线程都有自己独立的虚拟机栈,线程数多可能造成空间申请不够,从而报错--解决的思路:调小虚拟机堆的大小或者减小栈对象的大小,从而确保更多的线程能存在】

(3).方法区和运行时常量池溢出

  提示”PermGen space”方法区存放的是Class的相关信息。比如类名、修饰符、常量池、字段描述、方法描述等。运行时产生大量的类填充方法区,直到溢出。提示”PermGen space”通常是动态代理等字节码增强技术会导致方法区的内存溢出。

(4).直接内存溢出

  通常为NIO等技术会造成内存溢出。如果内存溢出,可以核心关注NIO技术实现部分的代码。

(二) 垃圾回收器

对象存活判断

(1).引用计数法

  对象表里面记录对象被引用的次数。为0代表没有被引用。可以不回收。大于0代表被引用,说明对象存活着。

  缺点:不能解决循环引用的问题。

(2).可达性分析法

  从GCRoot节点向下检索,如果有到达对象的路径,则该结点存活。切路径为引用链。

垃圾回收算法

(1).标记-清除法

  首先标记需要清除的对象,然后在统一清除。

(2).标记-复制法

  将内存分为两部分S1S2.每次只使用一部分。对使用的那部分内存比如S1进行标记存活的对象。让后将存活的对象移动到另外一个内存区域比如S2,之后对原来内存S1整体进行回收。

(3).标记整理法

  将内存存活的对象进行标记,然后将存活的对象向一段移动。然后清理到端边界以外的内存。

(4).分代收集法

  根据对象存活周期的不同将内存划分为几块。分别为新生代老年代

3  垃圾回收器

  具体实现垃圾回收的工具。根据处于不同的位置分为新生代垃圾回收器和老年代垃圾回收器。从线程角度而言,分为单线程和多线程垃圾回收器。

(1) Serial收集器(标记-复制法)

  新生代算法采用 标记-复制 算法。是一个单线程的收集器。只对新生代进行垃圾回收。它在进行垃圾回收的时候,必须停止所有的线程工作。Stw --stop the word

(2).ParNew收集器(复制算法)

  新生代ParNew serial收集器的多线程版本。也针对新生代。算法也是 标记-复制算法。

(3).Parallel scavenge 收集器(复制法)

  新生代并行的多线程,和ParNew类似。

(4).Serial Old 收集器(标记-整理法)

  老年代老年代的收集器,同样是单线程收集器。使用的算法是标记-整理

(5).Parallel Old 收集器(整理法)

  老年代CMS(Concurrent Mark Sweep )收集器(标记-清除法)-老年代

4) 详细熟悉CMS流程

三. 执行引擎

  个人理解,执行引擎的功能是方法的执行。具体分为三部分内容。方法的表达【栈帧的结构】。方法的调用【确定方法调用的版本】和方法的执行【字节码转成机器码的方法】

(一) 栈帧结构-方法的表达

局部变量表

  作用:存储函数的参数和函数自己定义的局部变量。

  类型:有基本类型数据和引用类型。可能是直接引用也可能是间接引用。

  控制方法中大内存对象的生命周期。方法执行过程中,如果方法中大内存变量使用完成后,可以在方法之前赋值为null,这样垃圾回收器可以提前对其进行回收。不用等到方法执行完成才对其回收。可以尽早的释放内存。

2  操作数栈

  作用:操作数栈主要进行计算使用的。

  在数据放入到局部变量表的时候,可能会首先放入操作数栈,然后在弹出栈放入局部变量表中。在进行计算的时候,也需要根据指令从局部变量表中获得数据,然后压栈到操作数栈,之后弹出,进行计算。这里和数据结构的运算式后序表达使用栈计算的知识点重合。

3  动态链接

  动态链接是指在栈帧中存储当前方法方法区的运行时常量池中的符号引用。运行时常量池中的符号引用一部分(可以通过解析确定调用版本)在类加载的链接的第三阶段(解析阶段)(具体为链接结点的解析过程中)替换成直接引用。另外一部分(通过分派确定调用方法版本)是在每一次运行期间转换成直接引用。这部分称之为动态链接。

4  方法返回地址

  方法不论正常退出还是异常退出,都需要返回到方法调用的位置,保证程序继续向下执行。所以,栈帧通过方法返回地址找到返回方法退出时候返回的位置。

(二) 方法调用-确定方法调用的版本

1  解析

  如果在调用之前就可以确定方法的版本,并且在程序运行期间,方法的版本不会发生变化。那么,在链接的解析阶段就将代表方法的符号引用替换成方法的直接引用。这种方法的调用称之为解析。

2  分派

  运行时才能确定方法版本,称之为分派。分派分为静态分派和动态分派。最典型的两种例子为重载重写

1.静态分派

  根据函数的参数声明来确定方法的版本【重载--相同的方法名,不同的参数类型,根据参数类型来确认方法的版本】定位函数版本的时候不是根据参数的具体类型。而是参数的定义类型来确定的

2.动态分派

  根据实际的类对象来确认方法的版本。根据运行时候实际对象的不同,才确定调用哪个对象中的方法。【重写--最能体现】

(三) 执行引擎-方法的执行

1) 解析执行

  JVM得到字节码,通过interpreter解释器将字节码翻译成机器码。然后执行。Hotspot的解析执行器是基于栈的指令集的解析执行器。

2) 编译执行

  JVM提供即时编译器技术,将一段字节码编译成机器码,避免解析执行,提升效率。对于循环的代码块或者多次执行的代码块每次解析执行效率低于一次编译,后续直接执行机器码。而编译执行的目的就是为了解决多次执行代码的编译。

四. JAVA内存模型

  Java内存模型的主要目标 定义程序中各个变量的访问规则。即虚拟机将变量存储到内存或者虚拟机从内存取出变量等细节

(一) 主内存和工作内存

  Java规定,所有变量存储在主内存中,每个线程都有自己的工作内存。所以,java内存模型分为主内存和工作内存两部分。Java内存的划分和JVM运行内存的划分是从不同的层面进行的划分。如果真需要关联。主内存主要是堆内存。工作内存主要是栈内存。

  1.主内存线程共享的内存区域。主要是堆和方法区【堆-容易理解,线程共享的数据大多数对象、方法区-线程共享的可能是运行时常量池中的字面常量等】。

  2.工作内存主要是指线程私有的内存。主要是JAVA虚拟栈和拷贝主内存变量的寄存器。

(二) 内存间的交互操作

  内存间的交互操作Lockreadloaduseassignstorewriteunlock上面8个命令,代表了对主内存变量锁定lock、从主内存读取到工作内存read、到将读取到工作内存的数据赋值给工作内存中的变量load、再到将工作内存的变量传递给执行引擎use、将执行引擎中变量赋值给工作内存变量assign、将工作区变量读取到主内存store、将工作内存中读取到主内存的变量赋值给主内存变量write、对主内存变量进行解锁unlock

(三) Volatile修饰的变量的特殊规则

  其中一个线程对volatile修饰的变量,对其它线程立即可见。【实现的方法:修改之后,立即同步到主内存,其次,如果工作内存需要使用该变量,不使用原有的内存数据,立即从主内存同步】在条件苛刻的情况下【volatile修饰的变量 运算不依赖当前值】,也可以保证操作的原子性。Volatile修饰的变量可以阻止“指令重排序”的优化

(四) 线程的原子性、可见性和有序性

  线程有三个特性。原子性、可见性、有序性

1) 原子性

  一个操作,要么执行、要么没有执行。

2) 可见性

  一个线程对变量的修改完成后、其它线程立即能发现改变。

3) 有序性

  如果在本线程观察,所有操作是有序的、如果在其它线程观察,所有操作是无序(乱序)的。对共享代码必须按照代码的逻辑顺序执行,避免因为代码重排优化而导致错误。

(五) 先行发生原则

  先行发生原则代码先行原则体现的是两个操作之间的偏序关系。假设操作A先于操作B执行。那么,操作B运行之前能观察到操作A的影响。

  实现先行发生原则有

   synchronizedlockvolatile

五. 线程

(一) 线程的实现

1.使用内核线程的实现

  内核线程是由操作系统支持的线程。这种线程的切换是通过操纵调度器scheduler对线程进行调度。用户一般不会直接使用内核线程,而是使用内核线程的高级接口。--轻量级线程(LWP)。

  每个线程都是一个独立的调度单元。所以,单个线程的阻塞不会影响整个线程的进行。但是,挂起或者启动操作需要CPU在用户模式和内核模式之间切换,所以,代价比较高。

2.用户线程的实现

  完全建立在用户空间的线程库上的线程。线程的建立,同步、调度都可以完全在用户模式下完成。不需要内核的帮助。

(二) 线程的调度

  线程的调度本质是给线程分配处理器权限的过程

1.协同式线程调度

  线程执行时间由线程自己控制。自己工作完成之后,在主动通知系统,切换到其它线程执行。

  优点:实现简单,没有线程同步问题。

  缺点:如果线程阻塞,不能主动退出,一直占用处理器。

2.抢占式线调度

  对于多个线程,由系统来分配处理时间。线程的切换不由线程本身控制(thread.yield 可以主动让出自己对处理器的使用权限,但是获取权限还是需要系统给分配)。JAVA使用的线程调度方式就是抢占式

(三) 用户模式和内核模式

1.内核模式和用户模式的区别

  内核模式下:代码具有对硬件和内存有直接控制权,如果有错误,错误是灾难性的。

  用户模式下:用户对硬件和内存是没有直接访问权限的。对内存和硬件的访问是通过系统接口来进行访问的、用户模式下,错误是可以恢复的。

2.为什么要设置用户模式?

  设置用户模式主要的目的是控制应用程序对系统资源无限制的访问。因为,通过系统接口对系统硬件和内存的访问,可以进行验证。

3.应用中的模式切换

  用户模式切换到内核模式,再回到用户模式的流程通常应用软件是在用户模式下进行的。

  假设用户模式下需要访问硬件或者内存。需要调用系统接口进行访问,首先会参数进行验证。其次,CPU切换到内核模式下进行处理。完成后再切换到用户模式,继续执行应用程序软件。

(四) 线程安全

1) JAVA线程安全的5个种类

  从线程安全的安全程度角度从强到弱的角度来分析。共享数据分为5类。

  1.不可变

    不可变的常量或者对象。因为不可变,多个线程最多只能读取,不能修改其值,所以,线程安全。

  2.绝对线程安全

    不管运行情况如何,调用者都不需要额外的同步手段就能达到线程安全。

  3.相对线程安全

    通常来说是线程安全的【方法都加了synchronize】。单对于一些特定的调用,需要调用端额外的调用手段。比如vector

  4.线程兼容

    本身不是线程安全的【方法没有同步方案】。需要调用端同步来保证安全性。

  5.线程对立

    无论是否在调用端采取错误,都不可能在多线程中并发使用代码。

2) 实现线程安全的方法

  实现线程安全的方法。

  1.互斥同步

    互斥同步也称之为阻塞同步。互斥是因,同步是果。互斥是方法,同步是目的。

    互斥:多个线程并发访问共享数据,保证一个时刻只有一个线程能访问共享数据。

    互斥同步是基于悲观锁的角度来处理的一个思路。认为必须加锁保证互斥,才能解决多线程的竞争。互斥同步的两种方案:synchronize【在字节码中,添加两个关键字】和ReentrantLock

  2.非阻塞同步

  基于检测冲突的乐观策略。认为不会发生冲突。直接对数据进行访问。如果发生冲突,再进行补救措施【不断重试,直到成功】。这种操作用硬件指令来完成。如下:

  l 测试并设置

  l 获取并增加

  l 交换

  l 比较并交换

  l 加载链接/条件存储

  3.无同步方案

  没有数据竞争,不需要同步方案。

(五) 锁优化

1  自旋和自适应自旋线

  程状态的切换需要在用户态和核心态之间切换。代价比较大。因此,可以尝试等待一段时间后,如果仍然未申请到需要的资源,在切换用户状态。其中等待一段时间,进行忙运行,称之为自旋。如果等待的时间根据运行自行调整就是自适应自旋。

2  锁取消

  根据判断,如果永远不会发生竞争,则取消锁机制。

3  锁粗化

  如果一段代码多次进行加锁和取锁操作。可以考虑将锁的范围扩大到整个循环之外。--核心思想是 加锁和取锁消耗比较大,所以,加锁取锁操作尽量不要出现在循环中。可以放在循环外部。

原文地址:https://www.cnblogs.com/maopneo/p/13947690.html