概览
Java运行时数据区也就是所谓的内存模型。包括以下5部分:
- The pc Register(程序计数器/pc指针)
Each Java Virtual Machine thread has its own pc (program counter) register. (每个java线程都有自己的程序计数器,即线程独享的。程序计数器就是用来记录下一条指令的地址的。) - Java Virtual Machine Stacks(java虚拟机栈/栈)
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. it holds local variables and partial results, and plays a part in method invocation and return(就是常说的栈,用于存储局部变量,函数调用等,是线程独享的。) - Heap(堆)
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads.
The heap is the run-time data area from which memory for all class instances and arrays is allocated.
(数组和对象实例存储的地方,是线程间共享的。注意:谈到垃圾回收时会将堆划分为年轻代、老年代、永久代等,这些划分并非java虚拟机的规范。) - Method Area(方法区)
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads.
The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process.
It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and instance initialization and interface initialization.(用于存储每个类的结构,即类的Class对象,常量池等等,是线程间共享的。)
- Native Method Stacks(本地方法栈)
An implementation of the Java Virtual Machine may use conventional stacks, colloquially called “C stacks,” to support native methods (methods written in a language other than the Java programming language). (就是为调用native方法服务的栈)
详解
先上图,大概看看一个类是如何从加载运到行的。
类的加载过程
当程序要使用某个类时,如果该类还未被加我到内存中,则系统会通过类的如载,类的连接,类的初始化这三个步骤来来完成对类的加载。(简单理解就是将.class文件从磁盘中加载到内存中)
- 类的加载
通过类的完全限定名(包名和类名)查找此类的字节码文件,把类的.class文件中的二进制数据读入到内存中,并存放在方法区内,然后利用字节码文件创建一个Class对象,用来封装类在方法区内的数据结构并存放在堆区内。
●就是指将class文件读入内存, 并为之创建一个Java.lang.Class对象
●加载类会先加载父类 - 连接
验证:确保被加载类的正确性。class 文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身的安全。
准备:为类的静态变量分配内存,并将其设置为默认值。此阶段仅仅只为静态类变量(即 static 修饰的字段变量)分配内存,并且设置该变量的初始值。(比如 static int num = 5,这里只将 num 初始化为0,5的值将会在初始化时赋值)。对于 final static 修饰的变量,编译的时候就会分配了,也不会分配实例变量的内存。
解析:把类中的符号引用转换为直接引用。符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针。相对偏移量或一个间接定位到目标的句柄。
3. 初始化
类加载最后阶段,若该类具有父类,则先对父类进行初始化,执行非final静态变量赋值和静态代码块代码。
实例初始化
在上面类的加载完成后还没有创建类的实例(比如Student类已经加载到内存里面了,但是还没有创建Student对象),因此实例初始化就是创建类的实例。
在完成实例初始化之后,堆中就有了Student对象,此时就可以进行方法的调用或其他操作了。
类加载器
类加载器的作用
负责将.class文件加载到内存中, 并为之生成对应的java.lang.Class对象,上面所说的类加载过程就是类加载器完成的。
Java有以下3种内置类加载器:
-
Bootstrap class loader(根类加载器/引导类加载器):它是虚拟机的内置类加载器,通常表示为null, 并且没有父null。
根类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,且没有父加载器,也没有继承 java.lang.ClassLoader 类。它主要负责加载由系统属性 “sun.boot.class.path” 指定的路径下的核心类库(即<JAVA_HOME>\jre\lib),出于安全考虑,根类加载器只加载 java、javax、sun开头的类。 -
Platform class loader(扩展类加载器):平台类加载器可以看到所有平台类,平台类包括由平台类加载器或其祖先定义的Java SE平台API,其实现类和JDK特定的运行时类扩展类加载器是指由原SUN公司实现的 sun.misc.Launcher.ExtClassLoader类(JDK9jdk.internal.loader.ClassLoaders$PlatformClassLoader类),它是由java语言编写,父加载器是根类加载器。负责加载<JAVA_HOME>\jre\lib\ext目录下的类库或者系统变量"java.ext.dirs"指定目录下的类库。
-
System class loader:(系统类加载器/应用类加载器),也是纯java类,是原SUN公司实现的 sun.misc.Launcher$AppClassLoader 类(JDK9是 jdk.internal.loader.ClassLoaders.AppClassLoader)。它的父加载器是扩展类加载器。它负责从classpath环境变量或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。一般情况下,该类加载器是程序中默认的类加载器,可以通过ClassLoader.getSystemClassLoader() 直接获得。
双亲委派机制
如果需要calssLoader加载一个类时,该classLoader先委托自己的父加载器先去加载这个类,若父加载器能够加载,则由父加载器加载,否则才用classLoader自己加载的这个类。(除根类加载器,其他类加载器都有一个唯一的父类加载器,但他们之间不是继承关系)
为什么使用双亲委派机制:
- **防止类重复加载。**当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次。
- 安全因素,防止加载到与核心API相同的类。假设通过网络传递一个名为java.lang.Object的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的java.lang.Object,而直接返回已加载过的Object.class,这样便可以防止核心API库被随意篡改。
如何破坏双亲委派机制:
- 自定义类加载器继承ClassLoader
- 重写loadClass()方法
问题:可以通过自定义类加载器来加载自己的java.lang.System类吗?
答案:不行,因为加载类过程种是不能用自定义类加载器加载java开头的类,会抛出异常