JVM专题1: 类和类加载机制
- 合集目录
- JVM专题1: 类和类加载机制
Java对象的结构
在HotSpot虚拟机中, 对象在内存中存储的布局可以分为3块区域
- 对象头Header
- 实例数据Instance Data
- 对齐填充Padding
对象头包含的数据有
- markword 用于存储对象自身的运行时数据, 如HashCode, GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit, 官方称它为MarkWord
- klass, klass类型指针, 即对象指向它的类元数据的指针. 虚拟机通过这个指针来确定这个对象是哪个类的实例.
- 只有数组对象有 - 数组长度. 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
32位JVM下的长度
- Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节
- 数组增加4字节数组长度, 对象头为12字节
64位JVM下的长度
- Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节
- 数组增加4字节数组长度, 对象头为20字节
64位JVM开启指针压缩下的长度
- Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节
- 数组增加4字节数组长度, 对象头为16字节
Java 类加载过程?
https://zhuanlan.zhihu.com/p/60328095
https://hesey.wang/2011/04/introduction-to-java-virtual-machine.html
类加载器子系统负责加载编译好的.class字节码文件,并装入内存,使JVM可以实例化或以其它方式使用加载后的类。JVM的类加载子系统支持在运行时的动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。
1、ClassLoader 的分类:
- 启动类加载器 BootStrap Class Loader: 负责加载rt.jar文件中所有的Java类, 即Java的核心类都是由该ClassLoader加载. 在Sun JDK中这个类加载器是由C++实现的, 并且在Java语言中无法获得它的引用.
- 扩展类加载器 Extension Class Loader: 负责加载一些扩展功能的jar包
- 应用类加载器 Application Class Loader: 负责加载启动参数中指定的Classpath中的jar包及目录, 通常我们自己写的Java类也是由该ClassLoader加载. 在Sun JDK中系统类加载器的名字叫AppClassLoader
- 用户自定义类加载器 User Defined Class Loader: 由用户自定义类的加载规则, 可以手动控制加载过程中的步骤
2、ClassLoader的工作原理
类加载分为装载、链接、初始化三步
-
Load 装载
ClassLoader通过类的全称加载类, 将指定的.class文件加载至JVM. 当类被加载以后, 在JVM内部就以类的全称+ ClassLoader实例ID
来标明类. ClassLoader实例和类的实例都位于堆中, 它们的类信息都位于方法区.
装载过程采用了一种被称为双亲委派模型(Parent Delegation Model)
的方式, 即上溯委托, 当一个ClassLoader要加载类时, 它会先请求它的上一级ClassLoader去加载, 而它的上一级ClassLoader会继续把加载请求提给再上一级的ClassLoader, 直到启动类加载器. 只有其所有上级ClassLoader无法加载指定的类时, 它才会自己去加载.
Parent Delegation Model保证了类的安全加载, 这里同时依赖了类加载器隔离的原理: 不同类加载器加载的类之间是无法直接交互的, 即使是同一个类, 被不同的ClassLoader加载, 它们也无法感知到彼此的存在. 这样即使有恶意的类冒充自己在核心包(例如java.lang)下, 由于它无法被启动类加载器加载, 也造成不了危害. 由此也可见, 如果用户自定义了类加载器, 也必须自己保障类加载过程中的安全. -
Link 链接
链接的任务是把二进制的类型信息合并到JVM运行时状态中去, 链接分为以下三步:- 验证: 校验.class文件的正确性, 确保该文件是符合规范定义的,并且适合当前JVM使用
- 准备: 为类分配内存, 同时初始化类中的静态变量赋值为默认值
- 解析(可选): 主要是把类的常量池中的符号引用解析为直接引用, 这一步可以在用到相应的引用时再解析
-
Initialize 初始化
初始化类中的静态变量, 并执行类中的static代码和构造函数. JVM规范严格定义了何时需要对类进行初始化- 通过new关键字, 反射, clone, 反序列化机制实例化对象时
- 调用类的静态方法时
- 使用类的静态字段或对其赋值时
- 通过反射调用类的方法时
- 初始化该类的子类时, 初始化子类前其父类必须已经被初始化
- JVM启动时被标记为启动类的类, 简单理解为具有main方法的类
描述一下 JVM 加载 Class 文件的原理机制?
类加载的时机
- 遇到new、getstatic、putstatic 等指令时
- 对类进行反射调用的时候
- 初始化某个类的子类的时候
- 虚拟机启动时会先加载设置的程序主类
- 使用JDK 1.7 的动态语言支持的时候
JVM 默认用于加载用户程序的ClassLoader为AppClassLoader, 不过无论是什么ClassLoader, 它的根父类都是java.lang.ClassLoader.
loadClass() 方法最终会调用到ClassLoader.definClass1()中, 这是一个 Native 方法.
其他就是上溯委托加载
什么是类加载器?
Java虚拟机把描述类的数据从Class文件加载进内存, 并对数据进行校验, 转换解析和初始化, 最终形成可以被虚拟机直接使用的Java类型, 这就是虚拟机的类加载机制.
虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流
这个动作放到Java虚拟机外部去实现, 以便让应用程序自己决定如何去获取所需要的类, 实现这动作的代码模块成为类加载器
类加载器就是根据指定全称将class文件加载到JVM内存, 转为Class对象. 如果站在JVM的角度来看, 只存在两种类加载器:
- 启动类加载器Bootstrap ClassLoader 由C++语言实现(针对HotSpot), 负责将存放在<JAVA_HOME>\lib 目录或 -Xbootclasspath 参数指定的路径中的类库加载到内存中
- 其他类加载器 由Java语言实现, 继承自抽象类ClassLoader
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。这句话可以表达的更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等
类加载器有哪些?
- 启动类加载器 BootStrap Class Loader: 负责加载rt.jar文件中所有的Java类, 即Java的核心类都是由该ClassLoader加载. 在Sun JDK中这个类加载器是由C++实现的, 并且在Java语言中无法获得它的引用.
- 扩展类加载器 Extension Class Loader: 负责加载一些扩展功能的jar包
- 应用类加载器 Application/System Class Loader: 负责加载启动参数中指定的Classpath中的jar包及目录, 通常我们自己写的Java类也是由该ClassLoader加载. 在Sun JDK中系统类加载器的名字叫AppClassLoader
- 用户自定义类加载器 User Defined Class Loader: 由用户自定义类的加载规则, 可以手动控制加载过程中的步骤
什么是类加载器双亲委派模型?
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a parent
class loader. When loading a class, a class loader first delegates
the search for the class to its parent class loader before attempting to find the class itself.
双亲委派的原文是parents delegate
. 实际上这个模型中, 只是表达“上一辈”的class loader而已, 并不是说真的有两个父辈的class loader, 翻译成上溯委托
是更合适的.
ClassLoader通过类的全称加载类, 将指定的.class文件加载至JVM. 当类被加载以后, 在JVM内部就以类的全称+ ClassLoader实例ID
来标明类. ClassLoader实例和类的实例都位于堆中, 它们的类信息都位于方法区.
装载过程采用了一种被称为双亲委派模型(Parent Delegation Model)
的方式, 即上溯委托, 当一个ClassLoader要加载类时, 它会先请求它的上一级ClassLoader去加载, 而它的上一级ClassLoader会继续把加载请求提给再上一级的ClassLoader, 直到启动类加载器. 只有其所有上级ClassLoader无法加载指定的类时, 它才会自己去加载.
Parent Delegation Model保证了类的安全加载, 这里同时依赖了类加载器隔离的原理: 不同类加载器加载的类之间是无法直接交互的, 即使是同一个类, 被不同的ClassLoader加载, 它们也无法感知到彼此的存在. 这样即使有恶意的类冒充自己在核心包(例如java.lang)下, 由于它无法被启动类加载器加载, 也造成不了危害. 由此也可见, 如果用户自定义了类加载器, 也必须自己保障类加载过程中的安全.
什么是tomcat类加载机制?
Tomcat类加载的要求
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是不合理的。
- web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,所以,web容器需要支持 jsp 修改后不用重启。
Tomcat加载器的实现
最上级3个类加载和默认的一致, 还是Bootstrap, Extension和Application, 但是新增加了四个Tomcat自己定义的类加载器: CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader, 它们分别加载/common/、/server/、/shared/ (在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/中的Java类库. 其中WebApp类加载器和Jsp类加载器通常会存在多个实例, 每一个Web应用程序对应一个WebApp类加载器, 每一个JSP文件对应一个Jsp类加载器.
tomcat 为了实现隔离性, 没有遵守上溯委托
的约定, 每个webapp ClassLoader加载自己的目录下的class文件, 不会传递给父类加载器.
-
Common ClassLoader
Tomcat最基本的类加载器, 加载路径中的class可以被Tomcat容器本身以及各个Webapp访问. CommonClassLoader能加载的类都可以被 Catalina ClassLoader和SharedClassLoader使用, 从而实现了公有类库的共用, 而Catalina ClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离. -
Catalina ClassLoader
Tomcat容器私有的类加载器, 加载路径中的class对于Webapp不可见 -
Shared ClassLoader
各个Webapp共享的类加载器, 加载路径中的class对于所有Webapp可见, 但是对于Tomcat容器不可见 -
Webapp ClassLoader
各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见. WebAppClassLoader可以使用SharedClassLoader加载到的类, 但各个WebApp ClassLoader实例之间相互隔离. -
Jasper ClassLoader
这个ClassLoader 加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件, 它出现的目的就是为了被丢弃, 当Web容器检测到JSP文件被修改时, 会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能