类加载器子系统负责从文件系统或网络中加载 class 文件。
类加载器ClassLoader只负责 class 文件的加载,至于是否可以运行则由执行引擎Execution Engine决定。
加载的类信息存放于一块称为方法区(元空间)的内存空间。
验证文件格式是否一致:
class文件在开头有特定的文件标识(字节码文件都以CA FE BA BE 标识开头
)
主/次版本号是否在当前Java虚拟机接收范围内
元数据验证:
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
例如:该类是否有父类,是否final修饰的类
准备
准备阶段负责为类的静态属性分配内存,并设置默认初始值。
不包含用final修饰的static常量,常量在编译时进行初始化
public static int value = 100;
value 在准备阶段后的初始值是 0,而不是 100
解析
将类的二进制数据中的符号引用替换成直接引用
符号引用是Class文件的逻辑符号,直接引用指向的方法区中的某一个地址
new User()
) or 反射(Class.forName("com.mysql.jdbc.Driver")
)or 序列化(Serialize)注意:访问该类中的常量并不会初始化该类
package com.ffyc.forword.jvm.classloader;
public class ClassInitOrder {
static String s1 = "父类静态变量";
String s2 = "父类变量";
static {
System.out.println(s1);
System.out.println("父类静态代码块");
}
{
System.out.println(s2);
System.out.println("父类代码块");
}
public ClassInitOrder(){
System.out.println("父类构造方法");
}
}
子类
package com.ffyc.forword.jvm.classloader;
public class ClassInitOrderChild extends ClassInitOrder{
static String s1 = "子类静态变量";
String s2 = "子类变量";
static {
System.out.println(s1);
System.out.println("子类静态代码块");
}
{
System.out.println(s2);
System.out.println("子类代码块");
}
public ClassInitOrderChild(){
System.out.println("子类构造方法");
}
}
创建子类的实例
public static void main(String[] args) {
new ClassInitOrderChild();
}
对于静态代码块和静态变量,按照顺序自上而下执行。(如下代码)
static int num = 10;
static {
num = 20;
}
public static void main(String[] args) {
/*num:
准备阶段:默认初始化为0
初始化:对静态变量num赋值为10
赋值为20
*/
System.out.println(num);//20
}
static {
num = 20;
}
static int num = 10;
public static void main(String[] args) {
/*num:
准备阶段:默认初始化为0
初始化:对静态变量num赋值为20
赋值为10
*/
System.out.println(num);//10
}
按JVM的角度可分为:
不是Java语言实现的
java.lang.ClassLoader
.按开发人员的角度可分为三层:
不是继承于 java.lang.ClassLoader
没有父加载器(不是Java语言写的,自然也不继承于Java类)
\lib
目录 或者被-Xbootclasspath
参数所指定的路径中存储的类(出于安全考虑)。Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。继承自ClassLoader类。
从 java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载。
Java语言编写,由 sun.misc.Launcher$AppClassLoader 实现,继承自ClassLoader类。
加载我们自定义的类,用于加载用户类路径(classpath)上所有的类。
应用程序类加载器是程序中默认的类加载器。
public static void main(String[] args) {
ClassLoader loader = ClassLoaderDemo.class.getClassLoader();
System.out.println(loader);
//sun.misc.Launcher$AppClassLoader 自定义类 由 应用程序类加载器 加载
System.out.println(loader.getParent());
//sun.misc.Launcher$ExtClassLoader 扩展类加载器
System.out.println(loader.getParent().getParent());
//null 扩展类加载器由引导类加载器 加载 为null是由启动类加载器加载的-->引导类加载器不是Java编写的
System.out.println(String.class.getClassLoader());//null
//java系统类库的类的加载器是启动类加载器
}
除启动类加载器外,其余类加载器都是继承自ClassLoader抽象类.
Java虚拟机对Class文件采用的是按需加载的方式,即当需要该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
例如java.lang.String
findLoadClass()
)判断当前类是否已经加载。我们来测试自定义一个java.lang.String
测试
在自定义的java.lang包下测试
错误:发生JNI错误,请检查安装并重试 线程“main”java中出现异常。lang.SecurityException:禁止的包名:java.lang
在其他包下测试
没有执行自定义String类的构造方法,依然创建的是Java核心类库中的String。
双亲委派机制保护了Java核心类不被用户自定义类所替换
JVM中规定,每个类or接口被首次主动使用时才对其进行初始化,有主动使用,也就有被动使用。
主动使用与被动使用的区别在于类是否会被初始化
public final static int num = 100; //不会导致类初始化,被动使用
public final static int random = new Random().nextInt(); //会导致类的初始化,主动使用
Demo[] demos = new Demo[10];
)JVM的运行时数据区,不同的虚拟机实现可能有所不同,但是都会遵从Java虚拟机的规范,Java8 虚拟机规范规定,Java虚拟机所管理的内存将会包含以下几个运行时数据区域:
存储局部变量表,操作数栈,动态链接,方法出口
等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。类信息、常量、静态变量、即时编译后的代码
等数据。概述
JVM中的程序计数寄存器(Program Counter Register)中的Register命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。
JVM中的程序计数器不是CPU中的寄存器,只是对物理PC寄存器的一种抽象模拟。
作用
程序计数器用来存储下一条指令的地址,也就是将要执行的指令代码。由执行引擎读取下一条指令。
分支、循环、跳转、异常处理、线程恢复
等基础功能都需要依赖这个计数器来完成。OutOfMemoryError
情况的区域。Java API源码中的native方法
)StackOverflowError
。由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更多的指令集。
Java虚拟机栈(Java Virtual Machine Stack),每个线程在创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次方法的调用。
Java虚拟机栈是线程私有的,生命周期随线程启动而产生,线程结束而消亡。
栈是运行时的单位,而堆是存储的单位。
主管Java程序的运行(方法),保存了方法的局部变量(基本数据类型,对象的引用地址),部分结果,并参于方法的调用和返回。
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM直接对Java栈的操作有两个:
因此对于栈来说不存在垃圾回收的问题。
StackOverflowError:
每个线程都有自己的栈,栈中的数据都以栈帧为单位存储。
在该线程上正在执行的每个方法各自对应着一个栈帧。
栈帧存储着方法执行过程中需要的各种数据信息。
JVM对Java栈的操作:进栈和出栈(先进后出FIFO)
在一条活动的线程中,一个时间点上,只会有一个活动栈。也就是只有当前在执行的方法的栈帧(栈顶)是有效的,这个栈帧叫做当前栈(Current Frame),与当前栈对应的方法叫当前方法(Current Method),定义这个方法的类称为当前类(Current Class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用其他方法,其他方法对应的新栈帧就会被创建出来放在栈顶,成为新的当前栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使前一个栈帧重新称为当前栈帧
不同线程所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)
Java方法有两种返回的方式,一种是return,另一种是抛出异常,两种方式都会导致栈帧被弹出。
每个栈帧存储着:
也叫 表达式栈
指向运行时常量池的方法引用
操作数栈
可以说程序中的所有计算过程都是借助于操作数栈来完成的
动态链接
方法返回地址
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须要保存一个方法返回地址。
Java8以及之后的堆内存分为:
新生代又分为:
将对象根据存活概率分类,堆存活时间长的对象,放到固定区,从而减少扫描垃圾时间和GC的频率。针对不同的分区使用不同的垃圾回收算法,提高垃圾回收效率。
为新对象分配内存,要考虑内存如何分配,在哪分配,由于内存分配算法于内存回收算法密切相关,还需考虑GC执行完内存回收后,是否会在内存空间中产生内存碎片。
new 的新对象先放到伊甸园区(该区大小有限制)
当伊甸园区满时,又有新的对象要创建,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被引用的对象进行销毁,再加载新的对象放到伊甸园区。
将伊甸园区中的剩余对象移动到幸存者0区。
如果再次发生垃圾回收,如果幸存者0区的对象没有被回收,就会被放到幸存者1区。清空幸存者0区,依次交替执行。
每次会保证有一个幸存者区是空的,内存是完整的。
何时移动到老年区?:经历15次垃圾回收依然存活的对象
默认15,最大值15,参数可以设置
-XX:MaxTenuringThreshold=
在对象头中,由4位数据保存GC的年龄,最大值为1111(转为10进制也就是15),因此在对象的GC年龄到达15时,就会从新生区转到老年区
老年区中,内存不足时触发Major GC,若垃圾回收之后依然无法保存新的对象,就会发生Java.lang.OutOfMemoryError:Java heap space
异常
进入安装目录:Java\jdk1.8.0_261\bin
中,打开jvisualvm.exe
执行文件
可以观察到幸存者区s0和s1之间交替存储.
配置新生区与老年区在堆结构的占比
默认-XX:NewRatio=2
表示新生区占1,老年区占2 新生区占堆的1/3
修改-XX:NewRatio=4
表示新生区占1,老年区占4 新生区占堆的1/5
如果在整个项目中,生命周期长的对象偏多,可以通过调整老年区的大小来进行调优
在HotSpot中, 默认-XX:SurvivorRatio=8
,Eden:S0:S1=8:1:1
可以通过参数-XX:SurvivorRatio
调整Eden区空间占比
-XX:SurvivorRatio=4
,Eden:S0:S1=4:1:1
HotSpot VM的GC按照回收区域分为两大类型:
. 部分收集
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集
老年区收集(Major GC / Old GC):只是老年区的垃圾收集
整堆收集(Full GC) 整个Java堆和方法区的垃圾收集
System.gc()时
开发期间尽量避免整堆收集,因为再垃圾回收时,会STW(stop the world )回收时会停止其他线程运行。
TLAB(Thread Local Allocation Buffer):线程本地分配缓存区,是一个线程独享的内存分配区域。
TLAB的产生背景:
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
对象在JVM中创建非常频繁,因此在并发环境下从堆区分配空间是线程不安全的
为了避免多个线程操作同一个地址,需要使用加锁等机制,影响分配速度。
TLAB大小默认占整个Eden区的1%
可以通过-XX:TLABWasteTargetPercent
来设置TLAB空间所占Eden空间的百分比大小.
在多线程情况下,可以通过在堆空间中设置参数-XX:UseTLAB
,在堆空间中为线程开辟一块空间只给当前线程使用,用来存储当前线程中产生的对象,每个线程使用自己的TLAB,避免线程同步,空间竞争,提高对象的分配效率。
JDK7之前,字符串常量池位置在方法区(永久代)中保存
JDK7以后,将字符串常量池的位置放到了堆空间,因为方法区只有触发Full GC 的时候才会执行永久代的垃圾回收,回收效率低,老年区空间 or 方法区不足触发Full GC
在开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放到堆中能够及时回收内存.
官网: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
常用参数:
-XX:+PrintFlagsInitial
查看所有参数的默认初始值-XX:+PrintFlagsFinal
查看所有参数的最终值(修改后的值)-Xms
:初始堆空间内存(默认为物理内存的 1/64)-Xmx
:最大堆空间内存(默认为物理内存的 1/4)-Xmn
:设置新生代的大小(初始值及最大值)-XX:NewRatio
:配置新生代与老年代在堆结构的占比-XX:SurvivorRatio
:设置新生代中 Eden 和 S0/S1 空间比例-XX:MaxTenuringTreshold
:设置新生代垃圾的最大年龄-XX:+PrintGCDetails
输出详细的 GC 处理日志方法区是一个线程共享的区域
主要存储(类的信息):
方法区包含了一个"运行时常量池"
Java虚拟机规范中说明:尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开
方法区可以看作是一块独立于Java堆的内存空间.
方法区在JVM启动时被创建,并且它和堆区的实际的物理内存空间都可以是不连续的.
方法区的大小与堆一样都可以选择默认或指定
若加载的类太多,导致方法区溢出,虚拟机也会抛出OOM 内存溢出
的错误
方法区、堆、栈的交互关系
方法区的大小可以动态调整
-XX:MetaspaceSize
和 -XX:MaxMataspaceSize
指定元数据区大小,替代上述原有的两个参数为了减少Full GC的触发,可以给-XXMetaspaceSize设置一个较高的值
方法区存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码缓存,运行时常量池等
通过反编译字节码文件查看.
反编译字节码文件,并输出值文本文件中,便于查看.参数-p
确保能查看private修饰的属性/方法.
鼠标右击.class文件,选择 Open in Terminal
在终端打开
javap -v -p homework1.class > test.txt
主要回收:
判断一个类型是否"不再使用"需要满足3个条件:
java.lang.Class对象
没有被引用,无法通过反射访问该类的方法