JVM内存管理学习整理

Java不像C++那样显式的分配和释放内存,对Java程序员是一种解放,很大程度上降低了编程的难度,因为内存管理的工作都交由JVM自动进行。但是JVM自动内存管理也是一把双刃剑,会造成宝贵的资源浪费,搞不好还会造成内存泄露。

内存空间的划分:
Sun jdk也是遵照jvm规范,将内存空间划分为方法区、堆、本地方法栈、pc寄存器、jvm方法栈。如下图:

如图所示,JVM主要包括两个子系统和两个组件。两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统;两个组件分别是Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。

Class loader子系统的作用:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Class loader。 

Execution engine子系统的作用:执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。

Native interface组件:与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。

Runtime Data Area组件:这就是我们常说的JVM的内存了。它主要分为五个部分——

①Method Area(方法区):

方法区存放了要加载的类的信息、类中的静态变量、类中被定义为final类型的静态常量、类中的field信息、类中的方法信息。方法区是全局共享的,特定条件下会进行GC,当方法区要是用的内存大于运行大小时会跑出OutOfMemory异常。

Sun jdk中这块内存对应Permanent Generation,也叫持久代,默认最小16M,最大64M,通过-XX:PermSize和-XX:MaxPermSize参数指定持久代的最小和最大值。

②堆(heap):

堆用于存储对象实例及数组值,可以认为java中所有通过new操作符创建的对象都放在堆中,堆中对象由GC进行回收。一个Java虚拟实例中只存在一个堆空间。

这块内存大小可以通过两个参数进行指定:-Xms和-Xmx。

-Xms表示JVM启动时申请的最小heap内存,默认为物理内存的1/64但小于1G。

-Xmx表示JVM可申请的最大heap内存,默认为物理内存的1/4但小于1G。

默认空闲堆内存小于40%时,JVM会增大heap到-Xmx指定的大小,这个比例可以通过参数-XX:MinHeapFreeRatio=来指定;默认当空闲堆内存大于70%时,jvm会减少heap到-Xms指定的大小,这个比例可以通过参数-XX:MaxHeapFreeRatio=来指定。建议将-Xms和-Xmx设置为相同的值,以避免频繁调整jvm堆大小。
由于不同对象在jvm中存活的时间不同,有的很快就可以回收,有的可能生命周期贯穿整个jvm的生命周期,所以在Sun jdk从1.2开始就对堆内存进行分代管理。如下图:

1. Young(年轻代)
大多数情况下java程序中创建的对象是从新生代分配内存,新生代有两部分组成:Eden Space和两块大小相等的Survivor Space(S0和S1)。可以通过参数-Xmn来指定新生代的大小,通过-XX:SurivorRatio来指定Eden Space和Survivor Space的大小。

2. Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。 

3. Perm(持久代)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

③Method Area(方法区域):被装载的class的信息存储在Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。

④Native method stack(本地方法栈):

本地方法栈用于保存native方法进入区域的地址,支持native方法的执行,Sun jdk中实现是本地方法栈和jvm方法栈是同一个。虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈。

⑤pc寄存器和jvm方法栈:

每个线程都有自己的pc寄存器和jvm方法栈,pc寄存器占用的可能是cpu寄存器或操作系统内存,jvm方法栈占用的为操作系统内存,jvm方法栈为线程私有,线程运行完毕时其对应栈所占用的内存全部自动释放。

PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。

Sun jdk中通过参数-Xss来指定jvm方法栈大小,当jvm方法栈空间不足时抛出StackOverflowError。

内存的分配:

Jvm堆是所有线程共享的,因此在堆上分配内存需要加锁,从而导致创建对象开销较大。当堆空间不足时会触发GC,如果GC后堆空间仍然不足会抛出OutOfMemory异常。

Sun jdk为提升内存分配效率,在新生代的Eden space为每个线程创建一个叫做TLAB(Thread Local Allocation Buffer)的区域,当在线程中创建对象时jvm会尽量在TLAB中分配内存,这时就不需要加锁,节省了创建对象的开销。

还有一种基于逃逸分析的方法,jvm会在栈上直接分配内存,线程结束时自动就释放掉。

内存的回收:

收集器算法

Jvm通过GC来回收内存,GC就是通过分析程序中不再被使用的对象,把这些对象所占的内存收回,GC通常采用收集器方式,主要有引用计数收集器和跟踪收集器。

引用计数收集器:采用分散的管理方式,通过记录对象的引用次数进行判断对象是否可能回收,当对象的引用计数为0时GC就可以回收该对象。但是引用计数器方式有他的缺点:每次对对象的赋值操作都会伴随有引用计数的增减,带来一定的额外消耗;对象间出现循环引用时会失效。所以Sun jdk实现中没有采用引用计数器的方式。

跟踪收集器:采用集中式的管理方式,全局记录数据的引用状态,执行GC时从根集合进行对象扫描,可能会造成应用程序暂停,线程阻塞。主要有复制(copying),标记-清除(mark-sweep),标记-压缩(mark-compact)三种算法实现。

Sun jdk中可用的GC算法

新生代GC(Minor GC):新生代中对象通常存活时间短,对象少,所以选择copying算法实现新生代的GC。GC过程中复制对象时需要一块未使用的内存区来存放存活的对象,这也是新生代划分为Eden,S0,S1的原因。Eden存放刚创建的对象,S0或S1的其中一块用作Minor GC的复制目标空间,另一块被清空;下一次Minor GC时S0和S1交换角色。

串行GC:在整个扫描和复制过程采用单线程的方式来进行,使用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

并行回收GC:在整个扫描和复制过程中采用多线程的方式来进行,使用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

并行GC与旧生代的并发GC配合使用。

旧生代GC:旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行(GCSerial)、并行(GCParallel)和并发。

最后,创建对象可能会触发GC,所以需要频繁创建的对象可以用池来解决。注意对象的作用域,不用的及时显式设置为null,便于GC早点回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值