zoukankan      html  css  js  c++  java
  • 运行时数据区介绍

    其中线程共享区域:方法区 、 堆

    线程私有区域:虚拟机栈、本地方法栈、程序计数器

    程序计数器

    程序计数器是一块非常小的内存空间,主要是用来对当前线程所执行的字节码的行号指示器;
    而且程序计数器是内存区域中唯一一块不存在OutOfMemoryError的区域

    虚拟机栈

    程序员经常说“堆栈”,其中的栈就是虚拟机栈,更确切的说,大家谈的栈是虚拟机中的局部变量表部分;

    虚拟机栈描述的是:Java方法执行的内存模型;(说白了就是:

    虚拟机栈就是用来存储:局部变量、操作栈、动态链表、方法出口这些东西;

    这些东西有个特点:都是线程私有的,所以虚拟机栈是线程私有的

    因为虚拟机栈是私有的,当线程调用某一个方法再到这个方法执行结束;其实就是对应着一个栈帧在虚拟机入栈到出栈的过程;

    对于虚拟机栈可能出现的异常有两种

    1:如果线程请求的栈深度大于虚拟机栈允许的最大深度,报错:StackOverflowError
    (这种错误经常出现在递归操作中,无限制的反复调用方法,最终导致压栈深度超过虚拟机允许的最大深度,就会报错)
    
    2:java的虚拟机栈可以进行动态扩展,但随着扩展会不断的申请内存,当无法申请足够内存的时候就会报错:OutOfMemoryError

    本地方法栈

    本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务(比如C语言写的程序和C++写的程序)

    Java堆

    Java堆是Java虚拟机管理内存最大的一块区域;并且Java堆是被所有线程共享的一块内存区域(最大的区域);

    对于堆内存唯一的目的就是用来存放对象实例的,而且几乎所有的对象实例都是堆内存中分配的内存(可能会有一些对象逃逸分析技术会导致对象实例不是在Java堆中进行内存分配的)

    经常会听到一些程序说“调优”,其中调优的95%部分都是在跟Java堆有关系的;

    因为Java堆是垃圾收集器管理的主要区域

    堆是JVM管理的内存最大的一块,随着虚拟机的启动而创建的。所有数组的内存和new出来的对象都是从这里分配的内存。简单说就是程序员的代码可触及的地方.

    而且一个JVM实例运行的时候只有一个Heap区域,这个区域会被所有的线程共享

    堆里面又分了区域

    1、新生代
    2、老年代

    新生代

    刚创建的对象都是在新生代的,然后垃圾回收的线程扫一遍,如果是垃圾就回收,不是垃圾就年龄+1,新生代和老年代的触发点是可以人为配置的

    老年代

    新生代的对象进入老年代以后,仍然需要垃圾回收的线程扫描,如果发现垃圾了就回收,如果没有垃圾的话,就年龄+1. 最后对象在无用后会被GC掉

    调整参数:

    -Xms: 2G

    -Xmx:2G

    -XX:NewSize和-XX:MaxNewSize用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
    -XX:SurvivorRatio 用来控制Eden和survivor Ratio比例
    -Xms:JVM初始分配的内存由-xms决定。系统默认给-xms的大小是服务器物理内存的1/64
    -Xmx:JVM最大分配的内存由-xmx决定。系统默认给-xmx的大小是服务器物理内存的1/4

    注意:

    当我们服务器的空余堆内存小于40%的时候,JVM就会增大堆内存,一直增加到-xmx限定的内存大小。
    当我们的服务器空余内存大于70%的时候,,JVM就会减小堆内存,直到达到-xms限制的内存大小。
    优化:因此,我们一般都是把JVM的-xms和-xmx都设置成相等。避免每次GC够调整堆的大小,造成内存抖动。

    方法区

    方法区和java堆一样,是线程共享的区域;

    方法区的作用的就是用来存储:已经被虚拟机加载的类信息、常量、静态变量等

    而且方法区还有另一种叫法:【非堆】,也有人给方法区叫做永久代

    当方法区存储信息过大时候,也就是无法满足内存分配的时候报错

     

    运行时常量池

    运行时常量池是方法区中的一部分,主要是用来存放程序编译期生成的各种字面量和符号引用,也就是在类加载之后会进入方法区的运行时常量池中存放

     

    直接内存

    直接内存(Direct Memory)并不是运行时数据区中的部分;但是这块儿往往会被大多数程序员忽略,不小心也会导致OOM的错误;

    这是因为在JDK1.4之前java操作数据过程中使用的IO操作是原始的socket IO

    传统的IO,通过socket的方式发送给服务端,需要干些什么事情:

    1、先把文件读到操作系统的缓存当中
    2、再把操作系统的缓存中内容读到应用中
    3、再从应用的缓存当中读到发送的socket缓存当中
    4、在从socket缓存当中把数据发出去

    总共做了4次的拷贝:

    NIO:

    NIO比较传统IO的话,系统中的buffer不再需要拷贝给应用了​而是read buffer 直接拷贝给socket buffer​我们的应用只需要在两个buffer之间建立一个管道的​这样省略了两次的copy。速度就快了很多

    NIO可以直接使用Native(本地方法栈)函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为堆外内存的引用进行操作;

    所以有时候程序员在分配内存时候经常会忽略掉直接内存。导致各个区域的内存总和大于物理内存限制,然后OOM

    常见的:OutOfMemeoryError

    Java堆溢出

    Vm Options添加:

    -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 出现OOM错误的时候产生堆栈日志:

    使用MAT分析堆栈日志:

    打开堆内存的树信息:

    点开树结构:

    可以看到出现OOM是因为创建了太多的OOM_demo这个类造成的,这样通过代码查找就能找到问题所在的根源了;

    注意:大数据框架的堆栈信息也是通过以上方法搞定的;

    虚拟机栈和本地方法栈溢出

    Java虚拟机规范中描述了两种异常:

    1:如果线程请求的栈深度大于虚拟机允许的最大深度,则报错:StackOverFlowError
    
    2:如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError

    StackOverFlowError

    -Xss128K

    public class StackOverFlow {
        public static void main(String[] args) {
            new StackOverFlow().pushStack();
        }
        int index = 0;
        public void pushStack(){
            System.out.println("压栈第 :  "+index++);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread().getState();
            pushStack();
        }
    }

    stackOOM

    -Xss2m

    public class Stack_OOM {
        public static volatile int index = 0;
        public static void main(String[] args) {
            new Stack_OOM().addThread();
        }
    
        public void dont_stop(){
            while (true){
    
            }
        }
    
        public void addThread(){
            while (true){
                Thread thread = new Thread(new Runnable()
                {
                    public void run()
                    {
                        System.out.println(index++);
                        dont_stop();
                    }
                });
                thread.start();
            }
        }
    }
    
    public class Stack_OOM {
        public static volatile int index = 0;
        public static void main(String[] args) {
            new Stack_OOM().addThread();
        }
    
        public void dont_stop(){
            while (true){
    
            }
        }
    
        public void addThread(){
            while (true){
                Thread thread = new Thread(new Runnable()
                {
    
                    public void run()
                    {
                        System.out.println(index++);
                        dont_stop();
                    }
                });
                thread.start();
    
            }
        }
    }

    每个方法在执行的时候都会创建一个栈帧,这个栈帧伴随着方法从创建到执行完成;

    对于虚拟机来说:总内存 –Xmx - MaxPermSize-程序计数器消耗内存(可以忽略) = 虚拟机栈+本地方法栈

    也就意味着说,如果每个线程分配的内存越大,可以创建的线程数就非常少,建立线程的时候就特别容易把内存吃光;

    所以在出现创建过多的线程导致OOM的时候,我们可以减少最大堆外内存或者减少栈容量(-Xss)来提高线程的数量;这样在高并发情况下可以解决因为创建过多的线程导致的OOM

    方法区和运行时常量池溢出:

    在方法区中有个小块区域叫做“常量池”。

    例子1:

    String a = "123";
    String b = "123";
    System.out.println(a == b);

    上面代码的结果为true,因为String + 变量 是将变量放在常量池的;

    ==操作比较的是两个变量的值是否相等

    对于引用型变量表示的是两个变量在堆中存储的地址是否相同,即栈中的内容是否相同。

    equals操作表示的两个变量是否是对同一个对象的引用,即堆中的内容是否相同

    例子2:

    String aa = new String("123");
    String bb = new String("123");
    System.out.println(aa == bb);

    上面代码的结果为false;

    通过new生成的对象是放在堆区的,使用==比较对象的地址,所以结果为false;

    例子3:

    String c = "c";
    String cc = new String("c");
    System.out.println(c == cc);

    上面的列子中String c = "c";是将变量值存放在常量池的;

    String cc = new String("c");是将对象存放在堆中的,所以两个在==号下是false

    例子4:

    String d = "d";
    String dd = new String("d").intern();
    System.out.println(d == dd);
    
    

    String d = "d";是将值存储在常量池的

    
    

    String dd = new String("d")是将值存储在对象中的

    
    

    但是使用intern()方法之后,d == dd就变成true了

    
    

    当调用intern()方法时,不管使用什么方式定义一个字符串,都会首先在常量池中查找是否有相应的字符串存在,如果有,直接返回常量池中的引用,否则,在常量池中生成相应的字符串并返回引用

    
    

    所以往往在生产上会在这方面做一些优化:



    因为使用intern()比不使用intern()消耗的内存更少; 注意:使用intern()方法后程序运行时间有所增加,这是因为程序中每次都是用了new String后又进行intern()操作的耗时时间,但是不使用intern()占用内存空间导致GC的时间是要远远大于这点时间的

    可是如果有非常多的值通过intern存储到常量池中的时候,常量池的空间或者说方法区的空间无法在继续添加值的时候,就会报错:OutOfMemoryError:PermGen space

    例子:

    VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M

    List<String> list = new ArrayList<String>();
    int i=0;
    while (true){
        list.add(String.valueOf(i).intern());
    }

    注意:在JDK1.8以后,正式移除了永久代!,取而代之的是【元空间】

    元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存[所以元空间Metaspace仍然在非堆中]。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

    -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 
     
    -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

    除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

    -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 
    -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

    所以上面的VM Args的参数稍微修改:

    -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m

    public class Demo {
        public static void main(String[] args) {
            int index = 1;
            List<String> list = new ArrayList<String>();
            while (true){
                String str = "写个长一点的字符串好报错" + index++;
                list.add(str.intern());
            }
        }
    }

    报错:

    Error occurred during initialization of VM
    
    OutOfMemoryError: Metaspace

    直接内存溢出

    当程序中使用NIO存储数据,存储的数据容量超过了本地方法栈允许的容量的时候,就会报错: java.lang.OutOfMemoryError: Direct buffer memory

    -Xmx 20m -XX:MaxDirectMemorySize=10m -XX:+PrintGCDetails

    代码:

    public class DirectMemOOM {
        private static final int _1m = 1024*1024;
        public static void main(String[] args) throws IllegalAccessException {
            ByteBuffer.allocateDirect(11*_1m);
        }
    }
  • 相关阅读:
    九大经典算法之插入排序、希尔排序
    1072 开学寄语 (20 分)
    1070 结绳 (25 分
    查找字符串中的所有数字
    通过类继承计算梯形面积
    将命令的输出生成一个Web页面
    从Internet下载一个文件
    使用Excel管理命令输出
    将一个命令的输出保存到CSV文件
    使用属性存储用户编号和姓名
  • 原文地址:https://www.cnblogs.com/niutao/p/10555865.html
Copyright © 2011-2022 走看看