jvm体系结构

jvm体系结构_第1张图片一、Jvm 的介绍

 

1、JVM体系结构

 

 

2、JVM运行时数据区

 

 

3、JVM内存模型

JVM运行时内存 = 共享内存区 + 线程内存区

 

 

3.1、共享内存区

共享内存区 = 持久带(方法区 + 其他)+ 堆(Old Space + Young Space(den + S0 + S1))

 

持久代:

JVM用持久带(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池等等。可通过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。Permanent Space并不等同于方法区,只不过是Hotspot JVM用Permanent Space来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区。

堆(heap):

主要用来存放类的对象实例信息(包括new操作实例化的对象和定义的数组)。

堆分为Old Space(又名,Tenured Generation)和Young Space。Old Space主要存放应用程序中生命周期长的存活对象;Eden(伊甸园)主要存放新生的对象;S0和S1是两个大小相同的内存区域,主要存放每次垃圾回收后Eden存活的对象,作为对象从Eden过渡到Old Space的缓冲地带(S是指英文单词Survivor Space)。堆之所以要划分区间,是为了方便对象创建和垃圾回收.

 

3.2、线程内存区

 

线程内存区(JVM栈):

线程内存区=单个线程内存+单个线程内存+.......

单个线程内存=PC Regster+JVM栈+本地方法栈

JVM栈=栈帧+栈帧+.....

栈帧=局域变量区+操作数区+帧数据区

在Java中,一个线程会对应一个JVM栈(JVM Stack),JVM栈里记录了线程的运行状态。JVM栈以栈帧为单位组成,一个栈帧代表一个方法调用。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。

线程在栈区,不能共享数据,只能通过复制共享区的数据作为一块缓存,所有多线程写会有bug,voliate使得取到的数据不做缓存,是实时更新的。关键字 volatile 是轻量级的同步机制。

Volatile 变量对于all线程的可见性,指当一条线程修改了这个变量的值,新值对于其他 线程来说是可见的、立即得知的。 Volatile 变量在多线程下不一定安全,因为他只有可见性、有序性,但是没有原子性。

 

二、JVM内存空间管理

JVM把内存划分了如下几个区域:

 

共享内存区 = 持久带(方法区 + 其他)+ 堆(Old Space + Young Space(den + S0 + S1));

Java 内存模型和线程:

每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享。

 

2.1 方法区(共享内存区的持久带)

方法区 (又称为持久代):要加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息。方法区域也是全局共享的,当开发人员调用类对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区。

在一定条件下它也会被GC,当方法区域要使用的内存超过其允许的大小时,会抛出OutOfMemory:PermGen Space异常。的错误信息。在Sun JDK中这块区域对应Permanet Generation,,默认最小值为16MB,最大值为64MB,可通过-XX:PermSize及-XX:MaxPermSize来指定最小值和最大值。

在Hotspot虚拟机中,这块区域对应的是Permanent Generation(持久代),一般的,方法区上执行的垃圾收集是很少的,因此方法区又被称为持久代的原因之一,但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。在方法区上进行垃圾收集,条件苛刻而且相当困难,关于其回后面再介绍。

运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);

运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量,比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址。JVM方法区的相关参数,最小值:--XX:PermSize;最大值 --XX:MaxPermSize。

 

2.2 堆区(堆区由所有线程共享)

堆用于存储对象实例及数组值,可以认为Java中所有通过new创建的对象的内存都在此分配,堆区由所有线程共享。Heap中对象所占用的内存由GC进行回收,在32位操作系统上最大为2GB,在64位操作系统上则没有限制,其大小可通过-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1GB;-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1GB,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRatio=来指定这个比例;当空余堆内存大于70%时,JVM会减小Heap的大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRatio=来指定这个比例,对于运行系统而言,为避免在运行时频繁调整Heap 的大小,通常将-Xms和-Xmx的值设成一样。

堆区是理解JavaGC机制最重要的区域。在JVM所管理的内存中,堆区是最大的一块,堆区也是JavaGC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区用来存储对象实例及数组值,可以认为java中所有通过new创建的对象都在此分配。

 

2.3 本地方法栈(Native Method Stack)

本地方法栈用于支持native方法的执行,存储了每个native方法调用的状态。本地方法栈和虚拟机方法栈运行机制一致,它们唯一的区别就是,虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

 

2.4 虚拟机栈(JVM Stack)(线程私有)

JVM方法栈:为线程私有,其在内存分配上非常高效。当方法运行完毕时,其对应的栈帧所占用的内存也会自动释放。当JVM方法栈空间不足时,会抛出StackOverflowError的错误,在Sun JDK中可以通过-Xss来指定其大小。

虚拟机栈占用的是操作系统内存,每个线程都对应着一个虚拟机栈,它是线程私有的,而且分配非常高效。一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。

 

2.5 程序计数器(Program Counter Register)(线程私有)

程序计数器是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

 

三、内存溢出与内存泄漏

内存泄漏:分配出去的内存回收不了

内存溢出:指系统内存不够用了

 

1、堆溢出

可以分为:内存泄漏和内存溢出,这两种情况都会抛出OutOfMemoryError:java heap space异常:

 

a、内存泄漏:

内存泄漏是指对象实例在新建和使用完毕后,仍然被引用,没能被垃圾回收释放,一直积累,直到没有剩余内存可用。如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。分析内存泄漏的工具有:Jprofiler,visualvm等。

 

public class OOMTest {  

public static void main(String[] args) {             

        List list = new ArrayList();  

       while(true){  

            list.add(UUID.randomUUID());  

        }  

    }     

}

看看控制台的输出结果,因为我这边的JVM设置的参数内存足够大,所以需要等待一定的时间,才能看到效果:

 

b、内存溢出

内存溢出是指当我们新建一个实力对象时,实例对象所需占用的内存空间大于堆的可用空间。如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。

 

public class OOMTest_1 {  

    public static void main(String args[]){  

        List byteList = new ArrayList();  

        byteList.add(new byte[1000 * 1024 * 1024]);  

    }  

}  

2、栈溢出

栈(JVM Stack)存放主要是栈帧( 局部变量表, 操作数栈 , 动态链接 , 方法出口信息 )的地方。注意区分栈和栈帧:栈里包含栈帧。

与线程栈相关的内存异常有两个::

a:StackOverflowError(方法调用层次太深,内存不够新建栈帧)

b:OutOfMemoryError(线程太多,内存不够新建线程)

 

a、java.lang.StackOverflowError

栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法运行的时候,请求新建栈帧时,栈所剩空间小于栈帧所需空间。例如,通过递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常 :

 

public class SOFTest {  

    public void stackOverFlowMethod(){  

        stackOverFlowMethod();  

    }  

    /** 

        * 通过递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常 : 

        * @param args 

        */  

    public static void main(String[] args) {  

        SOFTest sof = new SOFTest();  

        sof.stackOverFlowMethod();  

    }  

    

}  

b、OutOfMemoryError(暂不介绍)

四、JVM内存分配

Java对象所占用的内存主要在堆上实现,因为堆是线程共享的,因此在堆上分配内存时需要进行加锁,这就导致了创建对象的开销比较大。当堆上空间不足时,会出发GC,如果GC后空间仍然不足,则会抛出OutOfMemory异常。

 

为了提升内存分配效率,在年轻代的Eden区HotSpot虚拟机使用了两种技术来加快内存分配 ,分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)。由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的, 它会为每个新创建的线程在新生代的Eden Space上分配一块独立的空间,这块空间称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行情况计算而得。可通过-XX:TLABWasteTargetPercent来设置其可占用的Eden Space的百分比,默认是1%。在TLAB上分配内存不需要加锁,一般JVM会优先在TLAB上分配内存,如果对象过大或者TLAB空间已经用完,则仍然在堆上进行分配。因此,在编写程序时,多个小对象比大的对象分配起来效率更高。可在启动参数上增加-XX:+PrintTLAB来查看TLAB空间的使用情况。

 

对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Minor GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。

可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。如果对象比较大(比如长字符串或大数组),年轻代空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用 -XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

你可能感兴趣的:(jvm)