首页 > 代码库 > JVM内存管理

JVM内存管理

物理内存和虚拟内存

(1)在java中,分配内存和回收内存都由JVM自动完成,甚至不需要写和内存相关的代码
(2)物理内存即RAM还有寄存器(一种存储单元,用于存储计算机单元执行指令(如整形浮点等运算)的中间结果)是处理器通过地址总线连接的。地址总线:其宽度决定了一次可以存寄存器或者RAM中获取多少个bit和处理器最大的可以寻址的范围,每个地址会引用一个字节,所以如果是32位的总线则可以有4G的内存空间。(通常情况下地址总线和RAM或寄存器有相同的位数)
(3)通常操作系统的内存申请空间是按照进程来管理的,每个进程间不会互相重合,操作系统保证每个进程拥有一段独立的地址空间。(逻辑上独立,物理空间不一定独立,如虚拟内存,虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。)
(4)由于程序越来越庞大何设计的多任务性,物理内存无法满足要求,出现了虚拟内存,虚拟内存使得多个进程可以共享物理内存,并且逻辑上独立。虚拟内存提高了内存利用率,并且可以扩展内存空间,使得一个虚拟的地址可以映射到物理内存,文件或者其他可以寻址的存储上。如一个进程在不活动的情况下,操作系统将这个物理内存中的数据移到一个磁盘文件下(频繁地交换物理内存和磁盘上的数据,会导致效率低下,需要关注)。

内核空间和用户空间

(1)电脑的内存地址空间将被划分为内核地址空间和用户空间,程序只能使用用户空间的内存(指程序能够申请的内存)(如windows32为默认内核空间和用户空间的比例是1:1,linux32为默认的比例是1:3)
(2)内核空间主要指操作系统用于程序调度、虚拟内存或者连接硬件资源等的程序逻辑。程序不能访问操作系统的空间,并且不能直接访问硬件资源,必须通过系统提供的接口调用。(每一次系统调用都会引起内核空间和内存空间的切换,这一操作比较耗时)

Java中的那些组件需要使用内存

(1).java堆:用于存储java对象的内存区域,可以通过Xmx(最大大小)和Xms(初始大小)来控制大小,默认空余堆内存少于40%时就扩大到Xmx,空余堆内存大于70%时就缩小到Xms,因此,服务器一般把xmx和xms设置成一样,避免在GC后调节堆的大小。
(2).每个线程创建时,JVM都会为它创建一个运行方法栈、局部变量的堆还有操作栈。
(3).类和类加载器:类和类的加载器本身同样需要存储空间,存储在永久代PermGen(属于方法区,即java堆的永久区部分)
ps:

  1. JVM是按需加载类的,隐式加载只会加载那些应用程序中明确使用到的。
  2. 加载类超过PermGen区大小的话可能会导致内存溢出,所以对于自己实现的类加载器可能会导致类的重复加载时,可能需要实现对类的卸载,需满足:
    • Java堆中没有对表示该类的类加载器的java.lang.ClassLoader对象的引用,
    • Java对中没有该类的对应加载器的java.lang.class对象的引用,
    • Java堆上任何该类的类加载器的任何类的所有对象都不存活。

而JVM的默认类加载器都不满足该条件,所以他们加载的类都不能卸载。

(4)NIO:NIO使用ByteBuffer.allocateDirect()方法分配内存,可以避免数据从内核空间到用户空间的复制,提高效率,但是该方法直接使用的是本机内存而不是java堆内存,直接的ByteBuffer对象可以自动清理本机缓存区,但是其只是作为GC时的一部分执行,而GC只在Java堆被填满或者显示调用System.gc()来执行(也就是自动的GC只检查Java堆是否满,而不知道NIO操作的本机内存是否需要释放。),以至于NIO在很多框架中是通过显示调用System.gc()执行NIO内存的释放的(其实2对象本身有clean方法可以释放,见jdk随笔额,heap&direct)
(5)JNI:JNI使得本机代码(如C语言)可以调用java方法,JVM会准备空间以供运行本地方法,也会增加java运行时的本机内存占用。

JVM内存结构

JVM是按照运行时数据的存储结构来划分内存结构的,根据不同的格式存储在不同的区域。运行时数据包括java程序本身的数据信息和JVM运行Java程序需要的额外数据信息,java虚拟机规范将Java运行时数据分为6种:PC寄存器数据、Java栈、堆、方法区、本地方法区、运行时常量池。

PC寄存器数据:

用于保存当前执行程序的内存地址,也就是记录某线程当前执行的方法的那一条指令,如线程的执行被中断后就会依靠这些数据来恢复(JVM规范之定义了对java方法需要记录指针,对本地方法则没有规范)

Java栈:

java栈与线程相关联,每创建一个线程就会为该线程创建一个栈,而线程中运行的每一个方法则与栈中的每一个栈帧关联起来,栈帧中包含局部变量,操作栈,方法返回值等。每一个方法完成,就会弹出栈帧的元素(操作栈的栈顶元素),作为返回值,清除这个栈帧。java栈的栈顶就是当前正在执行的活动栈,PC寄存器会指向这个方法的地址。Java栈和线程对应起来,这些数据不是线程共享的,不存在一致性问题

堆:

存储Java对象的地方,由于时所有线程共享的,所以需要关心数据的一致性问题。

方法区:

用于存储类结构信息,如常量池、域,方法数据、方法体,构造函数、包括类中的专用方法、实力初始化、接口初始化等

  • 方法区同样属于java堆的永久代
  • 如果使用动态编译时要注意这部分是否能满足类的存储
  • 这个区域并不像其他java堆一样频繁地被GC回收

运行时常量池:

包括编译器的数字常量,方法或者域的引用。(注意,这一区域属于方法区)

本地方法栈:

JVM为运行native方法准备的空间。由于很多native方法是用c语言实现的,所以又叫C栈。这个区域jvm并没有严格的限制,由不同的JVM实现者自由实现。

JVM内存分配策略

  1. 静态内存分配策略:在编译期间必须知道内存空间(8个基本类型)的大小才可以分配(所以可以在编译期间分配内存,但java栈中的局部变量和引用等数据同样使用静态内存分配,该空间大小是在编译期间知道,但是在程序加载时才正式分配的,并且这一部分内存在java栈上分配),不允许可变数据类型或者递归、嵌套等结构的出现。
  2. 栈内存分配:不需要在编译时知道程序对数据的需求、但在进入程序模块时必须知道数据的要求才可以分配内存。并且按照后进先出的原则进行内存的分配
  3. 堆内存分配:可以在运行到相应代码才知道内存空间的大小,灵活但是效率较差

JVM的内存分配主要基于堆和栈,

栈:

  1. 栈的分配时和线程绑定的,为每一个线程创建一个栈,为线程每调用一个新的方法创建一个栈帧
  2. 栈中主要保存基本类型数据和对象的句柄(引用、指针),栈的数据大小和生存期都必须是确定的,而本地变量和操作栈的大小都可以在编译时(class字节码)确定
  3. 存取速度比堆要快,仅次于寄存器,这也是为什么运算要留在操作栈中执行
  4. 栈的内存分配是在程序运行时进行的,只是分配的大小是在编译时确定的

  1. 堆可供所有线程访问,主要存放实例数据,由于时动态分配内存大小的,所以存取速度较慢,同样通过GC回收内存
  2. 新对象如何分配内存:根据对应Constant_Class_info类型数据执行new指令,赋值,调用init初始化构造器最后才赋值给变量(所以在初始化完成前不应该把实例指针公布,可类比“对象逸出”的问题),栈中存放的只是指针(引用),而真正的实例数据是存放在堆中的
  3. 堆在运行时请求操作系统分配内存,灵活但效率低

JVM内存回收策略

静态内存的分配和回收:类中的局部变量和对象的引用都是静态内存分配的(这一部分内存空间在栈上分配),在编译时这一部分空间已经确定,只是在程序被加载时一次性分配,而当方法运行结束时随着对应栈帧的撤销回收。

动态内存分配和回收:像实例等数据只有在JVM解析类对象后才能知道具体需要分配多少空间,并且堆中的这些数据只有在对象不再被引用时才会被回收。只要某个对象不再被其他活动对象所引用就可以被回收,而活动对象是指可以被根集合对象所到达的对象。根集合对象所包含的对象跟jvm具体实现有关,但是大都会包含如下一些元素:方法中局部变量的引用、java操作栈中的对象引用、常量池中的对象引用、本地方法中持有的对象引用、类的class对象(当该Class对象不再被使用时同样会被回收)。

基于分代的垃圾收集算法:分为young、old、perm三个区

young区分为eden区和两个survivor区,eden区满后会触发minorGC,minorGC后仍存活的对象将放到survivor区(若另一个survivor区存在活动对象将放到同一个区中,保证一个survivor区是空的)
old区中已满将会触发FullGC,old区中存放的是:

  • Young的survivor区中已满后minorGC仍然存活的对象
  • survivor区中足够老的对象
  • Eden中已满,并且minorGC后存在,并因为servivor已满无法存放的对象。

perm区主要存放类的class对象,只有在FullGC时才会被回收

三类垃圾收集算法,Serial Collector、Parallel Colllector、CMS Collector

技术分享

Serial Collector

JVM在client模式下的默认的GC方式(可以通过配置jvm参数 -XX:+UserSerialGC来配置实用该算法)-XX:+PrintGCDetails 可以配置打印GC日志。所有创建的对象都将在Eden区分配,如果创建的对象超过Eden区的大小或者超过PretenureSizeThreshold配置(-XX:PretenureSizeThreshold=123)参数的大小都只能在old区分配当Eden区空间不足时会触发minorGC,但是触发minorGC之间会检查晋升到Old区的平均对象大小是否大于old的剩余空间,如果大于则触发FullGC,如果小于则根据HandlePromotionFailure(是否允许担保失败)参数,如果为true则仅触发MinorGC,否则触发FullGC。MinorGC时除了将Eden区的非活动对象回收外,还会把一些年老的对象晋升到Old区,而这个年老对象的‘岁数’则通过 -XX:MaxTenuringThreshold=10设置(在survivor的from/to区之间移动一次则为一岁),另外如果To的Servivor区空间不足移入对象时,这些对象也会直接放入Old区。如果old区或者Perm区空间不足时就会触发FullGC。GC时因为是串行的,所以动作是单线程完成的,JVM中的其他应用程序会全部停止。

Parallel Collector

Parallel GC根据MinorGC和FullGC的不同分为三种,分别是ParNewGC、ParallelGC和ParallelOldGC。

ParNewGC:
可以通过参数 -XX:+UseParNewGC参数来指定,与Serail Collector相似,只是回收是多线程并行的,并且通过一个UseAdaptiveSizePolicy配置参数来控制对象经过多少次回收后可以直接放入old区。
ParallelGC:
是server模式JVM下的默认GC方式,可以通过 -XX:+UserParallelGC参数来强制指定,并行回收的线程数可以通过 -XX:ParallelGCThreads来指定,这个值有个计算公式,如果cpu核数小于8,则可以和核数一样,如果大于8值为:3+(核数*5)/8,可以通过 -Xmn:10m来控制Young区的大小,而Eden、FROM区的大小比例可以通过 -XX:SurvivorRatio=8来设置Eden和FromSpace的比值是8:1(当然To区也占1)。当在Eden区中申请内存空间时,如果Eden区不够,则比较当前申请空间时否大于Eden的一半,是的话则直接在old中分配,不是的话则会执行MinorGC,但是执行MinorGC之前会检查old区的平均晋升大小是否大于剩余空间,大于则触发FullGC,并且在执行FullGC后会再一次检查old的晋升的平均大小是否大于剩余空间,不是的话会再次触发FullGC,也就是说可能会触发两次FullGC。,Young区的晋升规则可以通过以下参数设置:AlwaysTenure:默认为false,为true则表示只要在MinorGC时存活则晋升,NeverTenure,默认为false,是true则永不晋升。如果上面两个参数都没有配置的情况下设置UseAdaptiveSizePolicy,则启动时将以InitialTenuringThreshold值作为存活次数的阀值,并且在每次GC后调整。如果不使用UseAdaptiveSizePolicy则将以MaxTenuringThreshold为准(通过-XX:-UseAdaptiveSizePolicy设置)另外如果MinorGC时Servivor的To区空间不够,也会直接放到old区。old或者Perm区满时会触发FullGC,如果配置了参数ScavengeBeforeFullGC则在FullGC之前会触发MinorGC
PrarllelOldGC:
可以通过 -XX:+UseParallelOldGC参数来强制指定,同样可以通过-XX:ParallelGCThreads来指定线程数,这个值有个计算公式,如果cpu核数小于8,则可以和核数一样,如果大于8值为: 3+(核数*5)/8。与ParallelGC的不同在于FullGC,它的FullGC动作为清空整个Heap对中的垃圾对象,清楚Perm区中已经被卸载的类信息,并进行压缩,而ParallelGC只清楚部分heap堆中的垃圾对象,并对部分空间进行压缩。

CMS Collector

可以通过 -XX:+UseConcMarkSweepGC来指定,并发的默认线程为4,也可以通过ParallelCMSThreads指定。CMS Collector使用CMS GC、Minor GC、FullGC。而CMS GC不同于其他两种GC,触发规律是基于Old区、和Perm区的使用率(触发后回收对应old或perm区的内存),达到一定比例就会触发(默认是92%),该比例可以通过CMSInitiatingOccupancyFraction来指定,另外设置让Perm区也使用CMS GC可以通过参数 -XX:+CMSClassUnloadingEnabled来指定。这个模式下的minorGC与Serial Collector基本一致,只是采用多线程。FullGC只在两种情况触发,一种是Eden分配失败后分配到To区,To区满分配到Old区,Old区不够则触发FullGC,另外一种是当CMS GC向Old申请内存失败时会触发FullGC。Hotspot1.6下使用这种算法并显示调用System.gc(如Nio可能需要显示调用),且设置了ExplicitFCInvokesConcurrent参数,将会导致内存泄露。

技术分享

内存问题分析

日志格式:
[GC [<Collector>:<starting occupancy1> -> <ending occupancy1> (total size1) , <paise time1> secs ] <starting occupancy2> -> <ending occupancy2> (total size2), <paise time2> secs ]

<Collector> 收集器的名称
<starting occupancy1>Young区GC前内存
<endingoccupancy1>Young区GC后内存
<paise time1>YOUNG区局部收集时JVM的暂停时间
<starting occupancy2>表示JVMHeap GC前内存
<endingoccupancy2>表示JVMHeap GC后内存
<paise time2>GC过程中JVM的暂停总时间

GC日志对内存泄露的判断:

  1. 根据<starting occupancy1> - <ending occupancy1>得到young区被回收或晋升的内存大小,根据<starting occupancy2> - <ending occupancy2>得到当前整个堆的大小变化,两者的差值就是young区晋升到Old区的值
  2. 假如<ending occupancy2>随时间的延长一直增长,并且伴随频繁的GC,则很有可能是内存泄露,可使用jstat工具分析技术分享技术分享
  3. 堆快照的分析,可以通过参数: -XX:+HeadDumpOnOutOfMemoryError来配置在内存耗尽时记录下内存快照,同时可以通过-XX:HeadDumpPath来指定文件路径,这个文件的命名格式如java_[pid].hprof

JVM Crash日志分析:

  1. 可以通过-XX:ErrorFile = /tmp/log/hs_error_%p.log来指定jvm的日志文件
  2. 文件信息主要分为四种,退出原因分类、导致退出的Thread信息(栈信息,具体哪行代码出错)、退出时的Process状态信息(所有线程及线程处于的状态,jvm的堆信息)、退出时与操作系统相关信息
  3. 退出原因主要三种:
    • EXCEPTION_ACCESS_VIOLATION:运行的是JVM自己的代码,很可能是JVM的BUG
    • SIGSEGV:JVM在执行本地代码或者JNI的代码,很可能是第三方本地库有问题
    • EXCEPTION_STACK_OVERFLOW:这个是本地栈溢出的错误,可以将JVM的栈的尺寸调大,主要是两个参数-Xss 和 -XX:StackShadowPages=n

 

JVM内存管理