JVM内存模型

1. JVM内存模型

JVM内存模型

2. 程序计数器(PC)

每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。 也可以理解为下一条将要执行的指令的地址或者行号。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程上下文切换,线程恢复时,都要依赖PC. - 如果线程正在执行的是一个Java方法,PC值为正在执行的虚拟机字节码指令的地址 - 如果线程正在执行的是Native方法,PC值为空(未定义)

3. 虚拟机栈(VM Stack)

VM Stack也是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。 在《java虚拟机规范》一书中对这部分的描述如下:

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
栈帧的存储空间分配在 Java 虚拟机栈之中,每一个栈帧都有自己的局部变量表( Local Variables)、操作数栈( OperandStack)和指向当前方法所属的类的运行时常量池的引用。

说白了,VM Stack是一个栈,也是一块内存区域。 所以,他是有大小的。虽然有大小,但是一般而言,各种虚拟机的实现都支持动态扩展这部分内存。 - 如果线程请求的栈深度太大,则抛出StackOverflowError - 如果动态扩展时没有足够的大小,则抛出OutOfMemoryError 栈的大小

栈的大小可以受到几个因素影响,一个是jvm参数 -XSS,默认值随着虚拟机版本以及操作系统影响,从Oracle官网上我们可以找到:

In Java SE 6, the default on Sparc is 512k in the 32-bit VM, and 1024k in the 64-bit VM. On x86 Solaris/Linux it is 320k in the 32-bit VM and 1024k in the 64-bit VM.

我们可以认为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)

注意这只是帮助我们树立一个概念,实际上还有许多因素影响。

栈的大小还影响到一个就是如果单个栈超过了这个大小,就会抛出StackOverflowError,一般来说递归调用是常见的原因。

如何查看线程栈

使用命令 jstack <pid>可以列出当前pid对应jvm的所有线程栈描述,描述主要包括了每个线程的状态以及堆栈内各栈帧的方法全限定名,代码位置。注意这只是为了可阅读性,并不是说栈里存着的就是这些字符串。

4. 本地方法栈(Native Method Stack)

Java 虚拟机实现可能会使用到传统的栈(通常称之为“ C Stacks”)来支持 native 方法( 指使用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈( Native MethodStack)。

VM Stack是为执行java方法服务的,此处的Native Method Stack是为执行本地方法服务的。 此处的本地方法指定是和具体的底层操作系统层面相关的接口调用了(这部分太高高级了,不想深究……)。

《java虚拟机规范》中没有对这部分做具体的规定。所以就由VM的实现者自由发挥了。 有的虚拟机(比如HotSpot)将VM Stack和Native Method Stack合二为一,所以VM的另一种内存区域图就如下面所示了: JVM内存模型

5. Java堆(Heap)

在 Java 虚拟机中,堆( Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

以下是本人对《java虚拟机规范》一书中对Java堆的介绍的总结:

  • 在虚拟机启动的时候就被创建
  • 是所有线程共享的内存区域
  • 存储了被自动内存管理系统所管理的各种对象
    • 这些受管理的对象无需,也无法显式地被销毁
    • 自动内存管理系统:Automatic StorageManagement System,也即是常说的”Garbage Collector(垃圾收集器)”
    • 并未指明用什么具体的技术去实现自动内存管理系统
  • Java 堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩
  • Java 堆所使用的内存不需要保证是连续的
  • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常
  • 实现者应当提供给程序员或者最终用户调节 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主要指:
      • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
      • 方法区中的类静态属性引用的对象
      • 方法区中的常量引用的对象
      • 本地方法栈中JNI的引用的对象
  • 整理策略

    • 复制:主要用在新生代的回收上,通过from区和to区的来回拷贝。需要特定的结构(也就是Young区现在的结构)来支持,对于新生成的对象来说,频繁的去复制可以最快的找到那些不用的对象并回收掉空间。所以说在JVM里YGC一定承担了最大量的垃圾清除任务。
    • 标记清除/标记整理:主要用在老生代回收上,通过根搜的标记然后清除或者整理掉不需要的对象。

思考一下复制和标记清除/整理的区别,为什么新生代要用复制?因为对新生代来讲,一次垃圾收集要回收掉绝大部分对象,我们通过冗余空间的办法来加速整理过程(不冗余空间的整理操作要做swap,而冗余只需要做move)。同时可以记录下每个对象的『年龄』从而优化『晋升』操作使得中年对象不被错误放到老年代。而反过来老年代偏稳定,我们哪怕是用清除,也不会产生太多的碎片,并且整理的代价也并不会太大。

  • 具体的垃圾收集器(重点了解CMSGC和G1)
    • 新生代收集器:有Serial收集器、ParNew收集器、Parallel Scavenge收集器
    • 老生代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器

静态方法区

最后讲一讲静态方法区,又称为永久代(Perm Generation)。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 常见的JVM配置包括:

-XX:MaxPermSize=512m

我们有时候会看到java进程报一个错误类似

Exception in thread "State Saver" java.lang.OutOfMemoryError: PermGen space

说明我们此时要调整配置了,或者说代码中有一些bug导致大量的perm区被占用,可能是用到了太多的静态变量(一般怀疑map)或者说用到ASM框架导致产生了大量的类信息。

JVM内存模型

6. 方法区(Method Area)

方法区是由所有线程共享的内存区域。 方法区存储的大致内容如下:

  • 每一个类的结构信息
    • 运行时常量池( Runtime Constant Pool)
    • 字段和方法数据
    • 构造函数和普通方法的字节码内容
  • 类、实例、接口初始化时用到的特殊方法 每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
  • 当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

7. 直接内存(Direct Memory)

此处的直接内存并不是由JVM管理的内存。他是利用本地方法库直接在java堆之外申请的内存区域。 比如NIO中的DirectByteBuffer就是操作直接内存的。

直接内存的好处就是避免了在java堆和native堆直接同步数据的步骤。但是他并不是由JVM来管理的。

参考一

参考二

转载于:https://my.oschina.net/u/866172/blog/1818455

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值