• java内存结构(上)


    转载于一篇非常清晰明了的文章:https://blog.csdn.net/wo541075754/article/details/102623406

    转载于比较详细的内存结构文章:https://blog.csdn.net/rongtaoup/article/details/89142396

    JVM内存结构详解

    1 为什么学习jvm内存结构?

    堆内存该设置多大?OutOfMemoryError异常到底是怎么引起的?如何进行JVM调优?JVM的垃圾回收是如何?甚至创建一个String对象,JVM都做了些什么?

    2 jvm内存结构介绍

    java虚拟机在执行程序的过程中会将内存划分为不同的数据区域

     

    1、 JVM五个区中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。图中已经用颜色区分,绿色表示“通行”,橘黄色表示停一停(需等待)。

    2、 堆中存放对象,堆占用的空间最多,程序计数器占用的空间最少,

    3、 JVM 的优化问题主要在线程共享的数据区中:堆、方法区。

    下面分别对这五个区域介绍

    堆:

    存放对象,几乎所有的对象实例都在此分配,堆占用空间大,是java垃圾回收的主要区域,因此成为GC堆。正是因为GC的存在,现代收集器基本都采用分代收集算法,堆进一步细化。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。

    Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

    年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

    老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC

    注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。

    Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上“拼凑”(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

    第一,堆的GC操作采用分代收集算法。

    第二,堆区分了新生代和老年代;

    第三,新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。

    方法区:

    方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。

    正因为如此相像,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但目前实际上是与Java堆分开的(Non-Heap)。

    方法区个性化的是,它存储的是已被虚拟机加载的类信息(版本、方法、字段等)、常量、静态变量、即时编译器编译后的代码等数据,运行时常量池也存在方法区中。

    方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。

    注:JDK1.8 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。元空间两个参数:

     MetaSpaceSize:初始化元空间大小,控制发生GC阈值

     MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

    常量池:

    常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。方法引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

    常量池有什么用 ?

    优点:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

    举个栗子: Integer 常量池(缓存池),和字符串常量池

    Integer常量池:

    我们知道 == 基本数据类型比较的是数值,而引用数据类型比较的是内存地址。

    public void TestIntegerCache()

    {

        public static void main(String[] args)

        {

           

            Integer i1 = new Integer(66);

            Integer i2 = new integer(66);

            Integer i3 = 66;

            Integer i4 = 66;

            Integer i5 = 150;

            Integer i6 = 150;

            System.out.println(i1 == i2);//false

            System.out.println(i3 == i4);//true

            System.out.println(i5 == i6);//false

        }

       

    }

    i1 和 i2 使用 new 关键字,每 new 一次都会在堆上创建一个对象,所以 i1 == i2 为 false。

    i3 == i4 为什么是 true 呢?Integer i3 = 66 实际上有一步装箱的操作,即将 int 型的 66 装箱成 Integer,通过 Integer 的 valueOf 方法。

    public static Integer valueOf(int i) {

            if (i >= IntegerCache.low && i <= IntegerCache.high)

                return IntegerCache.cache[i + (-IntegerCache.low)];

            return new Integer(i);

        }

    Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。

    而 IntegerCache 是 Integer的静态内部类,作用就是将 [-128,127] 之间的数“缓存”在 IntegerCache 类的 cache 数组中,valueOf 方法就是调用常量池的 cache 数组,不过是将 i3、i4 变量引用指向常量池中,没有真正的创建对象。而new Integer(i)则是直接在堆中创建对象。

    IntegerCache 类中,包含一个构造方法,三个静态变量:low最小值、high最大值、和Integer数组,还有一个静态代码块。静态代码块的作用就是在 IntegerCache 类加载的时候,对high最大值以及 Integer 数组初始化。也就是说当 IntegerCache 类加载的时候,最大最小值,和 Integer 数组就已经初始化好了。这个 Integer 数组其实就是包含了 -128到127之间的所有值。

    IntegerCache 源码

    private static class IntegerCache {

            static final int low = -128;//最小值

            static final int high;//最大值

            static final Integer cache[];//缓存数组

            //私有化构造方法,不让别人创建它。单例模式的思想

            private IntegerCache() {}

            //类加载的时候,执行静态代码块。作用是将-128到127之间的数缓冲在cache[]数组中

            static {

                // high value may be configured by property

                int h = 127;

                String integerCacheHighPropValue =

                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");

                if (integerCacheHighPropValue != null) {

                    try {

                        int i = parseInt(integerCacheHighPropValue);

                        i = Math.max(i, 127);

                        // Maximum array size is Integer.MAX_VALUE

                        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);

                    } catch( NumberFormatException nfe) {

                        // If the property cannot be parsed into an int, ignore it.

                    }

                }

                high = h;

                cache = new Integer[(high - low) + 1];//初始化cache数组,根据最大最小值确定

                int j = low;

                for(int k = 0; k < cache.length; k++)//遍历将数据放入cache数组中

                    cache[k] = new Integer(j++);

                // range [-128, 127] must be interned (JLS7 5.1.7)

                assert IntegerCache.high >= 127;

            }

        }

    而 i5 == i6 为 false,就是因为 150 不在 Integer 常量池的最大最小值之间【-128,127】,从而 new 了一个对象,所以为 false。

    再看一段拆箱的代码。

    public static void main(String[] args){

           Integer i1 = new Integer(4);

           Integer i2 = new Integer(6);

           Integer i3 = new Integer(10);

           System.out.print(i3 == i1+i2);//true

        }

    由于 i1 和 i2 是 Integer 对象,是不能使用+运算符的。首先 i1 和 i2 进行自动拆箱操作,拆箱成int后再进行数值加法运算。i3 也是拆箱后再与之比较数值是否相等的。所以 i3 == i1+i2 其实是比较的 int 型数值是否相等,所以为true。

    String常量池:

    String 是由 final 修饰的类,是不可以被继承的。通常有两种方式来创建对象。

    //1、

    String str = new String("abcd");

    //2、

    String str = "abcd";

    第一种使用 new 创建的对象,存放在堆中。每次调用都会创建一个新的对象。

    第二种先在栈上创建一个 String 类的对象引用变量 str,然后通过符号引用去字符串常量池中找有没有 “abcd”,如果没有,则将“abcd”存放到字符串常量池中,并将栈上的 str 变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再常量池中创建“abcd”,而是直接将 str 引用指向常量池中的“abcd”。

    对于 String 类,equals 方法用于比较字符串内容是否相同; == 号用于比较内存地址是否相同,即是否指向同一个对象。通过代码验证上面理论。

    public static void main(String[] args){

           String str1 = "abcd";

           String str2 = "abcd";

           System.out.print(str1 == str2);//true

        }

    首先在栈上存放变量引用 str1,然后通过符号引用去常量池中找是否有 abcd,没有,则将 abcd 存储在常量池中,然后将 str1 指向常量池的 abcd。当创建 str2 对象,去常量池中发现已经有 abcd 了,就将 str2 引用直接指向 abcd 。所以str1 == str2,指向同一个内存地址。

    public static void main(String[] args){

           String str1 = new String("abcd");

           String str2 = new String("abcd");

           System.out.print(str1 == str2);//false

        }

    str1 和 str2 使用 new 创建对象,分别在堆上创建了不同的对象。两个引用指向堆中两个不同的对象,所以为 false。

    关于字符串 + 号连接问题:

    对于字符串常量的 + 号连接,在程序编译期,JVM就会将其优化为 + 号连接后的值。所以在编译期其字符串常量的值就确定了。

    String a = "a1";  

    String b = "a" + 1;  

    System.out.println((a == b)); //result = true 

    String a = "atrue";  

    String b = "a" + "true";  

    System.out.println((a == b)); //result = true

    String a = "a3.4";  

    String b = "a" + 3.4;  

    System.out.println((a == b)); //result = true

    关于字符串引用 + 号连接问题:

    对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象。

    public static void main(String[] args){

           String str1 = "a";

                String str2 = "ab";

                String str3 = str1 + "b";

                System.out.print(str2 == str3);//false

        }

    对于上边代码,str3 等于 str1 引用 + 字符串常量“b”,在编译期无法确定,在运行期动态的分配并将连接后的新地址赋给 str3,所以 str2 和 str3 引用的内存地址不同,所以 str2 == str3 结果为 false

    通过 jad 反编译工具,分析上述代码到底做了什么。编译指令如下:

    经过 jad 反编译工具反编译代码后,代码如下

    public class TestDemo

    {

        public TestDemo()

        {

        }

        public static void main(String args[])

        {

            String s = "a";

            String s1 = "ab";

            String s2 = (new StringBuilder()).append(s).append("b").toString();

            System.out.print(s1 = s2);

        }

    }

    发现 new 了一个 StringBuilder 对象,然后使用 append 方法优化了 + 操作符。new 在堆上创建对象,而 String s1=“ab”则是在常量池中创建对象,两个应用所指向的内存地址是不同的,所以 s1 == s2 结果为 false。

    注:我们已经知道了字符串引用的 + 号连接问题,其实是在运行期间创建一个 StringBuilder 对象,使用其 append 方法将字符串连接起来。这个也是我们开发中需要注意的一个问题,就是尽量不要在 for 循环中使用 + 号来操作字符串。看下面一段代码:

    public static void main(String[] args){

            String s = null;

            for(int i = 0; i < 100; i++){

                s = s + "a";

            }

        }

    在 for 循环中使用 + 连接字符串,每循环一次,就会新建 StringBuilder 对象,append 后就“抛弃”了它。如果我们在循环外创建StringBuilder 对象,然后在循环中使用 append 方法追加字符串,就可以节省 n-1 次创建和销毁对象的时间。所以在循环中连接字符串,一般使用 StringBuilder 或者 StringBuffer,而不是使用 + 号操作。

    public static void main(String[] args){

            StringBuilder s = new StringBuilder();

            for(int i = 0; i < 100; i++){

                s.append("a");

            }

        }

    使用final修饰的字符串

    public static void main(String[] args){

            final String str1 = "a";

            String str2 = "ab";

            String str3 = str1 + "b";

            System.out.print(str2 == str3);//true

        }

    final 修饰的变量是一个常量,编译期就能确定其值。所以 str1 + "b"就等同于 "a" + "b",所以结果是 true。

    String对象的intern方法。

    public static void main(String[] args){

            String s = "ab";

            String s1 = "a";

            String s2 = "b";

            String s3 = s1 + s2;

            System.out.println(s3 == s);//false

            System.out.println(s3.intern() == s);//true

        }

    通过前面学习我们知道,s1+s2 实际上在堆上 new 了一个 StringBuilder 对象,而 s 在常量池中创建对象 “ab”,所以 s3 == s 为 false。但是 s3 调用 intern 方法,返回的是s3的内容(ab)在常量池中的地址值。所以 s3.intern() == s 结果为 true。

    原文链接:https://blog.csdn.net/rongtaoup/article/details/89142396

  • 相关阅读:
    【第4题】 什么是https
    【第3题】 两个队列生成一个栈
    【第2题】 链表的逆置
    【第1题】 Pythonn内存管理以及垃圾回收机制
    tmunx error:invalid option: status-utf8 invalid option: utf8
    ubuntu install google-chrome-stable
    使用pyenv安装多个Python版本
    Linux命令行烧录树莓派镜像至SD卡
    freenode configuration sasl authentication in weechat
    尝试IRC & freenode
  • 原文地址:https://www.cnblogs.com/fulong133/p/12437373.html
Copyright © 2020-2023  润新知