知识总结--JVM相关(摘录+转载)

目录

JVM的基本特性

java字节码

jvm结构

运行时数据区

Java的垃圾回收机制

java内存模型(jmm)


java是编译型语言,所写的代码需要编译成静态块后才可以正常使用。而负责编译管理的就是jvm,可以说没有jvm那么java就没有办法正常执行。jvm通过类加载器加载应用,再通过java API执行。java设计成基于虚拟机的语言而不是物理机,正因为如此才有了一次编译到处执行的特性,只要有jvm的机器就可以执行java代码。

JVM的基本特性

  • 基于栈(Stack-based)的虚拟机: 不同于Intel x86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的
  • 符号引用(Symbolic reference): 除基本类型外的所有Java类型(类和接口)都是通过符号引用取得关联,而非显式的基于内存地址的引用。
  • 垃圾回收机制: 类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。
  • 通过明确清晰基本类型确保平台无关性: 像C/C++等传统编程语言对于int类型数据在同平台上会有不同的字节长度。JVM却通过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而做到平台无关。
  • 网络字节序(Network byte order): Java class文件的二进制表示使用的是基于网络的字节序(network byte order)。为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序。

市面上有众多版本的jvm,目前jdk默认jvm为hotspot,也是公认最成熟的虚拟机。jvm会将java代码编译为字节码,字节码是部署java程序的最小单元。

java字节码

java字节码是运行于用户语言和机器语言的中间语言。它是JVM的基本元素,JVM本身就是一个用于执行Java字节码的执行器。Java编译器并不会把像C/C++那样把高级语言转为机器语言(CPU执行指令),而是把开发者能理解的Java语言转为JVM理解的Java字节码。因为Java字节码是平台无关的,所以它可以在安装了JVM(准确的说,是JRE环境)的任何硬件环境执行,即使它们的CPU和操作系统各不相同(所以在Windows PC机上开发和编译的class文件在不做任何调整的情况下就可以在Linux机器上执行)。编译后文件的大小与源文件大小基本一致,所以比较容易通过网络传输和执行Java字节码。class文件是基于二进制的文件,可以通过javap命令来查看class中的内容。javap -c 可以查看具体文件的编译后内容。在Java字节码中有4个表示调用方法的操作码: invokeinterfaceinvokespecialinvokestaticinvokevirtual 。他们每个的含义如下:

  • invokeinterface: 调用接口方法
  • invokespecial: 调用初始化方法、私有方法、或父类中定义的方法
  • invokestatic: 调用静态方法
  • invokevirtual: 调用实例方法

下表列出了Java字节码中类型表示。

表1: Java字节码里的类型表示

Java 字节码类型描述
Bbyte单字节
CcharUnicode字符
Ddouble双精度浮点数
Ffloat单精度浮点数
Iint整型
Jlong长整型
L引用classname类型的实例
Sshort短整型
Zboolean布尔类型
[引用一维数组

表2: Java代码的字节码示例

java 代码Java 字节码表示
double d[][][][[[D
Object mymethod(int i, double d, Thread t)mymethod(I,D,Ljava/lang/Thread;)Ljava/lang/Object;

在jvm中规定方法的定义不能超过65535个字节。

jvm结构

java程序执行过程:编译源代码(.java文件)->编译生成字节码文件(.class文件)->类加载器加载字节码文件(.class文件)->执行引擎找到执行入口方法(main())执行其中的字节码指令。

JVM启动后,对操作系统来说,JVM是一个的进程,这个进程的基本结构如上图所示。它包括:类加载器子系统、运行时数据区、执行引擎和本地方法接口

运行时数据区

运行时数据区是JVM从操作系统申请来的堆空间和操作系统给JVM分配的栈空间的总称。JVM为了运行Java程序,又进一步对运行时数据区进行了划分,划分为Java方法区、Java堆、Java栈、PC寄存器、本地方法栈等,这里JVM从操作系统申请来的堆空间被划分为方法区和Java堆,操作系统给JVM分配的栈空间构成Java栈。Java方法区,在分代垃圾回收时也被称为永久区,包括了已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据,它实际上构成了Java程序的方法区和数据区。垃圾回收器也会对这部分内存进行回收,比如常量池的清理和类的卸载。java堆包括新生代和老年代,新生代包括Edge区两个Survivor区(from Survivor 和 To Survivor)。垃圾回收主要是回收java堆中的内存,可以通过-Xmx和-Xms来调整大小。

Java的垃圾回收机制

Java一个令人称赞的特性就是引入了垃圾回收机制,在C中内存管理一直是令人头疼的问题,而这个问题java程序员不需要担心,因为JVM会自动管理内存,java的对象不再有作用域的问题,只有对象的引用才有,垃圾回收可以有效的防止内存泄漏,有效的使用空闲内存。

内存泄露:指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,也称为“对象游离”。垃圾回收器算法包括:引用计数法、标记-清除算法、标记-整理算法、复制算法和分代算法,不同的垃圾回收器使用的算法不同,目前jvm中有Serial收集器、Serialold收集器、ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器、Concurrent Mark Sweep(CMS)收集器、G1收集器 7种收集器。

Serial收集器是新生代串行收集器,是历史最悠久的收集器。它是一个单线程收集器,在进行垃圾回收时会stop the world,在限于单Cpu机器上使用可以得到最高的效率。采用复制算法。

Serial Old收集器是老年代的串行收集器,是Serial收集器老年代版本。与serial收集器一样同样是单线程收集器,它使用标记整理算法,常作为CMS备用收集器。

ParNew是新生代并行收集器,它是serial的多线程版本,除了是用多线程进行垃圾回收之外其他的控制参数与serial相同。是cms默认适配的新生代收集器。可以使用-XX:+UseParNewGc来指定启用。他在多Cpu下可以发挥更高效率,并且是新生代唯一可以和cms搭配使用的垃圾收集器。采用停止-复制算法。

Parallel Scavenge也是新生代并行收集器,与ParNew区别不大,比起ParNew它更关注cpu吞吐量(吞吐量=运行代码时间/(运行代码时间+垃圾收集时间)),较高的吞吐量可以更好的利用cpu。-XX:MaxGCPauseMillis配置最大垃圾收集停顿时间,-XX:GCTimeRatio配置吞吐量大小。它有一个自适应调节参数(-XX:+UseAdaptiveSizePolicy),当这个参数打开后,无需手动指定新生代大小(-Xmn)、Eden和Survivor比例(-XX:SurvivorRatio)、晋升老年代年龄限制(-XX:PretenureSizeThreshold)等细节参数,虚拟机会动态调节这些参数来提供最适合的停顿时间或最大吞吐量。可以通过-XX:+UseParallelGC来指定,并且可以采用-XX:ParallelGCThread来指定线程数。采用的是停止-复制算法。

Parallel Old是Parallel Scavenge的老年代版本,为了配合Parallel Scavenge的面向吞吐量的特性而开发的对应组合。采用停止-复制算法。

CMS是一种以最小停顿时间为目标的收集器。它采用标记清理算法,整个过程分为4个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)和并发清除(CMS concurrent sweep)。其中初始标记、重新标记仍然是”Stop The World”,初始标记仅仅是标记一下GC Roots能直接关联的对象,并发标记进行GC Roots Tracing的过程,重新标记为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那部分对象,这个阶段停顿时间比初始标记阶段稍长一些,但比并发标记时间短。它是并发收集,重视停顿时间。

G1收集器是当前最先进的垃圾回收器,采用分代算法,它可以在不影响吞吐量的前提下达到低停顿,是cms很好的替换收集器。它最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。它将整个堆分成若干大小相等的内存区域,每个区域不会确定为某个代服务,可以按需在年轻代和老年代切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。回收过程分为四个部分:初始标记、并发标记、最终标记、筛选回收。与cms类似,初始标记需要STW,并发标记不需要,最终标记就是做一些小修改,需要STW,而筛选回收则有些不同,在众多的region中,每个region可回收的空间各不相同,但是回收所消耗的时间是需要控制的,不能太长,因此G1就会筛选出一些可回收空间比较大的region进行回收,这就是G1的优先回收机制。这也是保证了G1收集器能在有限的时间内能够获得最高回收效率的原因。通过-XX:MaxGCPauseMills=50毫秒设置有限的收集时间。 

总的来说在选择垃圾回收器时,如果内存够大,建议使用G1收集器,如果内存在6G以内建议使用cms+pn,单线程使用serial,高吞吐量使用Parallel。

默认GC组合:

    

可选的GC组合:

    

java内存模型(jmm)

java内存模型主要目标是定义程序中的变量,(此处所指的变量是实例字段、静态字段等,不包含局部变量和函数参数,因为这两种是线程私有无法共享)在虚拟机中存储到内存与从内存读取出来的规则细节,Java 内存模型规定所有变量都存储在主内存中,每条线程还有自己的工作内存,工作内存保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在自己的工作内存中进行而不能直接读写主内存的变量,不同线程之间无法相互直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

Java 内存模型对主内存与工作内存之间的具体交互协议定义了八种操作,具体如下:

  • lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占状态。

  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read(读取):作用于主内存变量,把一个变量从主内存传输到线程的工作内存中,以便随后的 load 动作使用。

  • load(载入):作用于工作内存变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时执行此操作。

  • assign(赋值):作用于工作内存变量,把一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个需要给变量进行赋值的字节码指令时执行此操作。

  • store(存储):作用于工作内存变量,把工作内存中一个变量的值传递到主内存中,以便后续 write 操作。

  • write(写入):作用于主内存变量,把 store 操作从工作内存中得到的值放入主内存变量中。

如果要把一个变量从主内存复制到工作内存就必须按顺序执行 read 和 load 操作,从工作内存同步回主内存就必须顺序执行 store 和 write 操作,但是 JVM 只要求了操作的顺序而没有要求上述操作必须保证连续性,所以实质执行中 read 和 load 间及 store 和 write 间是可以插入其他指令的。

Java 内存模型还会对指令进行重排序操作,在执行程序时为了提高性能编译器和处理器经常会对指令进行重排序操作,重排序主要分下面几类:

  • 编译器优化重排序:编译器在不改变单线程程序语义的前提下可以重新调整语句的执行顺序。

  • 指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统重排序:由于处理器使用缓存和读写缓冲区使得加载和存储操作看上去可能是在乱序执行。

其实 JMM 内存模型是围绕并发编程中原子性、可见性、有序性三个特征来建立的,关于原子性、可见性、有序性的理解如下:

  • 原子性:就是说一个操作不能被打断,要么执行完要么不执行,类似事务操作,Java 基本类型数据的访问大都是原子操作,long 和 double 类型是 64 位,在 32 位 JVM 中会将 64 位数据的读写操作分成两次 32 位来处理,所以 long 和 double 在 32 位 JVM 中是非原子操作,也就是说在并发访问时是线程非安全的,要想保证原子性就得对访问该数据的地方进行同步操作,譬如 synchronized 等。

  • 可见性:就是说当一个线程对共享变量做了修改后其他线程可以立即感知到该共享变量的改变,从 Java 内存模型我们就能看出来多线程访问共享变量都要经过线程工作内存到主存的复制和主存到线程工作内存的复制操作,所以普通共享变量就无法保证可见性了;Java 提供了 volatile 修饰符来保证变量的可见性,每次使用 volatile 变量都会主动从主存中刷新,除此之外 synchronized、Lock、final 都可以保证变量的可见性。

  • 有序性:就是说 Java 内存模型中的指令重排不会影响单线程的执行顺序,但是会影响多线程并发执行的正确性,所以在并发中我们必须要想办法保证并发代码的有序性;在 Java 里可以通过 volatile 关键字保证一定的有序性,还可以通过 synchronized、Lock 来保证有序性,因为 synchronized、Lock 保证了每一时刻只有一个线程执行同步代码相当于单线程执行,所以自然不会有有序性的问题;除此之外 Java 内存模型通过 happens-before 原则如果能推导出来两个操作的执行顺序就能先天保证有序性,否则无法保证,关于 happens-before 原则可以查阅相关资料。推荐图书:深入理解java虚拟机

对于并发操作而言,一定要保证它的原子性、可见性和有序性,否则就无法保证得到预期效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jet-W

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值