战狂粗人张

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

一.Java内存模型

Java程序内存的分配是在JVM虚拟机内存分配机制下完成。

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。

它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,

存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

 

(1)java定义内存模型的目的是:为了屏蔽各种硬件和操作系统的内存访问之间的差异。

(2)java内存模型规定了所有的变量都存储在主内存中,每条线程拥有自己的工作内存,工作内存保存了主内存中变量的副本。

(3)线程对变量操作只能在工作内存中进行,不能直接读写主内存的变量。

(4)不同线程之间的变量访问需要通过主内存来完成。

 

1、主内存和工作内存之间的交互

2、对于 volatile 型变量的特殊规则

volatile关键字,使得变量的更新在各个工作内存中都是实时可见的。在DCL的单例模式中有运用到!

关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。

一个变量被定义为 volatile 的特性:

(1)保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。

如果不符合 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值 和 变量不需要与其他的状态变量共同参与不变约束

就要通过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。

(2)禁止指令重排序优化。

通过插入内存屏障保证一致性。

 

3、对于 long 和 double 型变量的特殊规则

Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,

有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,

即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。

4、先行发生原则

也就是 happens-before 原则。这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系。

天然的先行发生关系:

java内存模型和java运行时数据区域的关系:主内存对应着java堆,工作内存对应着java栈。

 

Java 的内存模型由3个代组成,各个代的默认排列有如下图(适用JDK1.4.*  到 JDK6):

Java 的内存模型分为:Young(年轻代);Tenured(终身代);Perm(永久代)

有些旧版本也叫作:New;Old;Perm

叫法不同,表达的意思却是基本相同。

注意Young(年轻代)还可以分为Eden区和两个Survivor区(from和to,这两个Survivor区大小严格一至),

新的对象实例总是首先放在Eden区,Survivor区作为Eden区和 Tenure(终生代)的缓冲,可以向 Tenure(终生代)转移活动的对象实例。

Tenure(终生代)中存放生命周期长久的实例对象,但并不是如它的名字那样是终生的,里面的对象照样会被回收掉。

Young和Tenure共同组成了堆内存。

Perm(永久代)则是非堆内存的组成部分。主要存放加载的Class类级对象如class本身,method,field等等。

 

二.JVM内存模型划分

计算机中所有程序都是再内存中运行的,在程序运行的过程中,需要不断的将内存的逻辑地址和物理地址进行映射,找到相应的命令和数据。

作为操作系统进程,Java也会面临内存限制。即内存架构所提供的可寻址地址空间,在32位处理器中,这个可寻址空间位2^32可寻址范围,64可寻址空间为2^64

在操作系统中,定义了两块空间,即内核空间和用户空间,Java运行在用户空间上。

内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。

JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。

不同的JVM对于内存的划分方式和管理机制存在着部分差异。

根据JVM规范,JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。

JVM内存运行时结构图:

在JVM中,可以从线程私有和共享来划分它的内存模型。

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

共享的:方法区(JDK1.8已经去掉了方法区),Java堆。

 

jdk1.7之前的jvm内存区域,拥有永久代:

jdk1.8及之后的jvm内存区域,元空间取代了永久代:

 

 

 

JDK1.6,JDK1.7,JDK1.8不同版本JVM内存模型区别:

相对于jdk1.6,jDK1.7将运行时常量池从方法区移除到堆内存。

相对于JDK1.6,JDK1.8直接将方法区去掉,在本地内存中新增元数据空间。运行时常量池仍然在堆中。元数据区存放类加载信息。

JDK1.8为什么要移除方法区?

(1)字符串存在永久代中,容易出现性能问题和内存溢出。

(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

(3)永久代会为GC带来不必要的复杂度,并且回收效率偏低。

 

1、程序计数器(Program Counter Register )

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器?

对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,

为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

 

2、 Java栈(虚拟机栈,VM Stack)

同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型。

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。

每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出】

栈帧: 是用来存储数据和部分过程结果的数据结构。

栈帧的位置: 内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]

栈帧大小确定时间: 编译期确定,不受运行期数据影响。

通常有人将java内存区分为栈和堆,实际上java内存比这复杂,这么区分可能是因为我们最关注,与对象内存分配关系最密切的是这两个。

平时说的栈一般指局部变量表部分。

局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。

它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址。

需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,

这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。

虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

 

3、本地方法栈(Native Method Stack)

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,

打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。

 

4、堆(Heap)

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)。

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。

这些变量的指针可以被全局所引用,或者其其它线程所引用。

java堆又被分成了年轻代,老年代;年轻代进一步可以划分为Eden空间,From Survivor空间、To Survivor空间。

 

 当使用new关键字分配对象时,就是在java堆中生成对象。

注意:

它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,

Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)。

 

5、方法区(Method Area)

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,

从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】

jdk1.7开始逐步去永久代。从String.interns()方法可以看出来
String.interns()
native方法:作用是如果字符串常量池已经包含一个等于这个String对象的字符串,则返回代表池中的这个字符串的String对象,在jdk1.6及以前常量池分配在永久代中。
可通过
-XX:PermSize和-XX:MaxPermSize限制方法区大小。
public class StringIntern {
    //运行如下代码探究运行时常量池的位置
    public static void main(String[] args) throws Throwable {
        //用list保持着引用 防止full gc回收常量池
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}
//如果在jdk1.6环境下运行 同时限制方法区大小 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代
//如果是jdk1.7或1.8环境下运行 同时限制堆的大小  将报heap space 即常量池在堆中

 

6、直接内存(Direct Memory)

直接内存(Direct Memory)就是Java堆外内存。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,

然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。

服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,

使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

 

7、运行时常量池(Runtime Constant Pool)

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

方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码.其中常量存储在运行时常量池中。

我们知道,.java文件被编译之后生成的.class文件中除了包含:类的版本、字段、方法、接口等信息外,还有一项就是常量池

常量池中存放编译时期产生的各种字面量和符号引用,.class文件中的常量池中的所有的内容在类被加载后存放到方法区的运行时常量池中。

PS:int age = 21;//age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值;
int final pai = 3.14;//pai就是一个符号常量,一旦被赋值之后就不能被修改。

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

这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,

java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

在近三个JDK版本(6、7、8)中, 运行时常量池的所处区域一直在不断的变化,

在JDK6时它是方法区的一部分

7又把他放到了堆内存中

8之后出现了元空间,它又回到了方法区。

其实,这也说明了官方对“永久代”的优化从7就已经开始了。

特性:

class文件中的常量池具有动态性。

Java并不要求常量只能在编译时候产生,Java允许在运行期间将新的常量放入方法区的运行时常量池中。

String类中的intern()方法就是采用了运行时常量池的动态性.当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串,

则返回池中的字符串.否则,将此 String 对象添加到池中,并返回此 String 对象的引用。

可能抛出的异常:

运行时常量池是方法区的一部分,所以会受到方法区内存的限制,因此当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。

我们一般在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。

当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。

如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。

 

8、生成对象时的内存情况

下面来分析一下我们常见的生成对象或基本数据类型变量的内存模型。这样可以对JVM有一个更好的理解。

(1)int i =3;,一个方法对应一个栈帧,方法中的基本数据类型变量直接在栈帧中分配。如果是static、final类型的基本数据类型则存储在运行时常量池中,和String一样。

(2)Object o1 = new Object();,对象引用(Object o1)存储在栈帧中,但是对象数据(new Object())存储在java堆中,对象类型数据(Class等信息)存储在方法区中。

(3)String s1 = new String("abcd");,使用new声明的对象,对象引用(String s1)存储在栈帧中,对象数据(new String(“abcd”))存储在java堆中,字符串值(“abcd”)存储在运行时常量池中。

(4)String s2 = “abc”,对象引用(String s2)存储在栈帧中,字符串值(“abc”)存储在运行时常量池中。

 

 

 java栈、java堆、方法区这3者之间的关系大概就是上面的分析所示。

 

9、JVM关闭

正常关闭:当最后一个非守护线程结束或调用了System.exit或通过其他特定于平台的方式,比如ctrl+c。

强制关闭:调用Runtime.halt方法,或在操作系统中直接kill(发送single信号)掉JVM进程。

异常关闭:运行中遇到RuntimeException 异常等。

在某些情况下,我们需要在JVM关闭时做一些扫尾的工作,比如删除临时文件、停止日志服务。为此JVM提供了关闭钩子(shutdown hocks)来做这些事件。

Runtime类封装java应用运行时的环境,每个java应用程序都有一个Runtime类实例,使用程序能与其运行环境相连。

关闭钩子本质上是一个线程(也称为hock线程),可以通过Runtime的addshutdownhock (Thread hock)向主jvm注册一个关闭钩子。hock线程在jvm正常关闭时执行,强制关闭不执行。

对于在jvm中注册的多个关闭钩子,他们会并发执行,jvm并不能保证他们的执行顺序。

 

三.总结

1、JVM内存模型优点:

(1)内置基于内存的并发模型:多线程机制;

(2)同步锁Synchronization;

(3)大量线程安全型库包支持;

(4)基于内存的并发机制,粒度灵活控制,灵活度高于数据库锁。

(5)多核并行计算模型;

(6)基于线程的异步模型。

 

2、JVM内存参数设定:

-Xms 初始堆内存大小
-Xmx 最大堆内存大小
-Xss 单个线程栈大小
-XX:NewSize 初始新生代堆大小
-XX:MaxNewSize 生代最大堆大小
-XX:PermSize 方法区初始大小(JDK1.7及以前)
-XX:MaxPermSize 方法区最大大小(JDK1.7及以前)
-XX:MetaspaceSize 元数据区初始值(JDK1.8)
-XX:MaxMetaspaceSize 元数据区最大值(JDK1.8)

参数设置示例:

jdk1.7 windows设置tomcat的catalina.bat
set JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:PermSize=128m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
jdk1.8 windows设置tomcat的catalina.bat
set JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
jdk1.7 linux设置tomcat的catalina.sh
JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:PermSize=128m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m
jdk1.8 linux设置tomcat的catalina.sh
JAVA_OPTS=-Xms1024m -Xmx1024m -Xss1m -XX:MetaspaceSize=128m -XX:MAXMetaspaceSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m

 

posted on 2020-05-21 12:21  战狂粗人张  阅读(543)  评论(0编辑  收藏  举报