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函数。
JVM