JVM内存管理
1.物理内存和虚拟内存
(1)在 java中,分配内存和回收内存都由JVM自动完成,甚至不需要写和内存相关的代码2.内核空间和地址空间
(2)物理内存即RAM还有寄存器(一种存储单元,用于存储计算机单元执行指令(如整形浮点等运算)的中间结果)是处理器通过地址总线连接的。地址总线:
其宽度决定了一次可以存寄存器或者RAM中获取多少个bit和处理器最大的可以寻址的范围,每个地址会引用一个字节,所以如果是32位的总线则可以由4G的内存空间。(通常情况下地址总线和RAM或寄存器有相同的位数)
(3)通常操作系统的内存申请空间是按照进程来管理的,每个进程间不会互相重合,操作系统保证每个进程拥有一段独立的地址空间。(逻辑上独立,物理空间不一定独立,如虚拟内存)
(4)由于程序越来越庞大何设计的多任务性,物理内存无法满足要求,出现了虚拟内存,虚拟内存那是的多个进程可以共享物理内存,并且逻辑上独立。虚拟内存提高了内存利用率,并且可以扩展内存空间,使得一个虚拟的地址可以映射到物理内存,文件或者其他可以寻址的存储上。如一个进程在不活动的情况下没操作系统将这个物理内存中的数据一道一个磁盘文件下(频繁地交换物理内存和磁盘上的数据,会导致效率低下,需要关注)。
(1)电脑的内存地址空间将被划分为内核地址空间和用户空间,程序只能使用用户空间的内存(指程序能够申请的内存)(如windows32为默认内核空间和用户空间的比例是1:1,linux32为默认的比例是1:3)3.Java中的那些组件需要使用内存
(2)内核空间主要指操作系统用于程序调度,虚拟内存或者连接硬件资源等的程序逻辑。程序不能访问操作系统的空间,并且不能直接访问硬件资源,必须通过系统提供的接口调用。(每一次系统调用都会引起内核空间和内存空间的切换,这一操作比较耗时,可对比前面io章节)
(1).java堆:用于存储java对象的内存区域,可以通过Xmx(最大大小)和Xms(初始大小)来控制大小,默认空余堆内存少于40%时就扩大到Xmx,空余堆内存大于70%时就缩小到Xms,因此,服务器一般把xmx和xms设置成一样,避免在GC后调节堆的大小。(书本上说的是一旦分配后堆大小就固定,是不是指确定在这个范围中,不能扩大或归还到操作系统?)4.JVM内存结构
(2).每个线程创建时,JVM都会为它创建一个堆栈(是不是运行方法的栈,而局部变量的存储结构式堆?),堆栈的大小根据JVM的实现不同,通常在256kB~756KB之间。
(3).类和类加载器:类和类的加载器本身同样需要存储空间,存储在永久代PermGen(年轻代,年老代,永久代,详见转载的资料)(属于方法区,即java堆的永久区部分)
注意几点:
<1>JVM是按需加载类的,隐式加载只会加载那些应用程序中明确使用到的。(4)NIO:NIO使用ByteBuffer.allocateDirect()方法分配内存,可以避免数据重内核空间到用户空间的复制,提高效率(参照对比前面IO一章),但是该方法直接使用的本机内存,需然这种方式直接的ByteBuffer对象可以自动清理本机缓存区,但是其只是作为GC时的一部分执行,而GC只在Java堆被填满或者显示调用System.gc()来执行(也就是自动的GC只检查Java堆是否满,而不知道NIO操作的本机内存是否需要释放。),以至于NIO在很多框架中是通过显示调用System.gc()执行NIO内存的释放的,但是直接调用System.gc()会增加gc次数,消耗性能,需要平衡
<2>加载类超过PermGen区大小的话可能会导致内存溢出, 所以对于自己实现的类加载器可能会导致类的重复加载时,可能需要实现对类的卸载(需满足:1.java堆中没有对表示该类的类加载器的java.lang.ClassLoader对象的引用,2.java对中没有该类的对应加载器的java.lang.class对象的引用,Java堆上任何该类的类加载器的任何类的所有对象都不存活。而JVM的默认类加载器都不满足该条件,而他们加载的类都不能卸载)
(5)JNI:JNI使得本机代码(如C语言)可以调用java方法,JVM会准备空间以供运行本地方法,也会增加java运行时的本机内存占用。
(1)JVM是按照运行时数据的存储结构来划分内存结构的,根据不同的格式存储在不同的区域。运行时数据包括java程序本身的数据信息和JVM运行Java程序需要的额外数据信息,java虚拟机规范将Java运行时数据分为6种:PC寄存器数据、Java栈、堆、方法区、本地方法区、运行时常量池。5.jvm内存分配策略
(2)PC寄存器数据:用于保存当前执行程序的内存地址,也就是记录某线程当前执行的方法的那一条指令,如线程的执行被中断后就会依靠这些数据来恢复(JVM规范之定义了对java方法需要记录指针,对本地方法则没有规范)
(3)Java栈:java栈与线程相关联,每创建一个线程就会为该线程创建一个栈,而线程中运行的每一个方法则与栈中的每一个栈帧关联起来,栈帧中包含局部变量,操作栈,方法返回值等
<1>每一个方法完成,就会弹出栈帧的元素(操作栈的栈顶元素?),作为返回值,被清除这个栈帧(4)堆:存储Java对象的地方,由于时所有线程共享的,所以需要关心数据的一致性问题。
<2>java栈的栈顶就是当前正在执行的活动栈,PC寄存器会指向这个方法的地址,只有活动栈的局部变量可以被操作栈使用(执行方法是就会入栈,方法返回后出栈,则调用者方法作为活动栈)
<3>Java栈和线程对应起来,这些数据不是线程共享的,不存在一致性问题
(5)方法区:用于存储类结构信息,如常量池、域,方法数据、方法体,构造函数、包括类中的专用方法、实力初始化、接口初始化等
(1)方法区同样属于java堆(6)运行时常量池:包括编译器的数字常量,方法或者域的引用。(注意,这一区域属于方法区)
(2)如果使用动态编译时要注意这部分是否能满足类的存储
(3)这个区域并不像其他java堆一样频繁地被GC回收(永久代?)
(7)本地方法栈:JVM为运行native方法准备的空间。由于很多native方法是用c语言实现的,所以又叫C栈。这个区域jvm并没有严格的限制,由不同的JVM实现者自由实现。
(1)通常的内存分配策略:
<1>静态内存分配策略:在编译期间必须知道内存空间的大小才可以分配(所以可以在编译期间分配内存,但java栈中的局部变量和引用等数据需然同样使用静态内存分配,该空间大小是在编译期间知道,但是在程序加载时才正式分配的,并且这一部分内存在java栈上分配),不允许可变数据类型或者递归、嵌套等结构的出现。(2)Java中的内存分配详解
<2>栈内存分配:不需要再编译时知道程序对数据的需求、但在进入程序模块时必须知道数据的要求才可以分配内存。并且按照后进先出的原则进行内存的分配
<3>堆内存分配:可以在运行到相应代码才知道内存空间的大小,但是效率较差
<1> JVM的内存分配主要基于堆和栈
6.JVM内存回收策略<2>栈:
[1]栈的分配时和线程绑定的,为每一个线程创建一个栈,为线程每调用一个新的方法创建一个栈帧<3>堆
[2]栈中主要保存基本类型数据和对象的句柄(引用、指针),栈的数据大小和生存期都必须是确定的,而而变量(class文件的localVariableTable部分,这个大小是最大大小,可类比slot区域的共用)和操作栈的大小都可以在编译时确定(基本类型的局部变量和对对象的引用其实都是静态分配内存的,只是这一部分静态内存时在java栈上分配的,然则java栈中包含静态内存分配和栈式内存分配?)
[3]存取速度比堆要快,仅次于寄存器(所以使用局部变量比类属性要高效?),这也是为什么运算要留在操作栈中执行
[4]栈的内存分配是在程序运行时进行的,只是分配的大小是在编译时确定的
[1]堆可供所有线程访问,主要存放实例数据,由于时动态分配内存大小的,所以存取速度较慢,同样通过GC回收内存
[2]新对象如何分配内存:根据对应Constant_Class_info类型数据执行new指令,赋值,调用init初始化构造器最后才赋值给变量(所以在初始化完成前不应该把实例指针公布,可类比“对象逸出”的问题),栈中存放的只是指针(引用),而真正的实例数据是存放在堆中的
[3]堆在运行时请求操作系统分配内存,灵活但效率低
(1)静态内存的分配和回收:类中的局部变量和对象的引用都是静态内存分配的(这一部分内存空间在栈上分配),在编译时这一部分空间已经确定,只是在程序被加载时一次性分配,而当方法运行结束时随着对应栈帧的撤销回收。
(2)动态内存分配和回收:像实例等数据只有在JVM解析类对象后灿能知道具体需要分配多少空间,并且堆中的这些数据只有在对象不再被引用时才会被回收7.内存问题分析
(3)如何检测垃圾:
<1>只要某个对象不再被其他活动对象所引用就可以被回收,而活动对象时指可以被根集合对象所到达的对象(4)基于分代的垃圾收集算法:
<2>根集合对象所包含的需然根jvm具体实现有关,但是大都会包含如下一些元素:方法中局部变量的引用、java操作栈中的对象引用、常量池中的对象引用、本地方法中持有的对象引用、类的class对象(当该Class对象不再被使用时同样会被回收)
<1>hotspot jvm(sun(Oracle)提供的一个jvm实现)中使用基于分代的垃圾收集方式
<2>分为young、old、perm三个区
[1]Young区分为eden区和两个servivor区,eden区满后会触发minorGC,minorGC后仍存活的对象将放到servivor区(若另一个servivor区存在活动对象将放到同一个区中,保证一个servivor区是空的)<3>三类垃圾收集算法(收集器?):Serial Collector、Parallel Colllector、CMS Collector(前两者是以串行并行区分?)
[2]old区中存放的是:1.Young的servivor区中已满后取法minorGC,仍然存活的对象 2.servivor区中足够老的对象 3.Eden中已满,并且minorGC后存在,并因为servivor已满无法存放的对象。
old区中已满将会触发FullGC
[3]Perm区主要存放类的class对象,只有在FullGC时才会被回收
[4]VisualVm工具可以用于观测内存不同代的垃圾回收情况,它同时也是netbeans的默认分析器,详见转载文章
[5]sun对堆的大小建议:Young栈整个堆的1/4、而servivor则栈Young的1/8.
[6]minorGC和FullGC,针对年轻代的GC 和全部的GC
[1]Serial Collector
{1}Serial Collector 是JVM在client模式下的默认的GC方式(可以通过配置jvm参数 -XX:+UserSerialGC来配置实用该算法)[2]Parallel Collector
{2}-XX:+PrintGCDetails 可以配置打印GC日志
{3}所有创建的对象都将在Eden区分配,如果创建的对象超过Eden区的大小或者超过PretenureSizeThreshold配置(-XX:PretenureSizeThreshold=123)参数的大小都只能在old区分配
{4}当Eden区空间不足时会触发minorGC,但是触发minorGC之间会检查晋升到Old区的平均对象大小是否大于old的剩余空间,如果大于则触发FullGC,如果小于则根据HandelPromotionFailure(是否允许担保失败)参数,如果为true则仅触发MinorGC,否则触发FullGC(也就是设置该参数为false会导致Eden空间不足时触发的MinorGC会改为FullGC?)
{5}MinorGC时除了将Eden区的非活动对象回收外,还会把一些年老的对象晋升到Old区,而这个年老对象的‘岁数’则通过 -XX:MaxTenuringThreshold=10设置(在servivor的from/to区之间移动一次则为一岁),另外若然To的Servivor区空间不足移入对象时,这些对象也会直接放入Old区
{6}如果old区或者Perm区空间不足时就会触发FullGC
{7}GC时因为是串行的,所以动作是单线程完成的,JVM中的其他应用程序会全部停止。
{1}ParNewGC根据MinorGC和FullGC的不同分为三种,分别是ParNewGC、ParallelGC和ParallelOldGC(实际上相当于三种,只是他们都是多线程地执行垃圾回收?)
{2}ParNewGC:
【1】可以通过参数 -XX:+UseParNewGC参数来指定{3}ParallelGC:
【2】与Serail Collector相似,只是回收是多线程并行的,并且通过一个UseAdaptiveSizePolicy配置参数来控制对象经过多少次回收(移动的次数?,一次回收依然存活后?)后可以直接放入old区(young区是并行回收,old区依然是串行?)
【1】 是server模式JVM下的默认GC方式,可以通过 -XX:+UserParallelGC参数来强制指定,并行回收的线程数可以通过 -XX:ParallelGCThreads来指定,这个值有个计算公式,如果cpu核数小于8,则可以和核数一样,如果大于8值为: 3+(核数*5)/8
【2】可以通过 -Xmn:10m来控制Young区的大小,而Eden、FROM区的大小比例可以通过 -XX:SurvivorRatio=8来设置Eden和FromSpace的比值是8:1(当然To区也占1)
【3】当在Eden区中申请内存空间时,如果Eden区不够,则比较当前申请空间时否大于Eden的一半,是的话则直接在old中分配,不是的话则会执行MinorGC,但是执行MinorGC之前会检查old区的平均晋升大小是否大于剩余空间,大于则触发FullGC,并且在执行FullGC后会再一次检查old的晋升的平均大小是否大于剩余空间,不是的话会再次触发FullGC,也就是说可能会触发两次FullGC
【4】Young区的晋升规则可以通过以下参数设置{4}PrarllelOldGC
AlwaysTenure:默认为false,为true则表示只要在MinorGC时存活则晋升【5】old或者Perm区满时会触发FullGC,如果配置了参数ScavengeBeforeFullGC则在FullGC之前会触发MinorGC
NeverTenure,默认为false,书中的描述不太理解,应该是true则永不晋升
如果上面两个参数都没有配置的情况下设置UseAdaptiveSizePolicy,则启动时将以InitialTenuringThreshold值作为存活次数的阀值,并且在每次GC后调整
如果不使用UseAdaptiveSizePolicy则将以MaxTenuringThreshold为准(通过-XX:-UseAdaptiveSizePolicy设置)
另外如果MinorGC时Servivor的To区空间不够,也会直接放到old区
[3]CMS Collector【1】可以通过 -XX:+UseParallelOldGC参数来强制指定,同样可以通过-XX:ParallelGCThreads来指定线程数,这个值有个计算公式,如果cpu核数小于8,则可以和核数一样,如果大于8值为: 3+(核数*5)/8
【2】与ParallelGC的不同在于FullGC,它的FullGC动作为清空整个Heap对中的垃圾对象,清楚Perm区中已经被卸载的类信息,并进行压缩,而ParallelGC只清楚部分heap堆中的垃圾对象,并对部分空间进行压缩。
{1}可以通过 -XX:+UseConcMarkSweepGC来指定,并发的默认线程为4,也可以通过ParallelCMSThreads指定[4]三种GC算法的组合,GC的参数列表,优缺点相比见P220~P221
{2}CMS GC :CMS Collector使用CMS GC、Minor GC、FullGC。而CMS GC不同于其他两种GC,触发规律是基于Old区、和Perm区的使用率(触发后回收对应old或perm区的内存),达到一定比例就会触发(默认是92%,默认值的计算公式见P219),该比例可以通过CMSInitiatingOccupancyFraction来指定,另外设置让Perm区也使用CMS GC可以通过参数 -XX:+CMSClassUnloadingEnabled(是否允许Perm区通过CMSGC来卸载类?) 来指定
{3}这个模式下的minorGC与Serial Collector基本一致,只是采用多线程(ParNewGC?)
{4}FullGC只在两种情况触发Eden分配失败后分配到To区,To区满分配到Old区,Old区不够则触发FullGC,另外一种是当CMS GC向Old申请内存失败时会触发FullGC
{5}Hotspot1.6下使用这种算法并显示调用System.gc(如Nio可能需要显示调用),且设置了ExplicitFCInvokesConcurrent参数,将会导致内存泄露
(1)GC日志的参数见P222
(2)3中算法5中日志格式见P222
(3)除了CMS的日志和其他GC的日志差比较大,他们大概可以抽象成如下这个格式:
[GC [<Collector>:<starting occupancy1> -> <ending occupancy1> (total size1) , <paise time1> secs ] <starting occupancy2> -> <ending occupancy2> (total size2), <paise time2> secs ]
<Collector> 收集器的名称(4)根据GC日志对内存泄露的判断
<starting occupancy1>Young区GC前内存 <endingoccupancy1>Young区GC后内存 <paise time1>YOUNG区局部收集时JVM的暂停时间
<starting occupancy2>表示JVMHeap GC前内存 <endingoccupancy2>表示JVMHeap GC后内存 <paise time2>GC过程中JVM的暂停总时间
(1)根据<starting occupancy1> - <ending occupancy1>得到young区被回收或晋升的内存大小,根据<starting occupancy2> - <ending occupancy2>得到当前整个堆的大小变化,两者的差值就是young区晋升到Old区的值(5)堆快照的分析
(2)若然<ending occupancy2>随时间的延长一直增长,并且伴随频繁的GC,则很有可能是内存泄露
(3)可使用jstat工具分析,其参数的具体含义见P225
(1)可以通过参数: -XX:+HeadDumpOnOutOfMemoryError来配置在内存耗尽时记录下内存快照,同时可以通过-XX:HeadDumpPath来指定文件路径(6)JVM Crash日志分析
<1>可以通过-XX:ErrorFile = /tmp/log/hs_error_%p.log来指定jvm的日志文件
<2>文件信息主要分为四种,退出原因分类、导致退出的Thread信息(栈信息,具体哪行代码出错)、退出时的Process状态信息(所有线程及线程处于的状态,jvm的堆信息)、退出时与操作系统相关信息
<3>退出原因主要三种:
[1]EXCEPTION_ACCESS_VIOLATION:运行的是JVM自己的代码,很可能是JVM的BUG
[2]SIGSEGV:JVM在执行本地代码或者JNI的代码,很可能是第三方本地库有问题
[3]EXCEPTION_STACK_OVERFLOW:这个是栈溢出的错误,可以将JVM的栈的尺寸调大,主要是两个参数
-Xss 和 -XX:StackShadowPages=n(书本此处还分析了汇编指令,和使用linux的gdv工具来core文件,暂跳过)