JVM基础知识-part1
本文涉及到的内容如下:
- java内存模型
- java内存管理
- java堆和栈
- java垃圾回收
一、java内存模型
-
什么是内存模型?
首先,在多核系统中,处理器一般都会有一层或者多层缓存(对于普通单核系统来说,也需要缓存协调CPU和其他硬件比如IO、网络、内存速度差异的问题;多处理器都要遵循一定的协议保证内存各个处理器高速缓存和主内存中的数据一致性)来提升CPU性能:
- 加速数据的访问(数据距离处理器更近)
- 降低共享内存在总线上的通讯(本地缓存能完成许多内存操作)但是也带来一些挑战,比如两个CPU同时检查相同的内存地址会发生什么?(因为处理器优化和指令重排的存在)
所以,java内存模型(JMM)是java虚拟机规范定义的用来屏蔽java程序在各种不同的硬件和操作系统对于内存访问的差异,实现java程序在不同平台都能达到内存访问的一致性。JMM主要是目标就是定义程序中变量(包含实例字段、静态字段、构成数组对象的元素,不包括局部变量和方法参数–线程私有)的访问规则,虚拟机内将变量存储到主内存或者从主内存取出这样的底层细节。
大部分其他语言,像C和C++没有被设计成支持多线程,对于发生在编译器和处理器的重排序行为的保护机制是严重依赖于程序中使用的现场库等的。
-
基本概念
-主内存:JVM规定所有变量(非程序中的变量)都必须在主内存中产生。
-工作内存:JVM每个线程都有自己的工作内存,保存线程需要的变量在主内存中的副本,不同线程不可以访问对象的工作内存,所以线程之间变量值的传递需要通过主内存作为中介实现。

-
工作内存和主内存之间的交互方式-八种原子性操作
- lock:作用于主内存的变量,同一时间只有一个线程可以锁定
- unlock:释放后其他线程才可对该变量进行锁定
- read:把一个主内存变量的值传输到线程的工作内存
- load:把read操作从主内存中读取的变量的值放到工作内存的变量副本中
- use:把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作。
- assign:表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作。
- store:把工作内存中的一个变量的值传递给主内存
- write:把store操作从工作内存中得到的变量的值放入主内存的变量
-
内存模型的实现
(1)原子性:- synchronized:monitorenter和monitorexit
(2)可见性 :一线程修改一个变量的值后,其他线程立即可以感知到这个值的修改。
- volatile:变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取
- synchronized:通过unlock之前必须把变量同步回主内存实现的
- final:初始化后不可以修改,只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。
(3)有序性:
- synchronized:一个变量在同一时刻只能被一个线程对其进行lock操作
- volatile:禁止指令重排

-
三个关键字:
-volatile:volatile关键字可以保证直接从主存中读取一个变量(使用Lock前缀的指令禁止线程本地内存缓存,保证不同线程之间的内存可见性),如果这个变量被修改后,总是会被写回到主存中去。 volatile详情-synchronized:由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
-final:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(没有被允许逸出),那么在其他线程就能看见final字段的值。
二、java内存管理
- java内存划分:

(1)程序计数器:小块内存空间,通过改变这个计数器的值来选取下一条需要指向字节码指令。在一个时间,一个处理器/内核只会执行一条线程指令,为在线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。该区域是唯一一个没有任务OOM的区域。
(2)虚拟机栈:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame,栈帧是方法运行期的基础数据结构)用于存储局部变量表(只在当前函数调用中有效,函数调用结束,随着函数栈帧的销毁而销毁。可以存储基本数据类型、对象引用reference和returnAddress指向一条字节码指令的地址)、操作栈(保存计算过程中间结果)、动态链接(每个栈帧包含一个指向运行时常量池中该栈帧所属非法的引用)、方法出口(在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行)等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈栈从入栈到出栈的过程。有两种异常可能出现:StackOverflowError(执行 Java 方法是会进行压栈的操作,在栈栈会保存局部变量、操作栈和方法出口等信息。JVM 规定了栈的最大胜读,如果线程请求执行方法时栈的深度大于规定的深度,就会抛出栈溢出异常 StockOverflowError)和OutOfMemoryError(如果虚拟机在扩展时无法申请到足够的内存,就会抛出内存溢出异常 OutOfMemoryError)。
(3)本地方法栈:虚拟机栈为 Java 服务,而本地方法栈为 native 方法服务。异常也是有两种,StackOverflowError 和 OutOfMemoryError。
(4)java堆:JVM管理的内存中最大的一块,在虚拟机启动时创建。所有的对象实例以及数组都要在堆上分配(但是随着 JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发送,也不那么“绝对”了)。该区域也是GC主要区域,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为新生代+老年代,再细致一点,可以分为Eden空间、From Survivor空间、To Survivor空间(空间分配比例是8:1:1)。
(5)方法区:存储已经被虚拟机加载的数据(类信息、常量、静态变量、即时编译器编译后的代码)。方法区的大小决定系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样抛出内存溢出异常 OutOfMemoryError。方法区又可以分为运行时常量池(存放编译期生成的各种字面量和符号引用)和直接内存(可以用Native函数直接分配在物理内存中,并不占用堆空间,过一个存储在Java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免在Java堆和Native堆中来回复制数据)两部分。

- 对象访问实例
Object obj=new Object();
首先,Object obj会反应到Java栈的本地变量表中,作为一个reference类型数据出现。而new Object()会反映到java堆中,形成一块存储Object类型所有实例数据值的结构化内存。Java堆中还包含能查找到此对象类型数据的地址信息(存储在方法区内)。reference主流的访问方式有:句柄(Java堆中有一块内存作为句柄池,reference中存储的是对象的句柄地址。句柄优势是reference存储的是稳定的句柄,对象被移动的时候只会改句柄中的实例数据指针,reference本身不需要被修改)和直接指针(优势:速度很快,节省一次指针定位的时间开销)。

三、java垃圾回收
-
可达性分析:GC Roots为起点,这些节点开始向下搜索,当一个对象到 GC Roots 没有与任何引用链相连时,则证明此对象是无用的。
(1)GC Roots类型- 虚拟机栈
- 方法区:类静态属性引用的对象+常量引用的对象
- 本地方法栈
(2)可达性分析案例:object6, object7, object8可回收

-
GC的流程:
(1)找出堆中活着的对象
(2)释放死对象占用的资源
(3)定期调整活对象的位置 -
GC常用的算法:
(1)标记-清除:

缺点:会产生大量不连续的内存碎片,内存碎片大多会导致当程序需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发 GC。
(2)标记-复制:可用内存按容量划分为大小相同的两块,每次只使用其中的一块,存活对象复制过去。

浪费空间;在对象存活率高时,要进行较多的复制操作,这时效率低。
(3)标记-整理:所有存活的对象向一端移动,然后直接清除掉边界外的内存。

(4)分代收集算法:根据对象存活周期的不同将内存划分为几块。

- 新生代:
大多数情况下,对象都是在伊甸区中分配的,当伊甸区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC。当伊甸区的空间用完时,GC 会对伊甸区进行垃圾回收,然后把伊甸区剩下的对象移动到幸存0区。如果幸存0区满了,GC对该区域进行垃圾回收,然后把该 区域剩下的对象移动到幸存1区。如果幸存1区满了,GC会对该区域进行垃圾回收,然后把幸存1区中的对象移动到养老区。 - 养老代:保存从新生区筛选出来的 Java 对象。当幸存1区尝试移动对象到养老区,但是发现空间不足时,虚拟机会发起一次 Major GC(慢)。大对象直接进入养老区,比如很大的数字和很长的字符串。
- 永久代:存放 JDK 自身携带的 Class Interface 元数据。存储的是运行环境必需的类信息,被装载进该区域的数据是不会被垃圾回收器回收掉的,只有 JVM 关闭时才会释放此区域的内存。
(5)New Generation的GC有以下几种:Serial、ParallelScavenge、ParNew。均是在Eden Space分配不下时,触发GC。
(6)Old Generation的GC有以下几种:Serial Old、Parallel、CMS。详情解释 - 新生代:
四、其他
(1)对网站/服务访问慢的原因:
- 内存:垃圾收集占用cpu;放入了太多数据,造成内存泄露(java也是有这种问题的)
- 线程死锁
- I/O速度太慢
- 依赖的其他服务响应太慢
- 复杂的业务逻辑或者算法造成响应的缓慢
(2)内存泄漏/内存溢出:
- 内存泄漏:程序在申请内存后,对象没有被GC所回收,它始终占用内存,内存泄漏的堆积最终会造成内存溢出。
- 内存溢出:程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出。
本文深入讲解JVM基础知识,包括Java内存模型、内存管理、垃圾回收等核心概念。解析JMM如何确保多线程环境中内存访问的一致性,探讨Java内存布局及垃圾回收策略。
419

被折叠的 条评论
为什么被折叠?



