目录
一、类加载器
jvm类的加载过程
第一阶段:加载
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
第二阶段:链接阶段
验证 :文件格式 元数据验证 字节码验证 符号引用验证
准备:为类变量分配内存 设置默认值
解析: 符号引用转换为直接引用
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
第三阶段:初始化阶段:
为静态变量赋予正确的初始值
执行类构造器 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。 从上面可以看出初始化后,只能够执行一次初始化,这也就是同步加锁的过程
双亲委派机制
方法 getParent()
loadClass() 双委派机制
findClass() 编写加载规则 将取得的字节码转换成流
defineClass() 生成类的class对象
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//类加载器有父类加载器,使用父类加载器加载
c = parent.loadClass(name, false);
} else {
//直到顶级类加载器,即parent为空时,由findBootstrapClassOrNull()方法尝试到Bootstrap ClassLoader中检查目标类。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍未找到,则调用findClass来查找该类。
long t1 = System.nanoTime();
//通过findClass()方法尝试到对应的类目录下去加载目标类。
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//如果加载成功,则根据resolve参数决定是否要执行连接过程,并返回Class对象。
resolveClass(c);
}
return c;
}
}
Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?(SPI:在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoaden),这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。在JDK 6时,DK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
Java虚拟机对class文件采用的是按需加载的方式
-
避免类的重复加载
-
保护程序安全,防止核心API被随意篡改
沙箱安全机制
沙箱机制就是将Java代码限定在虚拟机(VN)特定的运行范围中,并且严格限刺代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。
运行时数据区
栈-Xss1m
-
局部变量表(Local Variables)
-
操作数栈(operand Stack)(或表达式栈)
-
动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
-
方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
堆
堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的对象实例以及数组都应当在运行时分配在堆上。
-
“-Xms"用于表示堆区的起始内存,等价于-xx:InitialHeapSize
-
“-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
-
Eden:From:to -> 8:1:1
-
新生代:老年代 - > 1 : 2
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。(有些大的对象在Eden区无法存储时候,将直接进入老年代)
YGC次数达到15次还存活的对象,会移动到老年代。次数可以设置。默认是15次。
-
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
-
关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
-
新生代采用复制算法的目的:是为了减少内碎片
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
-
新生代收集(MinorGC/YoungGC):只是新生代(Eden\s0,s1)的垃圾收集
-
老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
TLAB
VM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
逃逸分析
随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
-
栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配
-
同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
-
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
就会把这个对象拆解成若干个其中包含的若干个成员变量来代替.这个过程就是标量替换。
方法区
“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩
方法区主要存放的是 Class,而堆中主要存放的是 实例化的对象
-
加载大量的第三方的jar包
-
Tomcat部署的工程过多(30~50个)
-
大量动态的生成反射类
常量池中有什么
-
数量值
-
字符串值
-
类引用
-
字段引用
-
方法引用
运行时常量池(Runtime Constant Pool)是方法区的一部分。
常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
StringTable为什么要调整位置
stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
静态变量存放在那里?
静态引用对应的对象实体始终都存在堆空间
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
只要常量池中的常量没有被任何地方引用,就可以被回收。
对象实例化内存布局与访问定位
对象的实例化
对象头包含了两部分,分别是 运行时元数据(Mark Word)和 类型指针
运行时元数据
-
哈希值(HashCode)
-
GC分代年龄
-
锁状态标志
-
线程持有的锁
-
偏向线程ID
-
偏向时间戳
类型指针
指向类元数据InstanceKlass,确定该对象所属的类型。指向的其实是方法区中存放的类元信息
实例数据(Instance Data)
对齐填充说明
不是必须的,也没有特别的含义,仅仅起到占位符的作用
StringTable
注意
字符串常量池是不会存储相同内容的字符串的
String的string Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。
使用-XX:StringTablesize可设置stringTab1e的长度
字符串拼接操作
-
常量与常量的拼接结果在常量池,原理是编译期优化
-
常量池中不会存在相同内容的变量
-
只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
-
如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
从上述的结果我们可以知道:
如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果
而调用intern方法,则会判断字符串常量池中是否存在JavaEEhadoop值,如果存在则返回常量池中的值,否者就在常量池中创建
s1 + s2的执行细节
-
StringBuilder s = new StringBuilder();
-
s.append("a");
-
s.append("b");
-
s.toString(); -> 类似于new String("ab");