终于到了Java最核心的知识点JVM,今天主要来学习如下知识点:java
Java内存模型
虚拟机对象探秘
类的加载
String类和常量池
Java 虚拟机在执行 Java 程序的过程当中会把它管理的内存划分红若干个不一样的数据区域。JDK. 1.8 和以前的版本略有不一样,如图所示:git
JDK 1.8github
线程私有的:面试
线程共享的:算法
程序计数器
程序计数器主要有两个做用:数组
- 字节码解释器经过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的状况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候可以知道该线程上次运行到哪儿了。
注意:程序计数器是惟一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的建立而建立,随着线程的结束而死亡。缓存
虚拟机栈
Java 虚拟机栈是由一个个栈帧组成,而每一个栈帧中都拥有:局部变量表、操做数栈、动态连接、方法出口信息。多线程
虚拟机栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每个函数调用结束后,都会有一个栈帧被弹出。jvm
局部变量表主要存放了编译器可知的各类数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不一样于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置)。ide
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
Java 虚拟机栈也是线程私有的,每一个线程都有各自的 Java 虚拟机栈,并且随着线程的建立而建立,随着线程的死亡而死亡。
本地方法栈
和虚拟机栈所发挥的做用很是类似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
本地方法被执行的时候,在本地方法栈也会建立一个栈帧,用于存放该本地方法的局部变量表、操做数栈、动态连接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,所以也被称做GC堆(Garbage Collected Heap).从垃圾回收的角度,因为如今收集器基本都采用分代垃圾收集算法,因此 Java 堆还能够细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
上图所示的 eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分状况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,若是对象还存活,则会进入 s0 或者 s1,而且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增长到必定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,能够经过参数 -XX:MaxTenuringThreshold 来设置。
方法区
方法区与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也被称为永久代。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被完全移除了,取而代之是元空间,元空间使用的是直接内存。
运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各类字面量和符号引用)
既然运行时常量池是方法区的一部分,天然受到方法区内存的限制,当常量池没法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7 及以后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存
直接内存并非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用。并且也可能致使 OutOfMemoryError 异常出现。
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,而且检查这个符号引用表明的类是否已被加载过、解析和初始化过。若是没有,那必须先执行相应的类加载过程。
分配内存
在类加载检查经过后,接下来虚拟机将为新生对象分配内存。
初始化零值
内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),这一步操做保证了对象的实例字段在 Java 代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头
初始化零值完成以后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不一样,如是否启用偏向锁等,对象头会有不一样的设置方式。
执行init方法
执行init方法其实就是调用类的构造器方法,这样就能建立出一个可用的Java对象了。
在Hotspot虚拟机中,对象在内存中的布局能够分为3块区域:对象头、实例数据和对齐填充。
对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各类类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位做用。
Java类的加载是动态的,它并不会一次性将全部类所有加载后再运行,而是保证程序运行的基础类(像是基类)彻底加载到jvm中,至于其余类,则在须要的时候才加载。这固然就是为了节省内存开销。
全部的类都由类加载器加载,加载的做用就是将 .class文件加载到内存
类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其余类加载器均由 Java 实现且所有继承自java.lang.ClassLoader
:
%JAVA_HOME%/lib
目录下的jar包和类或者或被-Xbootclasspath
参数指定的路径中的全部类。%JRE_HOME%/lib/ext
目录下的jar包和类,或被java.ext.dirs
系统变量所指定的路径下的jar包。工做过程
- 当AppClassLoader加载一个class时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtensionClassLoader去完成。
- 当ExtensionClassLoader加载一个class时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 若是BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtensionClassLoader来尝试加载;
- 若ExtensionClassLoader也加载失败,则会使用AppClassLoader来加载
- 若是AppClassLoader也加载失败,则会报出异常ClassNotFoundException
其实这就是所谓的双亲委派模型。简单来讲:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
双亲委派模型保证了Java程序的稳定运行,能够避免类的重复加载(JVM 区分不一样类的方式不只仅根据类名,相同的类文件被不一样的类加载器加载产生的是两个不一样的类),也保证了 Java 的核心 API 不被篡改。若是没有使用双亲委派模型,而是每一个类加载器加载本身的话就会出现一些问题,好比咱们编写一个称为java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不一样的Object
类。
注意:类加载器在成功加载某个类以后,会把获得的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
类加载详细过程
加载器加载到jvm中,接下来其实又分了好几个步骤:
加载,查找并加载类的二进制数据,在Java堆中也建立一个java.lang.Class类的对象。
链接,链接又包含三块内容:验证、准备、初始化。
1)验证,文件格式、元数据、字节码、符号引用验证;
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3)解析,把类中的符号引用转换为直接引用
初始化,为类的静态变量赋予正确的初始值。
除了 BootstrapClassLoader 其余类加载器均由 Java 实现且所有继承自java.lang.ClassLoader。若是咱们要自定义本身的类加载器,很明显须要继承 ClassLoader。
String 对象的两种建立方式:
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",若是字符串常量池中没有,则建立一个,而后 str1 指向字符串常量池中的对象,若是有,则直接将 str1 指向"abcd""; String str2 = new String("abcd");//堆中建立一个新的对象 String str3 = new String("abcd");//堆中建立一个新的对象 System.out.println(str1==str2);//false System.out.println(str2==str3);//false
这两种不一样的建立方法是有差异的。
记住一点:只要使用 new 方法,便须要建立新的对象。
String 类型的常量池比较特殊。它的主要使用方法有两种:
直接使用双引号声明出来的 String 对象会直接存储在常量池中。
若是不是用双引号声明的 String 对象,可使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的做用是:若是运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;若是没有,JDK1.7以前(不包含1.7)的处理方式是在常量池中建立与此 String 内容相同的字符串,并返回常量池中建立的字符串的引用,JDK1.7以及以后的处理方式是在常量池中记录此字符串的引用,并返回该引用。
String s1 = new String("计算机"); String s2 = s1.intern(); String s3 = "计算机"; System.out.println(s2);//计算机 System.out.println(s1 == s2);//false,由于一个是堆内存中的 String 对象一个是常量池中的 String 对象 System.out.println(s3 == s2);//true,由于两个都是常量池中的 String 对象
字符串拼接:
String str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing";//常量池中的对象 String str4 = str1 + str2; //在堆上建立的新的对象 String str5 = "string";//常量池中的对象 System.out.println(str3 == str4);//false System.out.println(str3 == str5);//true System.out.println(str4 == str5);//false
尽可能避免多个字符串拼接,由于这样会从新建立对象。若是须要改变字符串的话,可使用 StringBuilder 或者 StringBuffer。
将建立 1 或 2 个字符串。若是池中已存在字符串常量“abc”,则只会在堆空间建立一个字符串常量“abc”。若是池中没有字符串常量“abc”,那么它将首先在池中建立,而后在堆空间中建立,所以将建立总共 2 个字符串对象。
更多关于String的面试题:String常见面试题
今天先对Java虚拟机有个初步的认识,后面咱们会简单介绍一下垃圾回收机制。