Jvm内存

作者: 离别刀 | 来源:发表于2018-06-06 22:07 被阅读0次

    本篇重点讲解JVM内存管理和垃圾回收,如下图JVM的基本结构:


    首先理解下JVM工作原理

    JVM俗称java虚拟机,它是用来执行.class文件的,大家通常编写的.java文件最后都会被javac编译成.class字节码文件,JVM将这些.class文件加载到内存中,然后再由JVM中的执行引擎,执行.class中的字节码指令。类加载器ClassLoader就是负责将.class文件装载到JVM的内存。

    JVM中默认有三个ClassLoader分别是:
    1.Bootstrap ClassLoader,负责加载%JRE_HOME%\lib下面的包,如:rt.jar、resources.jar、charsets.jar和class等。
    2.ExtClassLoader,负责加载%JRE_HOME%\lib\ext下面的包。
    3.AppClassLoader,负责加载当前应用的classpath的所有类。
    这个顺序也是jvm启动的时候类的加载顺序。
    下图是类加载的继承关系:



    从上面我们可以看到以上3个类只能加载固定目录的class,其实我们还可以实现自定义ClassLoader加载任意地方的class,比如某个磁盘,或者网络上class资源等。

    类加载有三种方式
    1、命令行启动应用时候由JVM初始化加载
    2、通过Class.forName()方法动态加载
    3、通过ClassLoader.loadClass()方法动态加载
    Class.forName()和ClassLoader.loadClass()区别
    Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
    ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
    注:
    Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

    类的加载时按照双亲委派模型进行,如下:
    1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
    2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
    3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
    4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

    双亲委派模型意义
    -系统类防止内存中出现多份同样的字节码
    -保证Java程序安全稳定运行

    以下是jdk1.8中类的加载源代码

     protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            synchronized(this.getClassLoadingLock(var1)) {
                Class var4 = this.findLoadedClass(var1);
                if(var4 == null) {
                    long var5 = System.nanoTime();
    
                    try {
                        if(this.parent != null) {
                            var4 = this.parent.loadClass(var1, false);
                        } else {
                            var4 = this.findBootstrapClassOrNull(var1);
                        }
                    } catch (ClassNotFoundException var10) {
                        ;
                    }
    
                    if(var4 == null) {
                        long var7 = System.nanoTime();
                        var4 = this.findClass(var1);
                        PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                        PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                        PerfCounter.getFindClasses().increment();
                    }
                }
    
                if(var2) {
                    this.resolveClass(var4);
                }
    
                return var4;
            }
        }
    

    ClassLoader将类加载到内存中,会分配到不同的内存块如下所示:

    1.栈内存,jvm参数 -XSS可以调整单个线程栈大小,每次方法调用就会创建一个贞栈压入栈,线程栈中每个栈贞包含了当前方法的本地变量信息,如:局部变量,常量池指针,操作栈数。每个线程只能读取自己本地变量信息,即使执行了同一段代码,它们也是copy一份到本地变量,因此线程之间的本地变量是完全隔离的。java中原始变量(boolean,byte,short,char,int,long,float,double)都存在线程的栈中,各个线程独自占有,无法共享。

    2.堆内存,通过参数-Xmx和-Xms来调整堆的大小,对于32位机器最大2G,64位无限制。对于分代GC来说:包含新生代,旧生代和持久代。持久代最小值为16MB,最大值为64MB。在JDK1.8以后合并为元生代。java中所有的对象信息包括原始类型(Byte、Integer、Long等),不管是哪个线程创建的,无论成员变量还是方法中的变量,都会被存储在堆中。

    3.静态区(或者叫方法区)也被称为永久代,可以通过-XX:MaxPermSize=512m调整,它也是全部存储在堆中。

    JVM内存回收

    大家都知道Java优于其他编程语言最大的好处是JVM自动管理内存,内存由GC自动回收,但是GC回收也有自己的缺陷:
    1.垃圾并不会按照我们的要求随时进行回收
    2.程序编程人员不能对垃圾回收进行控制
    3.垃圾回收并不会及时的进行内存清理,尽管有时候内存已经不够使用了
    因此就要求我们在写代码的时候能够写出符合GC回收规律的代码,以便GC能快速回收,释放内存,保证程序的正常运行。

    从前面JVM内存的结构我们知道,程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以它们不需要被回收。GC回收最大的块就是堆静态区。简单的说就是,如果某个对象已经不存在任何引用,那么它可以被回收。

    Sun的jvm采用了一种叫做“根搜索算法”,基本思想就是:从一个叫GC Roots的对象开始,向下搜索,如果一个对象不能到达GC Roots对象的时候,说明它已经不再被引用,即可被进行垃圾回收。

    常见的GC回收算法
    1、标记-清除算法(Mark-Sweep):最基础的GC算法,将需要进行回收的对象做标记,之后扫描,有标记的进行回收,这样就产生两个步骤:标记和清除。这个算法效率不高,而且在清理完成后会产生内存碎片,这样,如果有大对象需要连续的内存空间时,还需要进行碎片整理,所以,此算法需要改进。

    2、复制算法(Copying)
    前面我们谈过,新生代内存分为了三份,Eden区和2块Survivor区,一般Sun的JVM会将Eden区和Survivor区的比例调为8:1,保证有一块Survivor区是空闲的,这样,在垃圾回收的时候,将不需要进行回收的对象放在空闲的Survivor区,然后将Eden区和第一块Survivor区进行完全清理,这样有一个问题,就是如果第二块Survivor区的空间不够大怎么办?这个时候,就需要当Survivor区不够用的时候,暂时借持久代的内存用一下。此算法适用于新生代。

    3、标记-整理(或叫压缩)算法(Mark-Compact)
    和标记-清楚算法前半段一样,只是在标记了不需要进行回收的对象后,将标记过的对象移动到一起,使得内存连续,这样,只要将标记边界以外的内存清理就行了。此算法适用于持久代。

    常见的垃圾收集器:
    1、Serial GC。是最基本、最古老的收集器,但是现在依然被广泛使用,是一种单线程垃圾回收机制,而且不仅如此,它最大的特点就是在进行垃圾回收的时候,需要将所有正在执行的线程暂停(Stop The World),对于有些应用这是难以接受的,但是我们可以这样想,只要我们能够做到将它所停顿的时间控制在N个毫秒范围内,大多数应用我们还是可以接受的,而且事实是它并没有让我们失望,几十毫米的停顿我们作为客户机(Client)是完全可以接受的,该收集器适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

    2、ParNew GC。基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率,这样它就可以被用在服务器端(Server)上,同时它可以与CMS GC配合,所以,更加有理由将它置于Server端。

    3、Parallel Scavenge GC。在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。以下给出几组使用组合:

    image 249993-20170308202431391-1323581863.png

    有连线的的部分代表可以联合使用

    4、CMS (Concurrent Mark Sweep)收集器。该收集器目标就是解决Serial GC 的停顿问题,以达到最短回收时间。常见的B/S架构的应用就适合用这种收集器,因为其高并发、高响应的特点。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
    初始标记(CMS initial mark)、并发标记(CMS concurrenr mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)。

    Java程序性能优化

    1、gc调用,调用gc 方法暗示着Java 虚拟机做了一些努力来回收未用对象,以便能够快速地重用这些对象当前占用的内存。当控制权从方法调用中返回时,虚拟机已经尽最大努力从所有丢弃的对象中回收了空间,调用System.gc() 等效于调用Runtime.getRuntime().gc()。

    2、finalize()的调用及重写,gc 只能清除在堆上分配的内存(纯java语言的所有对象都在堆上使用new分配内存),而不能清除栈上分配的内存(当使用JNI技术时,可能会在栈上分配内存,例如java调用c程序,而该c程序使用malloc分配内存时)。因此,如果某些对象被分配了栈上的内存区域,那gc就管不着了,对栈上的对象进行内存回收就要靠finalize()。举个例子来说,当java 调用非java方法时(这种方法可能是c或是c++的),在非java代码内部也许调用了c的malloc()函数来分配内存,而且除非调用那个了 free() 否则不会释放内存(因为free()是c的函数),这个时候要进行释放内存的工作,gc是不起作用的,因而需要在finalize()内部的一个固有方法调用free()。

    优秀的编程习惯
    (1)避免在循环体中创建对象,即使该对象占用内存空间不大。
    (2)尽量及时使对象符合垃圾回收标准(1.对象赋值null,并且以后再也没有使用过,2.对象赋予了新的值,即重新分配了空间)
    (3)不要采用过深的继承层次。
    (4)访问本地变量优于访问类中的变量。

    常见问题

    1、内存溢出
    就是你要求分配的java虚拟机内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

    2、内存泄漏
    是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问,该块已分配出来的内存也无法再使用,随着服务器内存的不断消耗,而无法使用的内存越来越多,系统也不能再次将它分配给需要的程序,产生泄露。一直下去,程序也逐渐无内存使用,就会溢出。

    java线程数 = (系统空闲内存-堆内存(-Xms, -Xmx)- perm区内存(-XX:MaxPermSize)) / 线程栈大小(-Xss)

    以8核16G机器为例jvm设置参数:
    set JAVA_OPTS=%JAVA_OPTS% -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G

    参数调优可以参照https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

    总结

    Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
    在Java虚拟机规范中,对这个区域规定了两种异常情况:
    (1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
    (2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

    下面这篇文章讲的也很好
    https://www.cnblogs.com/lcword/p/5857918.html
    https://www.cnblogs.com/xiaoxi/p/6486852.html

    常见参数调优以及日志查看

    1.2G内存(jdk8)
    
                        -server -Xmx1550m -Xms1550m -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
    
     
           2. 4G内存(jdk8)
    
     
                        -server -Xmx3550m -Xms3550m -Xmn2g -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
    
           3. 2G内存(jdk7)
    
                      -server -Xmx1550m -Xms1550m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:+UseParNewGC  -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=100  -XX:+DisableExplicitGC -XX:+UseAdaptiveSizePolicy
    
     
    
           4. 4G内存(jdk7)
    
                      -server -Xmx3550m -Xms3550m -XX:PermSize=512m -XX:MaxPermSize=512m -XX:+UseParNewGC  -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=100  -XX:+DisableExplicitGC -XX:+UseAdaptiveSizePolicy
    
     k12 JVM设置(jdk7)
    
    -server -Xms1400m -Xmx1400m -Xss256k -XX:NewSize=940M -XX:MaxNewSize=940M -XX:NewRatio=2 -XX:PermSize=128m -XX:MaxPermSize=300m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
    
    
    
    
           2. 实时内存参数查看命令
    
               > jinfo pid
    
               > jmap -head pid
    
           3.线上jvm问题排查步骤
    
                 线上cpu飙高,一定要第一时间dump文件保留现场,dump文件找蒋欢
    
                 命令:jmap -dump:format=b,file=文件名 [pid]
    
                 分析dump文件--个人用mat最方便
    
                    3.1 通过 top 命令找到 CPU 消耗最高的进程,并记住进程 ID。
    
                    3.2 再次通过 top -Hp [进程 ID] 找到 CPU 消耗最高的线程 ID,并记住线程 ID.
    
                    3.3 通过 JDK 提供的 jstack 工具 dump 线程堆栈信息到指定文件中。具体命令:jstack -l [进程 ID] >jstack.log。
    
                    3.4 由于刚刚的线程 ID 是十进制的,而堆栈信息中的线程 ID 是16进制的,因此我们需要将10进制的转换成16进制的,并用这个线程 ID 在堆栈中查找。使用 printf "%x\n" [十进制数字] ,可以将10进制转换成16进制。
    
                    3.5 通过刚刚转换的16进制数字从堆栈信息里找到对应的线程堆栈。就可以从该堆栈中看出端倪。
    

    相关文章

      网友评论

        本文标题:Jvm内存

        本文链接:https://www.haomeiwen.com/subject/rjaesftx.html