深入理解java虚拟机第三版读书笔记02

以下是第二章 Java内存区域与内存溢出异常的内容

运行时数据区域

深入理解java虚拟机第三版读书笔记02

程序计数器

特点

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 程序计数器是线程私有的,线程轮换之后通过程序计数器确定要执行的指令位置。
  • 不存在内存溢出。

Java虚拟机栈

特点

  • Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
  • 虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。栈帧的入栈出栈对应一个方法的调用。
  • 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用和返回地址。
  • 局部变量表中的存储空间以 局部变量槽(Slot) 来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配。

异常

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,可能会有这两种情况:
    • 栈帧过多
    • 栈帧过大
  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。(HotSpot虚拟机的栈容量是不可以动态扩展的,也不会导致这种异常)

代码演示:通过-Xss参数来设定栈的大小

(无限递归导致栈帧过多)

/**
*   VM Args:-Xss128k
*   @author zzm
 */
public class JavaVMStackSOF {     
    private int stackLength = 1;
    public void stackLeak() {         
        stackLength++;         
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();         
        try {
            oom.stackLeak();         
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

(栈帧太大)

/**
 * @author zzm
 */
public class JavaVMStackSOF {     
    private static int stackLength = 0;
    public static void test() {
        long unused1, unused2, unused3, unused4, unused5,
        unused6, unused7, unused8, unused9, unused10,              
        unused11, unused12, unused13, unused14, unused15,
        unused16, unused17, unused18, unused19, unused20,
        unused21, unused22, unused23, unused24, unused25,
        unused26, unused27, unused28, unused29, unused30,
        unused31, unused32, unused33, unused34, unused35,
        unused36, unused37, unused38, unused39, unused40, 
        unused41, unused42, unused43, unused44, unused45,
        unused46, unused47, unused48, unused49, unused50, 
        unused51, unused52, unused53, unused54, unused55, 
        unused56, unused57, unused58, unused59, unused60,
        unused61, unused62, unused63, unused64, unused65,
        unused66, unused67, unused68, unused69, unused70,
        unused71, unused72, unused73, unused74, unused75,
        unused76, unused77, unused78, unused79, unused80,
        unused81, unused82, unused83, unused84, unused85,
        unused86, unused87, unused88, unused89, unused90,
        unused91, unused92, unused93, unused94, unused95,
        unused96, unused97, unused98, unused99, unused100;
        stackLength ++;
        test();
        unused1 = unused2 = unused3 = unused4 = unused5 =
        unused6 = unused7 = unused8 = unused9 = unused10 = 
        unused11 = unused12 = unused13 = unused14 = unused15 =
        unused16 = unused17 = unused18 = unused19 = unused20 =
        unused21 = unused22 = unused23 = unused24 = unused25 =
        unused26 = unused27 = unused28 = unused29 = unused30 =
        unused31 = unused32 = unused33 = unused34 = unused35 =
        unused36 = unused37 = unused38 = unused39 = unused40 =
        unused41 = unused42 = unused43 = unused44 = unused45 = 
        unused46 = unused47 = unused48 = unused49 = unused50 =
        unused51 = unused52 = unused53 = unused54 = unused55 =
        unused56 = unused57 = unused58 = unused59 = unused60 =
        unused61 = unused62 = unused63 = unused64 = unused65 =
        unused66 = unused67 = unused68 = unused69 = unused70 =
        unused71 = unused72 = unused73 = unused74 = unused75 =
        unused76 = unused77 = unused78 = unused79 = unused80 =
        unused81 = unused82 = unused83 = unused84 = unused85 =
        unused86 = unused87 = unused88 = unused89 = unused90 =
        unused91 = unused92 = unused93 = unused94 = unused95 =
        unused96 = unused97 = unused98 = unused99 = unused100 = 0;
    }
    public static void main(String[] args) {
        try {             
            test();         
        }catch (Error e){
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

本地方法栈

是java虚拟机栈的native方法版。
HotSpot虚拟机中并不区分虚拟机栈和本地方法栈

Java堆

特点

  • 是各个线程共享的。
  • 所有的对象实例以及数组都应当在堆上分配,日后可能出现值类型的支持。
  • Java堆中可以划分出多个线程私有的分配缓冲区,以提升对象分配时的效率。
  • Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。

异常

如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

代码演示:
(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展,通过参数-XX:+HeapDumpOnOutOf-MemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析)

/**·
*   VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*   @author zzm
*/
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

方法区

特点

  • 是各个进程共享的。
  • 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • JAVA8以前HotSpot使用永久代实现方法区,把堆的垃圾回收机制延申至方法区,JAVA8开始使用位于本地内存中的元空间实现方法区。
  • JAVA虚拟机规范不要求实现垃圾回收。
  • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
  • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
  • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

异常

如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。(由于JAVA8后方法区位于本地内存中,很少出现这种情况,一般是涉及到动态加载过多类)

运行时常量池

  • 运行时常量池是方法区的一部分。
  • 类的字节码中有一项信息是常量池表,用于存放编译期生成的类名、方法名、参数类型与字面量,这部分内容将在类加载后存放到方法区的运行时常量池中。
  • Java语言并不要求常量一定只有编译期才能产生,String类的intern方法可以将字符串常量放进字符串常量池表。

异常

常量池无法再申请到内存时会抛出OutOfMemoryError异常。

intern方法有意思的点

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);
        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。产生差异的原因是,在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在 Java堆上,所以必然不可能是同一个引用,结果将返回false。

而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返回false,这是因为“java”这个字符串在执行String-Builder.toString()之前就已经出现过了(sun.misc.Version类中加载),字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。

直接内存

特点

  • 直接内存就是机器的内存,JDK1.4中的NIO可以使用Native函数库直接分配堆外内存
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理
  • -XX:MaxDirectMemorySize指定可使用的直接内存大小,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致

分配与回收

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃
    圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/%e6%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3java%e8%99%9a%e6%8b%9f%e6%9c%ba%e7%ac%ac%e4%b8%89%e7%89%88%e8%af%bb%e4%b9%a6%e7%ac%94%e8%ae%b002/

发表评论

电子邮件地址不会被公开。