1.JVM内存架构
1、方法区:
线程共享;被所有线程共享的一块内存区域;用于存储已被虚拟机加载的类信息,常量,静态变量等。
2、堆:
线程共享;被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例。
3、Java虚拟机栈:
线程私有;每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈,动态连接,方法返回地址等;每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
4、本地方法栈:
线程私有;主要为虚拟机使用到的Native方法服务。
5、程序计数器:
线程私有;是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
2.Java类的加载机制
1.类的生命周期
1.1.加载
指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.class对象,用来封装类在方法区中的class对象,作为对方法区中这些数据的访问入口。
1.2.连接
一般会跟加载阶段和初始化阶段交叉进行,过程由三部分组成:验证、准备和解析三步
(1)验证:确保被加载的类的正确性
(2)准备:为静态变量分配内存,并将其初始化为默认值
(3)解析:把常量池中的符号引用转换为直接引用,说白了就是jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
1.3.初始化
将静态变量(类变量)赋予正确的初始化值,执行的顺序就是:父类静态域或着静态代码块,然后是子类静态域或者子类静态代码块
1.4.使用
在类的使用过程中依然存在三步:对象实例化、垃圾收集、对象终结
(1)对象实例化:就是执行类中构造函数的内容,如果该类存在父类JVM会通过显示或者隐示的方式先执行父类的构造函数,在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值,然后在根据构造函数的代码内容将真正的值赋予实例变量本身,然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法
(2)垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收
(3)对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头
1.5.类卸载 --结束生命周期
(1)执行了System.exit();
(2)程序正常执行结束
(3)程序在执行过程中遇到异常或错误而终止
(4)由于操作系统出现错误而导致Java虚拟机进程终止
2.类加载器启动类加载器: BootstrapClassLoader
,负责加载存放在 JDKjrelib
(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath
参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader
加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器: ExtensionClassLoader
,该加载器由 sun.misc.Launcher$ExtClassLoader
实现,它负责加载 JDKjrelibext
目录中,或者由 java.ext.dirs
系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器: ApplicationClassLoader
,该类加载器由 sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
JVM类加载机制
-
全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
-
父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
-
缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
类加载有三种方式:
-
1、命令行启动应用时候由JVM初始化加载
-
2、通过Class.forName()方法动态加载---默认会执行初始块
-
3、通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()区别
-
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块; -
ClassLoader.loadClass()
:只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 -
Class.forName(name,initialize,loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:
-
1、当
AppClassLoader
加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader
去完成。 -
2、当
ExtClassLoader
加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。 -
3、如果
BootStrapClassLoader
加载失败(例如在$JAVA_HOME/jre/lib
里未查找到该class),会使用ExtClassLoader
来尝试加载; -
4、若ExtClassLoader也加载失败,则会使用
AppClassLoader
来加载,如果AppClassLoader
也加载失败,则会报出异常ClassNotFoundException
双亲委派模型意义:
-
系统类防止内存中出现多份同样的字节码
-
保证Java程序安全稳定运行
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。这里有几点需要注意:
-
1、这里传递的文件名需要是类的全限定性名称,
-
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
-
3、这类Test 类本身可以被
AppClassLoader
类加载,因此我们不能把com/paddx/test/classloading/Test.class
放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader
加载,而不会通过我们自定义类加载器来加载。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构。