java虚拟机_02_Jvm内存模型

一、内存介绍

1.1 计算机内存

我们知道

  • 计算机CPU和内存的交互是最频繁的
  • 内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度

因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状,因此,现在CPU同内存交互就变成了下面的样子。

  • 同样,根据摩尔定律,我们知道单核CPU的主频不可能无限制的增长,要想很多的提升新能,需要多个处理器协同工作, Intel总裁的贝瑞特单膝下跪事件标志着多核时代的到来。

    基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(下文成主存,main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要将这些协议保证数据的一致性。

    这类协议包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。如下图所示

    1.2 Java虚拟机内存

    • Java虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致!

JVM内存区域
先看一张图,这张图能很清晰的说明JVM内存结构布局。

二 JVM内存区域介绍

从更高的一个维度再次来看JVM和系统调用之间的关系

线程共享内存

  • 可以被所有线程共享的区域,包括堆区、方法区、运行时常量池。

2.1堆(Heap)

大多数时候,Java 堆是 Java 虚拟机管理的内存里最大的一块,所有的对象实例和数组都要在堆上分配内存空间,Java 对象可以分为两类,一类是快速创建快速消亡的,另一类是长期使用的。所以针对这种情况大多收集器都是基于分代收集算法进行回收。

Java 的堆可以分为新生代(Young Generation)和老年代(Old Generation),而新生代(Young Generation)又可以分为 Eden Space 空间 (伊甸园区)、From Survivor 空间(From 生存区)、To Survivor 空间(To 生存区)。

Java 堆是一块共享的区域,会出现线程安全的问题,而操作共享区域就需要锁和同步,通过- Xms设置堆的最小值,堆内存越小越容易发生内存不够用的情况而触犯 Full GC(对新生代、老年代、永久代进行垃圾回收)。官方推荐新生代大小占整个堆大小的 3/8,通过- Xmx设置堆的最大值,堆内存超过此值会发抛出 OutOfMemoryError 异常:

2.2 方法区(Method Area)

方法区(Method Area)在 HotSpot 虚拟机上可以看作是永久代(Permanent Generation),对于其他虚拟机(JRockit 、J9 等)来说是不存在永久代的。方法区也是所有线程共享的区域,主要存储被虚拟机加载的类信息、常量、静态变量,堆存储对象数据,方法区存储静态信息。

方法区不像 Java 堆区那样经常发生垃圾回收,但不表示不会发生。永久代的大小跟新生代、老年代比都是很小的,通过设置- XX:MaxPermSize来指定最大内存,方法区需要的内存超过此值会抛出 OutOfMemoryError 异常。

2.3 运行时常量池(Runtime Constant Pool)

Java 通过类加载机制可以把字节码文件中的常量池表加载进运行时常量池,而我们也可使用 String 类的 intern() 方法在运行期将新的常量放入池中,运行时常量池是方法区的一部分,在 JDK1.7 的 HotSpot 中,已经把原本放在方法区的常量池移出来了。

线程私有内存

  • 只允许被所属的线程私自访问的内存区,包括 PC 寄存器、Java 栈和本地方法栈。

2.4 栈(Java Stack)

Java Stack 描述的是 Java 方法执行时的内存模型,每个方法执行时都会创建一个栈帧(Stack Frame),栈帧包含局部变量表(存放编译期间的各种基本数据类型,对象引用等信息)、操作数栈、动态链接、方法出口等数据。

一个线程运行时会分配栈空间,每个线程的栈空间数据是相互隔离的,所以栈是私有的,堆是共享的,一个线程执行多个方法,会入栈出栈多个栈帧(多个方法),栈是先进后出的数据结构,最先入栈的栈帧,最后出栈,可以通过-Xss设置每个线程栈的大小,越小,能创建的线程数就越多,但并不是可以无限的,在一个进程里(JVM 进程)能生成的线程数最多不超过五千

2.5 本地方法栈(Native Stack)

虚拟机栈(Java Stack)为执行 Java 方法(就是字节码)服务,而本地方法栈(Native Stack)则为 Native 方法(比如用 C/C++ 编写的代码)服务,其他方面都很类似。

2.6 PC 寄存器(程序计数器)

JVM 字节码解析器通过改变 PC 寄存器的值来明确下一条需要执行的字节码指令,每个线程都会分配一个独立的 PC 寄存器。

内存可见性-volatile

  • 每一个线程有一个工作内存和主存独立
  • 工作内存存放主存中变量的值的拷贝



2.7 volatile

public class VolatileStopThread extends Thread{
private volatile boolean stop = false;
public void stopMe(){
stop=true;
}

public void run(){
int i=0;
while(!stop){
i++;
             }
           System.out.println("Stop thread");
}

public static void main(String args[]) throws InterruptedException{
VolatileStopThread t=new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
  • 没有volatile -server 运行 无法停止
  • volatile 不能代替锁
    一般认为volatile 比锁性能好(不绝对
  • 选择使用volatile的条件是:
    语义是否满足应用

volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。而最彻底的同步要保证有序性和可见性,例如synchronized。任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于Valatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是有序的

  • 可见性
    • 一个线程修改了变量,其他线程可以立即知道
  • 保证可见性的方法
    • volatile
    • synchronized (unlock之前,写变量值回主存)
    • final(一旦初始化完成,其他线程就可见)
public class VolatileTest{  
  public volatile int a;  
  public void add(int count){  
      a=a+count;  
  }  
}  

当一个VolatileTest对象被多个线程共享,a的值不一定是正确的,因为a=a+count包含了好几步操作,而此时多个线程的执行是无序的,因为没有任何机制来保证多个线程的执行有序性和原子性。volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情形下可以使用 volatile 变量替代锁。

要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 1)对变量的写操作不依赖于当前值。
  • 2)该变量没有包含在具有其他变量的不变式中
    volatile只保证了可见性(直接读取内存的数据,不会脏读),所以Volatile适合直接赋值的场景,如
public class VolatileTest{  
public volatile int a;  
public void setA(int a){  
  this.a=a;  
  }  
}  
  • 有序性

    • 在本线程内,操作都是有序的
    • 在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)
  • 指令重排

    • 线程内串行语义
    • 写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
    • 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
    • 读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
    • 以上语句不可重排
    • 编译器不考虑多线程间的语义
    • 可重排: a=1;b=2;
  • 指令重排 – 保证有序性的方法

class OrderExample {
int a = 0;
boolean flag = false;

public synchronized void writer() {
    a = 1;                   
    flag = true;           
}

public synchronized void reader() {
    if (flag) {                
        int i =  a +1;      
        ……
    }
}
}
  • 同步后,即使做了writer重排,因为互斥的缘故,reader 线程看writer线程也是顺序执行的。

  • 线程A
    flag=true
    a=1

  • 线程B
    flag=true(此时a=1)

  • 指令重排的基本原则
    a=4;
    b=a+4;

    • 程序顺序原则:一个线程内保证语义的串行性
    • volatile规则:volatile变量的写,先发生于读
    • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
    • 传递性:A先于B,B先于C 那么A必然先于C
    • 线程的start方法先于它的每一个动作
    • 线程的所有操作先于线程的终结(Thread.join())
    • 线程的中断(interrupt())先于被中断线程的代码
    • 对象的构造函数执行结束先于finalize()方法

三 内存分配

3.1 新生、老年代分配

  • JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;
  • JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。

说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。

  • -Xmx –Xms
    指定最大堆和最小堆
    Xmx20m -Xms5m

  • Xmn
    设置新生代大小
    -XX:NewRatio

    • 新生代(eden+2*s)和老年代(不包含永久区)的比值
    • 4 表示 新生代:老年代=1:4,即年轻代占堆的1/5 3:5

-XX:SurvivorRatio
- 设置两个Survivor区和eden的比
- 8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10

-Xmx20m -Xms20m -Xmn1m


根据实际事情调整新生代和幸存代的大小:官方推荐新生代占堆的3/8,幸存代占新生代的1/10

在OOM时,记得Dump出堆,确保可以排查现场问题

-Xmn和-Xmx之比大概是1:9,如果把新生代内存设置得太大会导致young gc时间较长
-Xmn:年轻代堆内存(与Xmx比为1:9) XX:NewRatio=9表示年老代与年轻代的比值为9:1
-Xmx:最大堆内存
-Xmx:最小堆内存
-XX:SurvivorRatio=8 表示Eden区与Survivor区的大小比值是8:1:1,因为Survivor区有两个

3.2 永久区分配

  • -XX:PermSize -XX:MaxPermSize
    设置永久区的初始空间和最大空间
    他们表示,一个系统可以容纳多少个类型

    JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4

-Xss256k //设置每个线程的堆栈大小
-Xss128k //设置每个线程的堆栈大小

3.3 栈大小分配

-Xss
通常只有几百K
决定了函数调用的深度
每个线程都有独立的栈空间
局部变量、参数 分配在栈上

猜你喜欢

转载自blog.csdn.net/hardworking0323/article/details/81059166