1、Java内存分配
通俗来讲,分为堆内存和栈内存,细化来说,可分为以下几块:
程序计数器
记录当前线程执行的指令的地址。因为CPU同一时间只能执行一条指令,但CPU执行时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址。
栈
存储基础数据类型(int,long,double,float,boolean,byte,char,short)以及对象引用。基础数据类型直接将值保存在栈中,引用类型的变量保存的是指向堆的指针。方法一旦结束,栈中的数据立刻销毁。类中的方法本身是指令的一部分,保存在栈中。栈的内存管理是顺序分配的,而且定长,不存在内存回收问题
本地方法栈
未执行本地方法服务(native method),部分JVM将其和栈合在一起。
堆
存储对象本身,如被new出的对象,JVM中只有一个堆,为所有线程共有的内存区域,GC主要管理的就是这一块内存,当栈中没有变量指向堆中的对象时,GC回收扫描时就会将它销毁。堆的内存管理是随机分配内存,不定长度,存在内存分配和回收的问题
方法区(也在堆中?独立于堆中?)
所有线程共享,用于存储被虚拟机加载的类信息,常量,静态变量(静态变量在栈中,因此方法区可能也在栈中?),编译器编译后的代码等数据。方法区是堆的一个逻辑组成;该区域的数据很少被回收。常量池在方法区中。
注意:程序计数器、栈、本地方法栈的生命周期随线程的生命周期,这三块区域是线程专享;堆和方法区是线程共享.
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指 定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。
JVM Exception
StackOverflowError:如果线程请求的栈内存超过虚拟机允许的大小,则抛出此异常。
OutOfMemoryError:如果虚拟机可以动态扩展栈(现在基本所有JVM都可扩展,但是规定了扩展的大小),但扩展时无法申请到足够的内存,则抛出该异常。
GC 垃圾回收算法
GC的作用是回收没用的对象,清理内存中的碎片。
垃圾回收要做的两件事:判断哪些对象可回收,回收被无用对象占用的内存空间,使该空间可再次被分配
垃圾是否可回收判断方法
判断是否对象可回收:GC 回收时,会先判断堆中的对象是否还在被引用,常见的判断方法为
引用计数法
给对象添加一个计数器,每当有一个地方引用它时,计数器就加1;引用失效时,计数器就减1;当计数器为0时,则认为该对象是不可能再被引用了,可以被回收了
可达性分析法
找一个对象作为Root(正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量)),向下搜索,搜索走过的路径称为引用链。当一个对象没有任何引用链可到达Root节点,则认为该对象可以被回收了。
GC垃圾回收算法
分配内存时、回收内存时,内存空间变得碎片化、不连续,需要清理,碎片清理将占用的内存移动堆的另一端:
标记清除法
先标记出堆中要回收的对象,然后回收被标记的对象占用的空间;该方法会造成较多的内存碎片
复制法
将内存空间分为均等的两块,先使用其中的一块,当这一块的内存用完了,就将这块中存活的对象复制到另一块空闲的内存中,然后将原先的内存全部清理掉,这样可以避免内存碎片。缺点是使得可用内存较少了一半。
标记整理法
和标记清除类似,也是先标记要回收的对象,然后将存活的对象向一端移动,使得内存分为两块,一端是存活的对象,一端是需回收的对象,然后再清理要回收的这一端,这样就不会产生内存碎片。
分代收集算法
将内存划分成三块:年轻代,年老代,持久代(堆内存=持久代大小+年轻代大小+年老代大小)
持久代
用于存放静态文件,如今Java类、方法等,持久代对垃圾回收没有显著影响。但是有些应用可能动态生成或者调用一些class,这时候需要设置一个较大的持久代空间来存放这些可能新增的类。持久代一般固定大小为64m。
年轻代
新生成的对象都是放到年轻代里。年轻代的目的是为了快速的回收生命周期短的对象。年轻代一般分为一个Eden区,两个或多个Survivor区。对象一般在Eden区中生成,当Eden区满了时,GC将清除非存活对象,将Eden区中还存活的对象移动到Survivor区(做了一次Scanvenge GC),当这个Survivor区满了时,将其中的存活对象移动到另一个Survivor区中(做了一次Scanvenge GC)。以此类推,当最后一个Survivor区满了时,找出这个Survivor区中还存活的且是从第一个Survivor区中移动过来的对象,将他们移动到年老代中(做了一次Scanvenge GC)。增加Survivor区的数量,可以增加对象在年轻代中的存在时间。每次Scanverge GC,该对象年龄增大一岁,当到达指定岁数时,可移到年老代中;当还没到达年龄时,年轻代又满了时,会优先将年龄大的移到年老代中。
年老代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。年老代中可以认为存放的都是一些生命周期较长的对象。
常见设置:
-Xmx1024m:设置JVM最大可用内存为1024m
-Xms1024m:设置JVM初始内存为1024m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xss128k:设置每个线程分配的堆栈大小。该值越小,可分配的线程数越多,但OS一般对线程数有最大限制。
-Xmn1024m:设置年轻代大小为1024m。
-XX:MaxPermSize=16m:设置持久代大小为16m。持久代一般默认为64m。
-XX:NewRatio=4:设置年老代与年轻代(包括Eden和两个Survivor区,除去持久代)的大小比值。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
GC 分类
GC分为主GC和次GC
主GC
即FullGC,对整个堆进行整理,会暂停所有其他线程,应尽量减少主GC的次数。
触发条件:年老代被写满;持久代被写满;System.gc()被显示调用。
次GC
即Scanvenge GC,主要是在年轻代中出现。
触发条件:当Eden区或Survivor区满了时会被触发,清理非存活对象,将存活对象移动到Survivor区或年老代中。
减少GC的方法:
a、不要显示的调用 System.gc();该方法会建议JVM进行主GC,在有些情况下会触发主GC,增加程序停顿的次数。
b、尽量减少临时对象的使用。
c、对象不用时最好显示的置为 null;null 的对象会被当做垃圾处理,显示置为 null 利于垃圾回收。
d、尽量使用 StringBuffer,StringBuilder替代 String;String 累加字符串会创建多个对象。
e、尽量少用静态变量;静态变量不会被GC,一直会占用内存。
f、能用基础数据类型(int,long),就不用封装类型(如Long,Integer),占用空间更少。
g、尽量不在短时间内大量创建应用或删除应用
回收器选择(to be continue…)
JVM提供三种方案:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况,一般的JVM主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
静态方法和非静态方法
当一个 class 文件被 ClassLoader load 进入 JVM 后,方法指令保存在 Stack 中,此时 Heap 区没有数据。然后程序技术器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问 Heap 数据区的(因为没有 heap 的地址指针);非静态方法则不同,非静态方法中隐含了指向 heap 的指针的隐含参数,用来指向方法对应的对象。如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先 new 对象,在 Heap 中分配数据,并把 Stack 中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到 Heap 数据区了。