JVM内存分配
内存分配其实真正来讲是有三种的、但对于JVM来说只有两种
- 栈内存分配:
大家在调优的过程中会发现有个参数是-Xss 默认是1m,这个内存是栈内存分配, 在工作中会发现栈OutOfMemory Error内存溢出、就是因为它的内存空间不够了 一般情况下没有那么大的栈、除非你的一个方法里边有几十万行代码、一直往那压、不出,所以导致栈的溢出、栈的内存分配直接决定了你的线程数 、比如说你默认情况下是1m 、系统一共给你512m、那最高可以分配512个线程,再多系统分配不了啦、因为没有那么多的内存 、像tomcat、resin、jboss等、有个最大线程数、要根据这个来调、调个100万没有意义、分配不了那么大、调太少整个性能发挥不出来 ,调这个 、跟你的cpu有关系、需要找一个折中位置 、根据应用 、是IO密集型的还是CPU密集型的来调-Xss的值、它这里边主要保存了一些参数 、还有局部变量 、就比如说写代码、有开始有结束、这里边肯定定义了很多变量、比如:int x=1 y=0 只要在这方法内的都属于局部变量 、因为你要做运算、要把这东西存住、只有等程序结束的时候才能销毁,对于这种参数是不会产生线程安全问题、因为线程是私有的
- 堆内存分配:
Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢
jvm堆结构
(图一)
1.Young(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Old。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。
2.Old(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
3.Permanent:(持久代)
也叫方法区、用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。
通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是值指MethodArea,不属于Heap。
java堆结构和垃圾回收
图(二)
Direct Momery 严格意义来说也算堆,它是一块物理内存、可以分为操作系统内存、是比较快的、不会走JVM 在java里边实现了内存映射、这样速度更快
CodeCache 放一些字节码、类的信息会放在里边
Permanent Generation space 方法区、严格意义来说也属于堆
Eden Space 区
Survivor Space区
Tenured Generation Old区(年老代)
JVM GC 管理
调优大部分调优的是怎么回收,Minor GC 回收Eden Space和 Survivor Space , Full GC回收所有区域
不管什么GC,回收过程中会出现暂停、回收过程中用户线程是不会工作的、这样就造成程序卡了 这是无法改变不了的事实、避免不了、不过可以优化暂停时间的长短
原则上不能出现Full GC 、所有区域都要跑一遍 、出现Full GC 应用就不可用
Jvm 堆配置参数
1、-Xms初始堆大小
默认物理内存的64/1(<1GB),建议小于1G、可根据应用业务调节
2、-Xmx最大堆大小
默认物理内存的4/1(<1GB)、建议小于1G、实际中建议不大于4GB(否则会出现很多问题)
3、一般建议设置 -Xms= -Xmx
好处是避免每次在gc后、调整堆的大小、减少系统内存分配开销
4、整个堆大小=年轻代大小+年老代大小+持久代大小(Permanent Generation space区、也会被Full GC回收)
jvm新生代(young generation
图(三)
1、新生代=1个eden区和2个Survivor区
2、-Xmn 年轻代大小
设置年轻代大小、比如-Xmn=100m那么新生代就是100m,然后共享
3、-XX:NewRatio
年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
4、-XX:SurvivorRatio
Eden区与Survivor区的大小比值,设置为8(默认是8) ,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
比如新生代=100m,设置-XX:SurvivorRatio为8,那E =80m S0 =10m S1=10m(1/10)
5、用来存放JVM刚分配的Java对象
java老年代(tenured generation)
图(四)
1、老年代=整个堆-年轻代大小-持久代大小
年轻代就是上面讲的-xmn配置的参数、持久代参数默认是0
2、年轻代中经过垃圾回收没有回收掉的对象被复制到年老代。
就是这个对象收集完一次、发现被引用了、某个地方使用了、回收不掉才放进去,一般是多次回收、从E区回收过程中、先进S0或者S1、S0或者S1再回收一次、回收不掉再放到年老区
3、老年代存储对象比年轻代年龄大的多,而且不乏大对象。
对互联网企业来说、最常用的是"缓存"的对象比较多、缓存一般会用弱引用、但弱引用也不会轻易被回收的、除非是在整个堆的内存不够的情况下、防止你的内存宕机、强引用是和垃圾回收机制相关的。一般的,如果一个对象可以通过一系列的强引用引用到,那么就 说明它是不会被垃圾回收机制(Garbage Collection)回收的,
刚才说了缓存对象一般是弱引用、有些数据丢了是没关系的、只是提高你的系统性能才放到缓存里边去、但是如果有一天内存不够了 、缓存占了很大一部分对象、你不回收的话、你整个系统都不可用了、整个服务都不能用了、如果回收掉、我可以从数据库去取、可 能速 度慢点、但是我的服务可用性不会降低
比如说刚开始分配的对象 、这个对象暂定是OLD区、刚开始一部分内存区域被缓存占据了、一般情况下对于一个缓存的设计都有初始值、对于java来说、比较通用的缓存是可以自动伸缩的、
如图(四)整个OLD区50M有45M是被缓存占据了、不会被回收掉、那整个OLD区只有5M可以用了 、假如E区有40M 、S0 分配10M 、S1分配也是10M 、理想情况下、经过E区到S0、S1到老年代的大小不到1M、 那5M就够了、不会出现FULL GC 、也不会出现 内存溢出、一旦你的对象大于5M、比如10M的数据、 放不进去了、就会出现FULL gc 、FULL gc会把整个缓存全都收掉、瞬间缓存数据就没了、然后把10M的数据放进去、这就是弱引用、可以理解为这是一种服务降级、如果是强引用那就直接挂了
4、新建的对象也有可能直接进入老年代
4.1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0、也就是说所有的默认都在新生代)来代表超过多大时就不再新生代分配,而是直接在老年代分配
4.2、大的数组对象,切数组中无引用外部对象。
5、老年代大小无配置参数
java持久代(perm generation)
1、持久代=整个堆-年轻代大小-老年代大小
2、-XX:PermSize 最小 -XX:MaxPermSize 最大
设置持久代的大小,一般情况推荐把-XX:PermSize设置成 -XX:MaxPermSize的值为相同的值,因为永久代大小的调整也会导致堆内存需要触发fgc。
3、存放Class、Method元信息,其大小与项目的规模、类、方法的数量有关。一般设置为128M就足够,设置原则是预留30%的空间
刚开始设置了128M、随着程序的运行、java有一个叫lib的地方放了很多类库、这个类库并不是所有的都加载的、只有在用的时候或者系统初始化的时候会加载一部分、比如已经占了100M了、但是随着业务的运行会动态去类
库里加、把一些Class文件通过反射的方式装进去、这样你的内存不断增大、达到128M以后就挂了、就会报方法区溢出、怎么做?调大到256M、然后监控、超过阈值再调大、简单方式是调大、另外JDK里边有一个GC可以回收
如果能接受停机、就调大,简单、快速、已解决问题为主
4、永久代的回收方式
4.1、常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收
比如一个常量=5 它的意义就是个值、如果回收、发现它没被引用就被回收了
4.2、对于无用的类进行回收,必须保证3点:
类跟常量不一样、一个类里边可能有好多东西、比如这个类引用那个类、
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)