一、内存分配与回收策略(对象如何进入老年代?)
大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
Minor GC:发生在新生代的垃圾收集,非常频繁,回收速度快;
Major GC:发生在老年代的GC;
Full GC:清理整个空间包括新生代和老年代;
下面两种一般很难区分,许多Major GC是由Minor GC触发的,Major GC比Minor GC慢十倍以上。
1. 什么时候对象进入老年代?
(1)大对象
需要大量连续内存的大对象直接进入老年代;
(2)空间分配担保
新生成的对象放在Eden,当Eden被填满后,垃圾回收后存活的对象复制放入From(其中一个Survivor),当From满了,回收后存活的对象被复制到To区域,Eden存活的也直接进入To区域,原From区域被清空,当To被填满后,如果之前存活的对象还活着,直接进入老年代;
(3)年龄判定
年龄计数器会为对象记录年龄,每经过一次GC仍然存活的,年龄加1,当超过设定值时,直接进入老年代。或者动态对象年龄判定,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求年龄。
2. 空间分配担保
安全的Minor GC:
老年代中最大可用的连续空间大于新生代所有对象的空间;
冒险的Minor GC:
老年代中最大可用的连续空间大于历代晋升到老年代的平均水平且允许担保失败;如果小于平均值,则直接进行Full GC,让老年代腾出空间。
二、Class类文件结构
参考资料
Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础结构之一。
包括:魔数和版本号、常量池(注意和运行时常量池的区别)、访问标志(类定义标志)、类索引、父类索引和接口索引的集合(确定一个类的继承关系)、字段表集合、方法表集合、属性表。
数据类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u4 | magic | 1 | 魔数 |
u2 | minor_version | 1 | 次版本 |
u2 | major_version | 1 | 主版本 |
u2 | constant_pool_count | 1 | 常量池入口 |
cp_info | constant_pool | constant_pool_count-1 | 常量池 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interfaces_count | 1 | 继承接口数 |
u2 | interfaces | interfaces_count | 接口索引 |
u2 | fields_count | 1 | 字段数 |
field_info | fields | fields_count | 字段 |
u2 | methods_count | 1 | 方法数 |
method_info | methods | methods_count | 方法 |
u2 | attributes_count | 1 | 属性数 |
attribute_info | attributes | attributes_count | 属性 |
字段表包括什么信息?(就是我们平时定义一个字段的格式)
字段的作用域(public private protected修饰符),是实例变量还是类变量(static修饰符),可变性(final),并发可见性(volatile修饰符、是否强制从主内存读写),可否被序列化(transient),字段数据类型、字段名称。
三、JVM中类加载机制
参考资料
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化。
类的加载过程:加载、验证、准备、解析、初始化。
1. 加载
通过类型的完全限定名,产生一个代表该类型的二进制数据流;
解析这个二进制数据流的静态存储结构转化为方法区的运行时数据结构;
创建一个表示该类型的java.lang.Class类的实例,作为方法区这个类的各种数据的访问入口;
这个过程主要是类加载器完成。
2. 验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;
主要是完成四个阶段的验证:
(1)文件格式的验证:基于字节流验证。
验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验。
(2)元数据验证:基于方法区的存储结构验证。
主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
(3)字节码验证:基于方法区的存储结构验证。
这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
(4)符号引用验证:基于方法区的存储结构验证。
它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
3. 准备
正式为类变量(static修饰)基于方法区的存储结构验证。并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到Java堆中;
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwA9wBw1-1598434521210)(https://pics2.baidu.com/feed/962bd40735fae6cdf616ed21e7a1f42043a70fe3.png?token=531148998856e5d92d0b437d9f9131b2&s=1AAA7423131A4DC8585DB1CB0300C0B1)]
对于:
public static int value = 123;
那么变量value在准备阶段过后的初始值是0而不是123,这时候上未来是执行任何Java方法,把value赋值为123的动作将在初始化阶段才会被执行。
注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是123了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。
4. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用);符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。如果有了直接引用,那引用的目标必定已经在内存中存在。
主要有以下四种:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
主要是静态链接,方法主要是静态方法和私有方法。
5. 初始化
开始真正执行定义的Java代码,这个阶段就是执行类构造器<clinit>()方法的过程。
<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。
四、JVM中的类加载器及双亲委派模型
classloader 结构,是否可以自己定义一个 java.lang.String 类,为什么? 双亲代理机制。
1. 启动类加载器——Bootstrap ClassLoader
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
2. 扩展类加载器——Extension ClassLoader
扩展类加载器是指Sun公司实现sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3. 应用程序类加载器——Application ClassLoader
这个类加载器由sun.misc.Launcher$AppClassLoader实现,由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也成为系统类加载器。它负责加载用户类录用(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,要求除了顶层的启动类加载器外,其余的类加载器都应该由自己的父类加载器,这里父子关系通常是子类通过组合关系而不是继承关系来服用父加载器的代码。
为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型。
4. 双亲委派模型
如果一个类加载器收到了类加载的请求,先把这个请求委派给父类加载器去完成(所以所有的加载请求最终都应该传送到顶层的启动类加载器中),只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。
原因是:如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,那系统中将出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序将会一片混乱。
如果自己写一个java.lang.String会被加载吗?
答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。
但如果自定义一个String类但是包名不是java.*,则可以放在用户目录下进行加载。
5. 总结
- 启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载/lib下的类。
- 扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载/lib/ext下的类。
- 系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
双亲委派模型的作用
1、防止重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
五、JVM中静态分派与动态分派
动态分派和静态分派机制是Java多态实现的原理。
1. 静态分派
以来静态类型定位方法的分派,发生在编译时期,典型应用为方法的重载。重载的参数是通过静态类型确定的,直接调用父类。
void test() {
Father father = new Son(); //静态分派
print(father);
}
void print(Father father) {
System.out.println("this is father");
}
void print(Son son) {
System.out.println("this is son");
}
这段代码执行完成以后会输出this is father。之所以会输出这样一个结果,原因就是此处的多态实现是静态分派的。在编译阶段,由于father变量类型被声明为Father。因此在编译阶段就已经确定了调用的是参数为Father的方法,与具体的实例化对象无关。
2. 动态分派
在运行时期根据实际类型来确定方法的分派,发生在程序运行时期,典型应用是方法的重写,也是多态的一种体现。根据转型来确定是否调用父类还是子类的方法。
void test() {
Father father = new Son(); //静态分派
father.name(); //动态分派
}
class Son extends Father {
void name(){
System.out.println("son");
}
}
class Father {
void name(){
System.out.println("father");
}
}
这里我们声明了静态类型Father,但是实际上我们调用name方法的时候,输出的却是son。这里就牵扯到一个动态分派的问题,对于方法重写,Java采用的是动态分派机制,也就是说在运行的时候才确定调用哪个方法。由于father的实际类型是Son,因此调用的就是Son的name方法。
在Java多态的两种常见用法中,方法重载使用的是静态分派机制,而方法重写使用的是静态分派机制。这也就导致了,方法重载调用的时候是根据变量的静态类型来决定调用哪个方法。而方法重写的时候,则是根据变量的实际类型来决定调用哪个方法。
3. 虚方法和非虚方法
(1)非虚方法
通过invokespecial或invokestatic指令调用,对这个非虚方法的符号引用将转为对应的直接引用,即转为直接引用方法,在编译完成时就确定唯一的调用方法;常见的非虚方法有:静态方法、私有方法、实例构造器、父类方法四种。
(2)虚方法
除非虚方法以外的其它方法。虚方法是通过invokevirtual指令调用,且会有静态或者动态分派,具体先根据编译期时方法接收者和方法参数的静态类型来分派,再在运行期根据方法接收者的实际类型来分派。
注意:被final修饰的方法,虽然是invokevirtual指令调用,但依然为非虚方法。