JVM内存模型

来源:互联网 发布:淘宝流量钱包不能用了 编辑:程序博客网 时间:2024/06/11 13:42
程序计数器:
多线程时,当线程数超过CPU数量或CPU内核数量,线程之间就要根据时间片轮询抢夺CPU时间资源。因此每个线程要有一个独立的程序计数器,记录下一条要运行的指令。线程私有的内存区域,如果执行的是Java方法,计数器记录正在执行的Java字节码地址,如果执行的是native方法,则计数器为空

虚拟机栈:
线程私有的,与线程在同一时间创建,管理Java方法执行的内存模型,每个方法运行时都会创建一个桢栈来存储方法的变量表,操作数栈,动态链接方法,返回值,返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小),栈的大小可以是固定的,可以是动态扩展的,如果请求的栈深度大于最大可用深度,则抛出stackOverflowError,如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError

本地方法区:
和虚拟机栈功能类似,但管理的不是Java方法,是本地方法,本地方法是用C实现的。

堆:
线程共享,存放所有对象实例和数组,垃圾回收的主要区域,可以分为新生代和老年代。
新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代
新生代又可进一步细分为Eden,SurvicorSpace0(s0,from space),SurvicorSpace1(s1,to space),刚创建的对象都放入Eden,s0和s1都至少经过一次GC并幸存,如果幸存对象经过一定时间任然存在,则进入老年代

方法区:
线程共享,用于存放被虚拟机加载的类的元数据信息:如常量,静态变量,即时编译器编译后的代码,也成为永久代,如果虚拟机确定一个类的定义信息不会被使用,也会将其回收,回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收

JVM的类装载流程
分为三个步骤,加载、链接、初始化,如图


1.JVM内存模型
2.程序计数器(PC)
每个线程都会有自己私有的程序计数器,可以看作是当前线程所执行的字节码的行号指示器
也可以理解为下一条将要执行的指令的地址或者行号,字节码解释器就是通过改变这个计数器的值来选取下一个需要执行的字节码指令,分支,循环,跳转,异常处理,线程上下文切换,线程恢复时,都依赖PC
如果线程正在执行一个Java方法,PC值为正在执行的虚拟机字节码指令的地址
如果线程正在执行的是Native方法,PC值为空(未定义)
说白了,PC就是一块内存区域,存放这下一条要执行的指令的地址
3.虚拟机栈(VM Stack)
VM Stack是线程私有的区域,是Java方法执行时的字典,它里面记录了局部变量表,操作数栈,动态链接,方法出口等信息
VM Stack是一个栈,也是一块内存区域
所以,它是有大小的,支持动态扩展
如果线程请求的栈深度太大,则抛出StackOverflowError
如果动态扩展时没有足够的大小,则抛出OutOfMemoryError
4.本地方法栈(Native Method Stack)
VM Stack是为执行Java方法服务的,此处的Native Method Stack是为执行本地方法服务的
此处的本地方法指定是和具体的底层操作系统层面相关的接口调用
5.Java堆(Heap)
在Java虚拟机中,堆(Heap)是可供个线程共享的运行时内存区域,也是可供所有类实例和数组对象分配内存的区域
在虚拟机启动的时候就被创建
是所有线程共享的内存区域
存储了被自动内存管理系统所管理的各种对象
这些受管理的对象无需,也无法显式的被销毁
自动内存管理系统:GC
Java的堆的容量可以是固定大小的,也可以是随着程序执行的需求动态扩展的,并在不需要过多空间时自动收缩
Java堆所使用的内存不需要保证是连续的
如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常
6.方法区(Method Area)
方法区是由所有线程共享的内存区域
方法区存储的大致内容如下:
运行时常量池
字段和方法数据
构造函数和普通方法的字节码内容
类,实例,接口初始化用到的特殊方法
在虚拟机启动的时候被创建
虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集
不限定实现方法区的内存位置和编译代码的管理策略
容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
方法区在实际内存空间中可以是不连续的
Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段
  • 对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段
如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常
6.1运行时常量池
当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
7.直接内存(Direct Memory)
此处的直接内存并不是由JVM管理的内存。他是利用本地方法库直接在java堆之外申请的 内存区域。
比如NIO中的DirectByteBuffer就是操作直接内存的。
直接内存的好处就是避免了在java堆和native堆直接同步数据的步骤。但是他并不是由JVM来管理的。
当然,这部分内存区域的操作也可能会抛出OutOfMemoryError



1.线程栈
注意这个栈和数据结构中的stack有相似之处,但并不是用户态的。准确的讲它压入的每个栈帧(Stack Frame)是程序指令以及局部变量表,每个方法调用对应一个栈帧。局部变量表包括各种基本数据类型:boolean、byte、char、short、int、float、long、double以及对象的引用。我们需要注意到每个线程都有独立的栈并且是互相隔离的。
栈的大小
栈的大小可以受到几个因素影响,一个是jvm参数 -XSS,默认值随着虚拟机版本以及操作系统影响,从Oracle官网上我们可以找到:
我们可以认为64位linux默认是1m的样子。
除了JVM设置,我们还可以在创建Thread的时候手工指定大小:
public Thread(ThreadGroup group, Runnable target, String name , long stackSize)
栈的大小影响到了线程的最大数量,尤其在大流量的server中,我们很多时候的并发 数受到的是线程数的限制,这时候需要了解限制在哪里。
第一个限制在操作系统,以ubuntu为例,/proc/sys/kernel/threads-max 和/proc/sys/vm/max_map_count 定义了总的最大线程数(根据资料windows总的来说线程数会更少)和mmap这个system_call的最大数量(也就是从内存方面限制了线程数)
第二个限制自然是在JVM,理论上我们能分配给线程的内存除以单个线程占用的内存就是最大线程数。所以说对Java进程来讲,既然分配给了堆,栈和静态方法区(或叫永久代,perm区),我们可以大致认为
线程数 = (系统空闲内存-堆内存(-Xms, -Xmx)- perm区内存(-XX:MaxPermSize)) / 线程栈大小(-Xss)

2.堆和垃圾收集
堆的结构
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
首先堆可以划分为新生代和老年代。
然后新生代又可以划分为一个Eden区和两个Survivor(幸存)区。
按照规定,新对象会首先分配在Eden中(如果对象过大,比如大数组,将会直接放到老年代)。在GC中,Eden中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过minor GC的次数),会被移动到老年代。
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。


垃圾收集
垃圾收集的意义:
垃圾收集的出现解放了C++中手工对内存进行管理的大量繁杂工作,手工malloc,free不仅增加程序复杂度,还增加了bug数量。
分代收集。即在新生代和老生代使用不同的收集方式。在垃圾收集上,目标主要有:加大系统吞吐量(减少总垃圾收集的资源消耗);减少最大STW(Stop-The-World)时间;减少总STW时间。不同的系统需要不同的达成目标。而分代这一里程碑式的进步首先极大减少了STW,然后可以自由组合来达到预定目标。
可达性检测:
引用计数:一种在jdk1.2之前被使用的垃圾收集算法,我们需要了解其思想。其主要思想就是维护一个counter,当counter为0的时候认为对象没有引用,可以被回收。缺点是无法处理循环引用。目前iOS开发中的一个常见技术ARC(Automatic Reference Counting)也是采用类似的思路。在当前的JVM中应该是没有被使用的。
根搜算法:思想是从gc root根据引用关系来遍历整个堆并作标记,称之为mark,等会在具体收集器中介绍并行标记和单线程标记。之后回收掉未被mark的对象,好处是解决了循环依赖这种『孤岛效应』。这里的gc root主要指:
  • a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • b.方法区中的类静态属性引用的对象
  • c.方法区中的常量引用的对象
  • d.本地方法栈中JNI的引用的对象
整理策略:
复制:主要用在新生代的回收上,通过from区和to区的来回拷贝。需要特定的结构(也就是Young区现在的结构)来支持,对于新生成的对象来说,频繁的去复制可以最快的找到那些不用的对象并回收掉空间。所以说在JVM里YGC一定承担了最大量的垃圾清除任务。
标记清除/标记整理:主要用在老生代回收上,通过根搜的标记然后清除或者整理掉不需要的对象。
整理的过程
清除的过程

具体的垃圾收集器:
新生代收集器:有Serial收集器、ParNew收集器、Parallel Scavenge收集器
老生代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器
静态方法区:
静态方法区,又称为永久代,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
0 0
原创粉丝点击