jvm内存

2018/03/11 jvm

jvm内存结构

jvm内存结构

简介

线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余的数据类型占1个。局部变量表所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

栈的大小

栈的大小默认值随着虚拟机版本以及操作系统影响,64位linux默认是1m的样子。(32位默认值不同,win默认值没研究)大小可以使用jvm参数 -Xss设置 除了JVM设置,我们还可以在创建Thread的时候手工指定大小:

  public Thread(ThreadGroup group, Runnable target, String name , long stackSize)

栈的大小影响到了线程的最大数量,尤其在大流量的server中,我们很多时候的并发数受到的是线程数的限制,这时候需要了解限制在哪里:

  1. 操作系统限制,以ubuntu为例,/proc/sys/kernel/threads-max 和/proc/sys/vm/max_map_count 定义了总的最大线程数和mmap这个system_call的最大数量(也就是从内存方面限制了线程数)
  2. JVM限制,理论上我们能分配给线程的内存除以单个线程占用的内存就是最大线程数。所以说对Java进程来讲,既然分配给了堆,栈和静态方法区(或叫永久代,perm区),我们可以大致认为:

线程数 = (系统空闲内存 - 堆内存 - perm区内存) / 线程栈大小 !!!还有很多其他因素

溢出相关问题

  • StackOverflowError:一般来说递归调用是常见的原因。
  • OutOfMemoryError:一般是因为创建过多线程(上面的线程数分析可知)。

简介

对于大多数应用来说,Java 堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

结构

jvm堆结构

如上图所示,Eden Space字面意思是伊甸园,对象被创建的时候首先放到这个区域;Survivor区也被称为幸存者区,在垃圾回收时,eden空间中的存活对象会被复制到未使用的Survivor空间中(假设是to区)(复制算法),正在使用的survivor空间(假设是from)中的年轻对象也会被复制到to空间中。此时,eden空间和from空间中的剩余对象就是垃圾对象,可以被直接清空。新生代中执行的垃圾回收被称之为Minor GC(因为是对新生代进行垃圾回收,所以又被称为Young GC),每一次Young GC后留下来的对象age加1。当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC。

比例: 新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即:Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

垃圾回收(说到堆内存一定少不了垃圾回收)

什么时候回收?

Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

那些内存可以回收?

这里主要引出两个可达性检测方法:

  • 引用计数:一种在jdk1.2之前被使用的垃圾收集算法,我们需要了解其思想。其主要思想就是维护一个counter,当counter为0的时候认为对象没有引用,可以被回收。缺点是无法处理循环引用。在当前的JVM中应该是没有被使用的。
  • 根搜算法:思想是从gc root根据引用关系来遍历整个堆并作标记,称之为mark,等会在具体收集器中介绍并行标记和单线程标记。之后回收掉未被mark的对象,好处是解决了循环依赖这种『孤岛效应』。这里的gc root主要指:

    1\. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
    2\. 方法区中的类静态属性引用的对象
    3\. 方法区中的常量引用的对象
    4\. 本地方法栈中JNI的引用的对象
    

怎么回收?(收集算法)

标记-清除算法

算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程就是使用可达性算法进行标记的。 主要缺点有两个:

  • 效率问题,标记和清除两个过程的效率都不高
  • 空间问题,标记清除之后会产生大量不连续的内存碎片

标记清除算法

复制算法

将可用内存按照容量分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。

  • 优点:内存分配时不用考虑内存碎片问题,只要一动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  • 缺点:代价是将内存缩小为原来的一半。

复制算法

标记-整理算法

标记整理算法(Mark-Compact),标记过程仍然和”标记-清除”一样,但后续不走不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

  • 优:没有内存碎片化问题。
  • 缺:时间上比标记-清除更慢。

标记整理算法

我们发现上面的所有收集算法,都有或多或少的问题,那么实际上我们的Hotspot是怎么做的呢?

分代收集算法

在新生代中,每次垃圾收集时有大批对象死去,只有少量存活,都是选用复制算法。而老年代对象存活率高,使用标记-清除或者标记-整理。这个也是为什么堆内存结构会有这样的划分!

垃圾收集器

1.6版本下的,所有收集器:

java1.6的收集器

Serial(新生代)

Serial收集器是单线程收集器,是分代收集器。它进行垃圾收集时,必须暂停其他所有的工作线程(stop the world),直到它收集结束。使用单线程复制收集算法;Serial一般在单核的机器上使用,是Java 5非服务端JVM的默认收集器,参数-XX:UseSerialGC设置使用。

ParNew(新生代)

现在大部分的应用都是运行在多核的机器上,显然Serial收集器无法充分利用物理机的CPU资源,因此出现了ParNew收集器。ParNew收集器和Serial收集器的主要区别是新生代的收集,一个是单线程一个是多线程。ParNew收集器多在CPU的服务器上,是Java5 服务器端JVM的默认收集器。参数-XX:+UseParallelGC进行设置使用。

Parallel Scavenge(新生代)—>吞吐量最优收集器

一个新生代收集器,使用复制算法的收集器,又是并行(用户线程阻塞)的多线程收集器。目标是达到一个可控制的吞吐量。他的一个重要特征:

GC自适应的调节策略!!!Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是单线程的。使用”标记-整理”算法。

Parallel Old收集器

Parallel old是Parallel Scavenge收集器的老年代版本,使用多线程和”标记-整理”算法。

CMS收集器

cms收集器运行示意图

为了解决老年代暂停时间过长的问题,并且真正实现并行收集(程序和GC并行执行)。是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于”标记-清除”算法实现的。 分为四个阶段:

  • ①初始标记(initial mark):暂停一会,找出所有活着对象的初始集合。
  • ②并行标记(concurrent marking):根据初始集合,标记出所有的存活对象,由于程序在运行,一部分存活对象无法标出。此过程标记操作和程序同时执行。
  • ③重新标记(remark):程序暂停一会,多线程进行重新标记所有在②中没有被标记的存活对象。
  • ④并行清理concurrent sweep:回收所有被标记的垃圾区域。和程序同时进行。

由于此收集器在remark阶段重新访问对象,因此开销有所增加。此收集器的不足是,老年代收集采用标记清除算法,因此会产生很多不连续的内存碎片。 此收集器一般多用于对程序暂停时间要求更短的程序上,多由于web应用(实时性要求高)。参数-XX:+UseConcMarkSweepGC设置使用它。

G1收集器

主要思路是将新生代老生代进一步分为多个region,每次gc可以针对部分region而不是整个堆内存。由此可以降低stw的单次最长时间,代价是可能在总时间上会更高。G1让系统在整体吞吐量略降的情况下变得更加平滑稳定。 深入理解g1垃圾收集器

溢出相关问题

  • 循环对象创建

方法区

又称为永久代(Perm Generation)。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。常见异常 java.lang.OutOfMemoryError: PermGen space说明我们此时要调整配置了,或者说代码中有一些bug导致大量的perm区被占用,可能是用到了太多的静态变量(一般怀疑map)或者说用到cglib这样的字节码技术生成过多的类…(待补充)

程序计数器

是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有自己的独立的程序计数器。如果线程正在执行的是Java方法,那么这个计数器的值就是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

直接内存

直接内存不是虚拟机运行时数据区的一部分。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。在JDK1.4中新加入了NIO类,引入了一种基于通道与缓存区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。所谓的零拷贝就是这玩意了。

一些常用命令

jstack [pid] 列出当前pid对应jvm的所有线程栈描述
jstat -gcutil [pid] [interval] 实时打印gc情况以及各代内存占用比例
jmap -dump:format=b,file=f1 <pid> dump内存到二进制文件
jmap -histo [pid] 按占大小倒序列出内存中的实例类型
jmap -heap [pid] 插件GC策略

关于堆的一些参数详解:
-Xmx //JVM最大允许分配的堆内存,按需分配
-Xms //JVM初始分配的堆内存,一般和Xmx配置成一样以避免每次gc后JVM重新分配内存。
-Xmn //年轻代内存大小。整个JVM堆的内存=年轻代 + 年老代 + 持久代,持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小
-XX:PermSize=128m //持久代内存大小 (jdk1.8中改为元数据区-XX:MaxMetaspaceSize=128m 设置最大的元内存空间128M)
-Xss 256k //设置每个线程的堆栈大小
-XX:NewRatio=4//设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

Search

    Post Directory