Java内存模型,Java内存工作原理及JVM常用配置参数

JAVA内存基本结构

基本组成结构

JAVA内存模型主要有JVM运行时数据区,字节码执行引擎,类加载系统,本地方法接口及库等部分组成

其中JVM运行时数据区又由如下部分组成:

线程私有空间:虚拟机栈,本地方法栈,程序计数器

线程共享空间:堆内存/Heap,方法区(元空间)/Perm区,直接内存

Java堆内存/Heap:主要指Young代内存(Eden, s1, s2)+Old代内存,可以用Xms,Xmx指定整个区的最小和最大大小; 对Young区可以使用Xmn指定大小

 

从另一个角度看JVM内存

堆内存(Heap):包含新生代和老年代,新生代被分为Eden区和两个Survivor区。

非堆内存(Non-Heap):非堆内存包含:

1.Thread Stacks:所有运行的线程的空间。可以使用 -Xss 参数设置每个线程栈的大小;

2.Metaspace:元空间,用来实现方法区,可以通过设置 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数设置 Metaspace 大小;

3.Code Cache:用来保存由JIT(即时编译器)产生的native code 的内存区域,CodeCache 是用来保存由JIT 产生的native code 的内存区域,它是独立于JVM heap的非堆内存。即时编译是指在运行时,把频繁执行的 bytecode 转换成操作系统相关的机器码,这样程序执行时就不需要解释执行,可以提高程序性能。可通过参数-XX:ReservedCodeCacheSize设置CodeCache的大小,或者使用-XX:+UseCodeCacheFlushing参数在CodeCache在接近满时,释放一半Cache。关于即时编译后面再做详细分析。

4.Compressed Class Space:使用XX:CompressedClassSpaceSize来设置压缩类空间的最大内存;

5.Direct NIO Buffers:NIO所使用的直接内存,既参数MaxDirectMemorySize所指定的值。

Java虚拟机栈(Java Virtual Machine Stacks)

虚拟机栈是线程私有的,栈使用的内存不需要保证是连续的,主要由栈帧组成。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

Java虚拟机规范即允许Java虚拟机栈被实现成固定大小(-Xss),也允许通过计算结果动态来扩容和收缩大小。

如果采用固定大小的Java虚拟机栈,那每个线程的Java虚拟机栈容量可以在线程创建的时候就已经确定,并写入方法表的Code属性之中。

Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

在运行时的线程中,只有当前栈帧有效(Java虚拟机栈中栈顶的栈帧),与当前栈帧相关联的方法称为当前方法。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。

虚拟机栈可以出现的异常:

    1. StackOverFlowError 当前线程请求栈的深度超过了虚拟机栈的最大深度。
    2. OutOfMemoryError 当请求栈时,内存用完了且无法拓展时。

本地方法栈/Stack

线程私有,和虚拟机栈类似,区别是主要为 Native 方法服务。

程序计数器

主要有2个作用

  1. 字节码解释器通过改变程序计数器来决定下一条指令,从而控制代码流程。
  2. 多线程情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候可以继续之前的流程,所以程序计数器是私有的。

堆/Heap

虚拟机管理的内存中最大的一块,线程共享。垃圾回收管理的主要区域,所以也成为GC堆。几乎所有的对象实例以及数组都在这分配内存。

方法区/Metadata/Perm区

方法区也是各个线程共享的内存区域。它用于存储已被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

运行时常量池:是方法区的一部分。

class文件中除了有类的版本,字段,方法,接口等描述信息外;还有一项常量池,用于存放编译期生成的各种字面量和符号引用。

JVM遇到一条new指令时,首先将会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化。如果没有就必须先执行相应的类加载过程。

JDK1.7中方法区的实现:在JDK1.7中,使用永久代来实现方法区,永久代实际上是使用了堆中的一部分内存。永久代的大小可以通过-XX:PermSize 和 -XX:MaxPermSize设置,比如-XX:PermSize=64m -XX:MaxPermSize=128m;当永久代内存不够时会抛出OutOfMemoryError:PermGen异常。

JDK1.8中方法区的实现:在JDK1.8中,去掉了堆中的永久代,使用元空间(使用本地内存)来实现方法区,可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize来设置元空间的大小,比如-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m,如果没有设置,元空间大小会根据应用程序在运行时的需求进行调整,如果设置过小,会抛出OutOfMemoryError:Metadata space异常。

废弃永久代的原因:

一. 移除永久代可以促进HotSpot JVM与JRockit VM的融合,因为JRockit没有永久代;

二. 由于永久代内存经常不够用或发生内存泄露,爆出OutOfMemoryError:PermGen异常。

 

对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。
当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。 

Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。这里的变量值包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。
Java内存模型规定了所有的变量都存储在主内存中(物理上它是虚拟机内存的一部分)。每条线程还有自己的工作内存,可与前面讲的处理器高速缓存类比,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。

这里所讲的主内存、工作内存和Java内存区域中的Java堆、栈、方法区并不是同一个层次的对内存的划分,这俩者基本没有任何关系的。如果俩者一定要勉强对应起来,那么从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。或者说。主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存先存储于寄存器与高速缓存中,因为程序运行时主要访问的是工作内存。

内存间的交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的技术细节,Java内存模型中定义了以下8种操作来完成:

  • lock(锁定):作用于主内存的变量,将一个变量标记为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个需要给变量赋值的字节码指令时将会执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入到主内存的变量中。

此外还有一些使用限制。
在新的描述中,将Java内存模型的操作简化为read、write、lock、unlock四种,但这只是语言描述的等价化简,Java内存模型的基础设计并未改变,

volatile型变量的特殊规则

当一个变量被定义成volatile之后,它将具备俩项特性:

  • 第一项是保证此变量对所有线程的可见性,这里的可见性指的是当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。它不能保证对变量操作的原子性。
  • 第二项就是禁止指令重排序优化

假定t表示一个线程,v和w分别表示俩个volatile型变量,那么在进行操作的时候需要满足如下规则:

  • 只有当线程t对变量v执行的前一个动作是load的时候,线程t才能对变量v执行use动作;并且,只有当线程t对变量v执行的后一个动作是use的时候,线程t才能对变量v执行load操作。线程t对变量v的use动作可以认为是和线程t对变量v的load、read动作相关联的,必须连续且一起出现。

这条规则要求在工作内存中,每次使用v前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量v所做的修改。

  • 假定动作a是线程t对变量v实施use或assign动作,假定动作f是和动作a相关联的load或者store动作,假定动作p是和动作f相应的对变量v的read或write动作;与此类似,假定动作b是线程t对变量w实施的use或assign动作,假定动作g是和动作b相关的load或store动作,假定动作q是和动作g相应的对变量w的read或write动作,如果a先于b,那么p先于q。

这条规则要求volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同。

long和double型变量的特殊规则

在Java内存模型中:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为俩次32位的操作来进行。即允许虚拟机自行选择是否要保证64位数据类型的操作原子性。即“long和double的非原子性协定”。

原子性、可见性和有序性

原子性

定义:对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应的我们就称该操作是具有原子性的。

1.原子操作是对于多线程而言的,对于单一线程,无所谓原子性。有点多线程常识的朋友这个都应该知道,但也要时刻牢记。

2.原子操作是针对共享变量的。因此,涉及局部变量(如方法中的变量)我们是没必要要求它具有原子性的,

3.原子操作是不可分割的。(我们要站在多线程的角度)指访问某个共享变量的操作从其执行线程之外的线程来看,该操作要么已经执行完毕,要么尚未发生,其他线程不会看到执行操作的中间结果。学过数据库的朋友应该很熟悉这种原子性。那么,站在访问变量的角度,我们可以这样看,如果要改变一个对象,而该对象包含一组需要同时改变的共享变量,那么,在一个线程开始改变一个变量之后,在其它线程看来,这个对象的所有属性要么都被修改,要么都没有被修改,不会看到部分修改的中间结果。(这只是最简单的一种解释,以后我们还会讲到i++以及初始化等操作)

Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store和write这6个;对于基本数据类型的访问,读写都是具备原子性的。如果应用场景需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足需求,在高层次的字节码指令是monitorenter和monitorexit。这俩个字节码反映到Java代码中就是同步块--synchronized。

可见性

可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此的。

普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。除了volatile关键字,Java中synchronized和final也可以实现可见性。

有序性

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程所有的操作都是无序的。

前半句是指线程内似表现为串行的语义,后半句是指指令重排序现象和工作内存与主内存同步延迟。

Java语言中提供了volatile和synchronized两个关键字来保证线程之间操作的有序性:

volatile关键字本身就包含了禁止指令重排序的语义。

synchronized通过锁机制实现。

先行发生原则

先行发生是Java内存模型中定义的俩项操作之间的偏序关系,比如说操作a先行发生于操作b,其实就是说在发生操作b之前,操作a产生的影响能被操作b观察到,“影响”保证修改了内存中共享变量的值、发送了消息、调用了方法等。
下面是Java内存模型中一些“天然的”先行发生关系:

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则:对同一个volatile变量的写操作先行发生于后面对这个变量的读操作。
  • 传递性:如果操作a先行发生于b,操作b先行发生于操作c,那么就可以得出操作a先行发生于c。
  • 线程启动规则:线程对象的start方法先行发生于此线程的每一个动作。
  • 线程终止规则:线程中所有的操作都先行发生于对此线程的终止检测。
  • 线程中断规则:对线程interrupt方法的调用先行发生于被中断的代码检测到中断事件的发生。
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize方法的开始。


JVM常用配置参数

1.JVM(Java Virtual Machine)

1.-Xms 初始堆大小

2.-Xmx 最大堆大小

3.-Xmn 青年代大小

4.-Xss 每个线程的堆栈大小

5.-XX:+UseParNewGC 青年代垃圾收集方式为并行收集

6.-XX:+UseParallelOldGC 老年代垃圾收集方式为并行收集

7.-XX:ParallelGCThreads 并行收集器的线程数(最好与处理器数目相等)

8.-XX:MaxGCPauseMillis 每次青年代垃圾回收的最长时间(最大暂停时间)

9.-XX:+UseAdaptiveSizePolicy 自动选择青年代区大小和相应的Survivor区比例

10.-XX:GCTimeRatio 设置垃圾回收时间占程序运行时间的百分比

11.-XX:+ScavengeBeforeFullGC Full GC前调用YGC

21.-XX:NewSize 青年代大小(for 1.3/1.4)

22.-XX:MaxNewSize 青年代最大值(for 1.3/1.4)

23.-XX:PermSize 设置持久代/方法区(perm gen)初始值(jdk7)

24.-XX:MaxPermSize 持久代/方法区最大值(jdk7)

25.-XX:MetaspaceSize和-XX:MaxMetaspaceSize 设置方法区大小(jdk8)

26.-XX:SurvivorRatio=n Survivor比例是设置Suvivor区的比例,是s0和s1相对于Eden区域的大小,
年轻代默认的比例是8:1:1,8是Eden,1和1是s0和s1,那么我们可能会根据需要去调整,缩小Eden区的大小,
扩大Survivor区的大小,s0或者s1的大小,那么我们就可以用这个命令了,不让他8:1:1走
27.-XX:NewRatio=n 这里也是设置年轻代的大小,不同的是NewSize给的是具体的单位,比如800M,500M,ratio是
比率,按照多大的比率去设置各代的大小

29.-XX:StringTableSize 字符串常量池大小

30.-XX:MaxDirectMemorySize 直接内存大小

CMS(Concurrent Mark-Sweep)收集器常用配置参数

以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。

对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

在启动JVM参数加上-XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用CMS。

CMS采用的基础算法是:标记—清除。

1.-XX:+UseConcMarkSweepGC 使用CMS内存收集

2.-XX:CMSFullGCsBeforeCompaction 多少次后进行内存压缩

3.-XX:+CMSParallelRemarkEnabled 降低标记停顿

4.-XX+UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的压缩

5.-XX:+UseCMSInitiatingOccupancyOnly 使用手动定义初始化定义开始CMS收集

6.-XX:CMSInitiatingOccupancyFraction=70 使用cms作为垃圾回收,使用70%后开始CMS收集

7.-XX:CMSInitiatingPermOccupancyFraction 设置Perm Gen使用到达多少比率时触发

8.-XX:+CMSIncrementalMode 设置为增量模式

posted @ 2022-12-22 17:16  原子切割员  阅读(299)  评论(0编辑  收藏  举报