zoukankan      html  css  js  c++  java
  • 4.运行时数据区及栈

    JVM运行时数据区规范
    线程共享区域:JVM启动的时候,这块区域就开始分配空间。
    线程私有区域:没有线程的时候,这块区域是不存在的。这块空间的生命周期特别短暂,不存在垃圾回收的问题。
    Hotspot运行时数据区
    JDK1.6&JDK1.7&JDK1.8+
    分配JVM内存空间
     分配堆的大小
     –Xms(堆的初始容量)
    -Xmx(堆的最大容量)
    分配方法区的大小
    -XX:PermSize
    永久代的初始容量
    -XX:MaxPermSize
    永久代的最大容量
    -XX:MetaspaceSize
    元空间的初始大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释
    放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
    -XX:MaxMetaspaceSize
    最大空间,默认是没有限制的。
    分配线程空间的大小
    -Xss
    jvm启动的每个线程分配的内存大小,默认JDK1.4中是256KJDK1.5+中是1M
    1.方法区
    存储内容

     1.1 类型信息

    类型的全限定名
    超类的全限定名
    直接超接口的全限定名
    类型标志(该类是类类型还是接口类型)
    类的访问描述符(public、private、default、abstract、fifinal、static).
    1.2类型的常量池
    存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用
    常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。
    因为常量池中保存着所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)。
    1.3字段信息(该类声明的所有字段)
    字段修饰符(public、protect、private、default)
    字段的类型
    字段名称
    1.4方法信息
    方法信息中包含类的所有方法,每个方法包含以下信息:
    方法修饰符
    方法返回类型
    方法名
    方法参数个数、类型、顺序等
    方法字节码
    操作数栈和该方法在栈帧中的局部变量区大小
    异常表
    1.5类变量(静态变量)
    指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。
    1.6指向类加载器的引用
    每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。
    1.7指向Class实例的引用
    类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。
    通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
    1.8方法表
    为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能
    调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的。
    1.9运行时常量池(Runtime Constant Pool)
    Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面常量和符号引用,
    这部分内容被类加载后进入方法区的运行时常量池中存放。
    运行时常量池相对于Class文件常量池的另外一个特征具有动态性,可以在运行期间将新的常量放入池中(典型的如String类的intern()方法)。
    永久代和元空间的区别
    永久代和元空间存储位置和存储内容的区别:
    存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;
    由于永久代它的大小是比较小的,而元空间的大小是决定于内地内存的。
    所以说永久代使用不当,比较容易出现OOM异常。而元空间一般不会。
    存储内容不同,元空间存储类的元信息,[静态变量][常量池]等并入堆中。相当于永久代的数据被分到了堆和元空间中。
    为什么要把永久代替换为元空间?
    1.原来Java是属于Sun公司的,后来JavaOracle收购了。
    Sun公司实现的Java中的JVMHotspot
    当时OracleJavaJVM也有一个实现,交JRockit
    后来Oracle收购了Java之后,也同时想把HotspotJRockit合二为一。他们俩很大的不同,就是方法区的实现。
     2.字符串存在永久代中,容易出现性能问题和永久代内存溢出。
    3.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
    4.永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
    最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。
    JSP页面,需要动态生成Servletclass文件
    运行时常量池和字符串常量池(面试高频)
    字符串常量池如何存储数据的
     

     stringtable是类似于hashtable的数据结构,hashtable/hashmap数据结构如下:

    底层数组是:数组+链表
    数组中的元素是entry对象
    entry对象是包含一个K/V对的。
    K就是hashmap中的key。
    V就是hashmap中的value。
    一个entry对象,如何去在数组中什么位置进行存储呢?
    hash算法来分配
    首先对于要存储到hashmap或者hashtable中的key进行hashcode求值。
    其次拿着这个hashcode值对数组的长度进行取余(余数肯定是0-------数组的长度-1);
    余数是几,则存储到数组的对应下标位置。
    当数组长度过短,容易产生hash冲突,那么hash冲突之后呢,一般使用链表方式去解决。链表的特点是:增删快、查找慢。
    链表的特点是:增删快、查找慢。
    结论:如果链接过长,不利于查找
    字符串常量池查找字符串的方式:
    根据字符串的 hashcode 找到对应entry。如果没冲突,它可能只是一个entry,如果有冲突,它可能是一个entry链表,然后Java再遍历entry链表,匹配引用对应的字符串。
    如果找得到字符串,返回引用。如果找不到字符串,会把字符串放到常量池,并把引用保存到stringtable里
    字符串常量池案例分析
    public class Test {
       public  void test() {
    String str1 = "abc";
    String str2 = new String("abc");
    System.out.println(str1 == str2); //false
    String str3 = new String("abc");
    System.out.println(str3 == str2); //false
    String str4 = "a" + "b";
    System.out.println(str4 == "ab"); true
    final String s = "a";
    String str5 = s + "b";
    System.out.println(str5 == "ab"); true
    String s1 = "a";
    String s2 = "b";
    String str6 = s1 + s2;
    System.out.println(str6 == "ab"); //false
    String str7 = "abc".substring(0,2);
    System.out.println(str7 == "ab"); false
    String str8 = "abc".toUpperCase();
    System.out.println(str8 == "ABC"); false
     String s3 = "abc";
    String s4 = "ab" + getString();
    System.out.println(s3 == s4); //false
     String s5 = "a";
    String s6 = "abc";
    String s7 = s5 + "bc";
    System.out.println(s6 == s7.intern()); //true
    }
    private String getString(){
    return "c";
    }
    }
    String的Intern方法详解
     String a = "hello";
     String b = new String("hello");
     System.out.println(a == b); //false
      String c = "world";
      System.out.println(c.intern() == c); //true
      String d = new String("mike");
      System.out.println(d.intern() == d); //false
      String e = new String("jo") + new String("hn"); newString("john") -->不会放到常量池。
      System.out.println(e.intern() == e); //true --->intern 将字符串放到常量池中。
      String f = new String("ja") + new String("va");
      System.out.println(f.intern() == f); //false (java关键字 字符串在编译器就放入常量池中)
    intern的作用是把new出来的字符串的引用添加到stringtable中,java会先计算string的hashcode,查找stringtable中是否已经有string对应的引用了,如果有返回引用(地址),然后没有把字符串的地址
    放到stringtable中,并返回字符串的引用(地址)。
    public static void main(String[] args) {    
       String s = new String("1");    
       s.intern();    
       String s2 = "1";    
       System.out.println(s == s2);    //false
       String s3 = new String("1") + new String("1");    
       s3.intern();    
       String s4 = "11";    
       System.out.println(s3 == s4); jdk1.6 false jdk1.7+ true
    }
    2.Java堆
    Java堆中,主要是用来存储对象和数组的。
    堆内存划分
    堆被划分为老年代和年轻代。
    年轻代和老年代的内存占比默认为12
    年轻代又划分为:伊甸园(Eden)、两个幸存区(Survivor
    伊甸园和两个幸存区的内存占比默认为811
    之所以我们堆内存要进行以上这么细粒度的内存划分,是为了垃圾回收。
    垃圾回收针对不同情况的对象,回收策略(回收算法)是不同的的。而通过内存的划分,可以将不同的
    算法在不同的区域中进行使用。
    比如说年轻代使用了复制算法。
    年轻代中的对象,生命周期很短,基本上是很快就死了,也就是被GC了。
    老年代中的对象,都是一些老顽固,都是多次回收的对象或者大对象才存到老年代中。
    内存的分配原则
    优先在Eden分配,如果Eden空间不足虚拟机则会进行一次MinorGC
    大对象直接进入老年代{大对象一般是 很长的字符串或数组}
    长期存活的对象也会进入老年代,每个对象都有一个age,当age达到设定的年纪的时间就会进入老年代。默认是15.
    内存分配安全问题
    在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。
    在JVM中有两种解决办法:
    1. CAS,比较和交换(Compare And Swap): CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
    2. TLAB,本地线程分配缓冲(Thread Local Allocation Buffffer即TLAB): 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
    对象的内存布局
    普通对象在内存中存储的布局可以分为4块区域:
    对象头(Header 在hotport称为markword 长度8个字节)
    ClassPointer 指针:--xx:+UseCompressedClassPointers 为4个字节 ,若不开启压缩 为8个字节
    实例数据(Instance Data)引用类型: -XX:UseCompressedOops 开启压缩为4个字节,不开启为8个字节。
    对齐填充(Padding) 这个对齐是8的倍数。
    数组对象在内存中存储的布局可以分为4块区域
    对象头(Header 在hotport称为markword 长度8个字节)
    ClassPointer 指针:--xx:+UseCompressedClassPointers 为4个字节 ,若不开启压缩 为8个字节 。
    数组长度:4个字节。
    实例数据(Instance Data)引用类型: -XX:UseCompressedOops 开启压缩为4个字节,不开启为8个字节。
    对齐填充(Padding) 这个对齐是8的倍数。
    对象头具体包含:(JDK1.8)32位
    对象定位
    1.句柄池 :稳定,对象被移动只需要修改句柄中的地址。
    2.直接指针:指向对象然后指向class,访问速度快。
    Hotspot 中使用直接指针。
     Java虚拟机栈
    虚拟机栈也是线程私有,而且生命周期与线程相同,每个Java方法在执行的时候都会创建一个栈帧。
    栈内存为线程私有的空间,每个线程都会创建私有的栈内存。栈空间内存设置过大,创建线程数量较多时会出现栈内存溢出StackOverflowError
    同时,栈内存也决定方法调用的深度,栈内存过小则会导致方法调用的深度较小,如递归调用的次数较少。
    本地方法栈
    执行其他语言的方法,主要指的就是C语言的方法(也叫本地方法,使用native关键字修饰的方法),它在执行的时候,也需要执行空间,那么这个空间就是本地方法栈
    为什么需要本地方法栈?
    clone方法,不是通过java的构造方法去创建java对象,而是直接操作JVM中的堆内存,进行对象的复制。
    直接操作JVM中的堆内存,这不是Java语言能干的事情,但是还必须要有该功能。怎么办呢?
    通过本地方法接口,去调用C函数。
  • 相关阅读:
    fenby C语言 P32
    fenby C语言 P31 使用数组的指针
    fenby C语言 P30
    fenby C语言 P29
    fenby C语言 P28
    fenby C语言 P27使用指针
    fenby C语言 P25
    fenby C语言 P26
    fenby C语言P24
    Python学习之路:通过socket实现处理多个连接
  • 原文地址:https://www.cnblogs.com/wangyang1991/p/13347151.html
Copyright © 2011-2022 走看看