目录
class.forName()与classLoader.loadClass()区别
JVM总览:
一个进程实例对应一个JVM实例,一个JVM实例中只有一个运行时数据区,每一个线程拥有独立的一套程序计数器、本地方法栈、虚拟机栈,一个进程中的线程公用方法区和堆空间。
双亲委派机制和三种类加载器
类加载器:
每个类加载器有一个独立的命名空间,也就是说相同的一个类(源自同一个.class文件)由两个不同的类加载器加载,得到两个类我们说他们不相等。
引导类加载器:这个类的加载使用C/C++实现的,其他类加载器都可以叫做自定义类加载器,因为都是通过java代码实现的,都继承ClassLoader。其他类加载器也需要引导类加载器来加载。
Class.forName()与classLoader.loadClass()区别
Class.forName()是一个静态方法,classLoader.loadClass()
是一个实例方法
class.forName()会初始化类,classLoader.loadClass()
不会初始化类,直到类第一次使用的时候才初始化。
虚拟机栈:
内部保存一个个的栈帧(Stack Frame),对应这个一次次的java方法调用。它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
它是线程私有的。对于栈来说不存在垃圾回收问题。
栈帧中可能出现的异常:
如果采用固定大小的Java虚拟机栈,如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 StackOverFlowError异常。比如说在一个方法中声明了过多的局部变量。
如果java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常。
栈帧的内部结构:
局部变量表(Local Variables):
- 包括各类基本数据类型、对象引用(reference),以及returnAddressleixing,局部变量表,最基本的存储单元是Slot(变量槽),JVM会为局部变量表中的每一个slot都分配一个访问索引。
- 32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- Slot的复用:栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
操作数栈(Operand Stack)(或表达式栈):
根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)
比如执行一个相加操作:
- 15加载到操作数栈当中
- 15保存到局部变量表中的地址1
- 8加载到操作数栈当中的地址1
- 8保存到局部变量表中
- 加载地址1的变量的值到操作数栈中
- 加载地址2的变量的值到操作数栈中
- 执行相加操作
- 把相加操作的结果保存到局部变量表中
- 返回结果
看lby的
栈顶缓存技术:
将栈顶元素也就是最常用的操作数存到物理cpu的寄存器中,操作数栈想要入栈这个操作数的时候就直接从cpu寄存器中获取而不是内存中,效率就高很多了。
动态链接(Dynamic Linking)(或执行运行时常量池的方法引用)
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接就是为了将符号引用转换为直接引用。
早期绑定和晚期绑定(静态链接和动态链接):
早:能在编译期就能确定方法是具体调用的哪一个(非虚方法,不能被重写)
晚:由于多态机制的存在需要在运行时才能确定调用的方法(虚方法)
用lambda表达式的时候会用到5invokedynamic
虚方法表:用索引表来代替查找提高性能
栈中的变量出栈后它所指向的堆中的对象不会立刻消亡,要等待GC处理。
方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
方法正常return退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值也如调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
一些附加信息
例如,对程序调试提供支持的信息。
本地方法栈
本地方法是使用其他编程语言(如C、C++等)编写的方法,在hotspot虚拟机中本地方发展和虚拟机栈合二为一,也就是共用一个栈。
程序计数器(PC寄存器)
作用:用来指向下一条指定的地址,由执行引擎读取下一条指令。因为CPU需要不停地切换各个线程并发进行,切到某个线程之后就知道从哪开始运行了。
它是线程私有的,每一个线程都有自己的程序计数器。
堆空间:
新生代和老年代的默认比例是1:2,即NewRatio=2。
SurvivorRatio=8表示新生代中Eden区与两个survivor区的比例为8:1:1,默认比例不是这个,但默认值确实是8。
新生代80%的对象都是朝生夕死的,所以GC在新生代触发得最多,其中主要是在Eden区。
survivor区满了不会触发gc,而是Eden区满了触发gc的时候顺带处理survivor区
TLAB:在堆空间中每个线程都会分配一个私有的TLAB,创建一个对象时如果这个对象是线程私有的,那么会优先将这个对象存储到TLAB中,直到TLAB的空间不足。所以堆空间不一定都是线程公有的,所有线程的tlab加起来都只占Eden的1%。
栈上分配:没有发生逃逸的对象被分配到栈中,本质是标量替换
同步省略:如果一个对象只能被一个线程访问,那么对于这个对象可以不用考虑同步(使用同步监视器)。
标量替换:如果一个对象在一个方法中没有发生逃逸,可以将这个对象替换成标量,如new Student();可以将这个对象替换为int id=1;
String name = “gy”;可以节省堆空间就不用分配内存了,也能减少gc的调用
为什么堆空间要分eden区、s1、s2、老年区
1、G1垃圾回收器使用的是分带收集算法
2、大部分对象朝生夕死,放入新生代好回收。
2、为了内存连续规整,为了区分哪些对象可以放入老年区。新创建很多对象之后存到eden区中,因为之前是空的所以内存规整,eden区满了进行垃圾回收之后将eden区和s1中留下来对象存入s0区,清空eden区,再进来新对象又是规整的,然后又gc,s0和eden区留下来的对象又存入s1,始终保持s0、s1有一个空的。在幸存区不断s0到s1,s1到s0,多次gc后还存活的部分对象被判断为经典中的经典,于是存入老年区。
方法区:加载类信息
存储内容:类信息、域信息(成员变量)、方法信息、non-final的类变量、全局常量static final
常量池:
Java编译之后不可能把所有东西都存到字节码中,那会非常大,所以我们就用常量池来保存一些字面量和符号引用(这些引用会在类加载的链接阶段转换成直接引用直接指向内存地址)。
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息。
存储的东西:数量值、字符串值、类引用、字段引用、方法引用。
运行时常量池(方法区中):
常量池中的各种字面量和符号引用等数据在类加载之后存放到运行时常量池当中转变为直接引用(直接指向内存地址)。
HotSpot中方法区的变化
1.8以前有一个永久代实现方法区,这个永久代在堆空间中存放类信息,常量等,1.8之后永久代没有了,永久代改成元空间了。元空间不在堆空间中,使用独立的本地内存,之前永久代中存放的信息改成存放到元空间了,但是静态变量,字符串常量池还是和以前一样存在堆空间中。
对象实例化过程:
在main方法中new了一个custom对象的实际内存分配:
执行引擎:
解释器和JIT即时编译器
java半解释半编译的语言是因为执行引擎有解释器和JIT即时编译器两种行为。即时编译器更加高效
解释器响应速度快,可以解释一行cpu运行一行,即时编译器需要编译成机器码后cpu再执行
热点代码探测确定何时用JIT:
String Table字符串常量池:在堆空间中
字符串两种定义方式
String s = “hello”;直接指向字符串常量池中的数据,如果没有就在字符串常量池中创建
String s = new String(“hello”);是在堆空间中new,如果字符串常量池中没有“hello”就在字符串常量池中也创建创建一个。
所以字面量赋值创建0/1个对象,new赋值创建1/2个对象。
字符串拼接
如果拼接符号两边是字符串常量或者字符串常量引用(final修饰)则不用new,如果拼接符号的前后出现了变量,则相当于在堆空间中new了一个对象,(开发中建议使用final)
比如:
String s1=”hello”; String s2=”hi”; String s3=s1 + “hi”; String s4=”hellohi” sout(s3==s4); //false
这个s3是在堆空间中new的对象,而s4是指向常量池的,所以两者的地址值不同,故为false。
其中String s3=s1 + “hi”;的细节:
StringBuilder s3 = new StringBuilder();
s3.append(“hello”);
s3.append(“hi”);
s3.toString() ----->约等于 new String(“hellohi”)但是这个toString不会在字符串常量池中创建对象,只会在堆空间创建。
intern():
字符串.intern();判断常量池中是否存在这个值,如果存在则直接返回这个常量池的值的地址,如果不存在则在常量池中加载这个值再返回地址值。
new String(“hello”).intern(),改成这样的话变量就指向常量池了
String asd = new String("asd").intern(); System.out.println(asd == "asd"); //true
如果intern()要判断的对象在字符串常量池中没有但是在堆空间中存在,它也不会在字符串常量池中再创建一个这个值了,而是直接指向堆空间中这个值的地址
如:String s1 = new String(“a”) + new String(“b”);
(此时一共创建了6个对象分别是堆空间中的两个String对象、字符串常量池中“a”和“b”、StringBuilder对象、StringBuilder对象中new String(“ab”)对象,StringBuilder中new的String对象不会在字符串常量中再创建对象。)
s1.intern();
(检查字符串常量池中是否有“ab”-->没有—>但是堆空间中有new String(“ab”)-->所以就不在字符串常量池中创建“ab”了,而是存放一个指向堆空间中new String(“ab”)的地址)
String s2 = “ab”;
(s2到常量池中找“ab”,只找到常量池中指向堆空间new String(“ab”)的地址,所以s2是指向堆空间的)
s1 == s2;
(所以s1和s2都是指向堆空间中的new String(“ab”),故为true)
GC
对象的finalization机制
类可以重写finalize()方法,这个方法会在这个对象被JVM回收之前调用。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
在finalize方法中有可能会使这个对象复活。
永远不要主动调用finalize()方法。
GC的垃圾标记阶段:判断是不是垃圾
引用计数算法:Java未使用
计数计的是这个对象还有多少个其他对象引用他,如果这个计数值为0了就说明没有对象引用他了他就是垃圾了
可达性分析算法:
是以根对象(GC root)集合为起始点,按照从上到下的方式搜索被跟对象连接的目标是否可达。
GC root:对象引用链的根,比如虚拟机栈栈帧中引用的对象、本地方法栈引用的对象、静态属性引用的对象等。
GC清除阶段:
标记清除算法:
把非垃圾标记,把没标记的清除,清除并不是清空,而是让它变得允许被其他资源覆盖。这种方法效率一般,清理后内存不是连续的。
复制算法:
将内存分为两个区,GC时把可达对象移动到空的区,将剩下的垃圾回收。内存规整,效率高,但是内存消耗大。如果垃圾很少那就是白白吧可达的对象复制一遍了,所以它适合垃圾多的情况,而新生代就是朝生夕死的并且survivor区满足复制的特性,所以survivor区使用复制算法是非常合适的。
标记压缩算法:
标记可达对象,将他们在内存中排列整齐,再把垃圾清除。效率低,内存规整
G1的分代收集、增量收集、分区收集
分代收集:
因为对象的生命周期不同,有的很长有的很短,就区分了新生代和老年代,根据不同的声明周期使用不同的垃圾回收算法以提高效率。
增量收集:
垃圾回收线程和和应用程序线程交替进行,防止stw过长的时间影响用户的体验。但是线程之间的上下文切换会使整体的效率下降。
分区收集:
将内存分为一个个的region,每个region属于eden区、survivor区、老年区中的一个(除此三个之外还有用于存储大对象的Humongous),每次收集控制回收多少个region来减少stw的时间,G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
内存泄漏:
对象不会再使用了,但是因为被持有,所以GC回收不了。
内存泄漏例子
修改hashSet中一个元素的属性值,这样会导致元素的哈希值改变,由于hashSet删除对象是根据hash值删除的所以会导致删除不了,由此就引发了内存泄漏。
容器中add一个对象然后delete掉,这个对象已经不用了但是还是被容器给持有gc不掉。
一个只在某个方法中使用的对象在类中声明成了成员变量,在类的对象被释放时这个对象才会被释放。
STW:
stop the world 为了保持数据一致,jvm要把程序停下来进行gc
并发与并行:
并发:一个cpu快速切换任务
并行:多个cpu同时执行多个程序
各种引用:
强引用:死不回收
软引用:内存不足回收
弱引用:发现即回收
虚应用:对象回收跟踪
评估GC性能的指标:
各种垃圾回收器:
serial:串行
parnew:并行
parallel scavenge:可控制吞吐量,吞吐量优先(适合服务器端,交互少)
parallel old:用标记整理算法
cms:低延迟,低暂停时间(适合交互多)
G1:
延迟可控的情况下获得尽可能高的吞吐量(全能GC)
优势:
兼具并行与并发
分代收集:能同时兼顾老年代和年轻代,将堆空间分成若干份(region),每一个region包含了年轻代、老年代等。
空间整合:region之间是复制算法,整体可看成标记压缩算法,可避免内存碎片
可预测的停顿时间模型:回收以region为单位,每次根据允许的收集时间优先回收价值大的region,回收效率高。
缺点:
内存占用高(要存放Remembered Set),在小内存上的表现不如cms,大内存应用上G1能发挥,平衡点
6-8GB。
G1垃圾回收器回收过程:
类的加载过程:
在什么时候加载
如果一个类有static final
修饰的常量或者有静态代码块,那么这个类在编译的时候就会加载,直接将常量的值写入字节码并且执行静态代码块。
其他情况无论是使用new关键字还是反射机制创建对象,都是在程序运行的时候需要用到这个类的时候再加载类,只是使用反射机制编译时不会判断这个类存不存在,不存在的话报错也是在运行执行到那一步的时候报错。
编译时只是将new关键字的使用编译成字节码指令,所以如果new的对象的类不存在会编译失败,并不是说new的类是在编译时加载的,还是在程序运行的时候加载的。
五个过程
一、加载
1、加载阶段通过类的全类名获取到类的二进制字节流。
2、将这个流的静态存储结构转换为方法区的运行时数据
3、在内存中生成一个代表这个类的.Class对象,作为方法区访问这个类各种数据的入口。
二、链接
1、验证:确保字节流中包含的信息不回危害虚拟机,
主要包括四种验证:文件格式验证、源数据验证、字节码验证、符号引用验证。
2、准备:为类变量分配初始值,不包括final修饰的变量,因为他在编译的时候就赋好值了。
3、解析:将符号引用转换成直接引用的过程。
符号引用是用一些符号来表是被引用的目标,直接引用则是直接指向目标的指针
三、初始化
1、执行构造器方法clinit的过程
2、此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。如果没有类变量或者静态代码块就不会有clinit方法
3、若该类具有父类,jvm会保证子类的clinit()执行前,父类的clinit()已经执行完毕
4、要保证clinit()的线程安全性。
赋值在初始化阶段还是准备环节
1、链接阶段的准备阶段给静态变量默认赋值,给static final修饰的基本数据类型或用字面量赋值的String(String = “hello”这种)赋值。
2、初始化阶段编译器帮你生成clinit方法负责静态变量显式赋值并执行静态代码块,如果不需要,那编译器就不会帮你生成clinit方法。
总结:需要执行代码的赋值就在初始化阶段,因为链接阶段不能执行代码。构造方法也算执行代码,所以new()了的统统在初始化阶段赋值。
clinit():
只有在主动使用这个类的时候才会调用这个方法
被动使用:
1、通过子类调父类中的静态变量,子类不会初始化,只有真正声明这个变量的类才会初始化
2、定义一个类引用的数组时不会初始化,只是开辟了数组内存空间。
3、调用一个类的静态常量不会加载这个类,因为静态常量在链接过程的准备环节就已经赋好值了。
4、使用ClassLoader加载一个类不会初始化这个类。(但是forName()会)
初始化类的时候要先初始化它继承的父类,但是不会初始化它实现的接口,初始化接口时也不会初始化它的父接口。
如果一个接口中的一个default方法中new一个对象,那么这个对象对应的类在实例化的时候也会实例化这个接口。
类的卸载:
基本不能回收,自定义类加载器加载的类有可能可以回收
性能优化
步骤:性能监控--性能分析--性能调优
性能评价/测试指标:
深堆与浅堆
浅堆就是这个对象自己的大小,深堆是这个对象本身的大小加上只被它引用的对象的大小