当我们使用java命令运行某个类的main函数启动程序时,首先需要类加载器把主类加载JVM中。
package com.sonny.classexercise.jvm;
/**
* 类加载:将用户定义的类通过类加载器加载到JVM中
*
* @author Xionghaijun
* @date 2022/9/25 20:21
*/
public class LoadUserClass {
public static final int INIT_DATA = 2;
static {
System.out.println("加载 LoadUserClass 类静态方法");
System.out.println("静态常量:" + INIT_DATA);
}
public static User user = User.builder().userId(1L).name("sonny").build();
public static void main(String[] args) {
System.out.println("类加载器测试:");
LoadUserClass loadUserClass = new LoadUserClass();
System.out.println(LoadUserClass.class.getClassLoader());
int c = loadUserClass.calculate();
System.out.println("计算结果:" + c);
System.out.println(LoadUserClass.user.toString());
System.out.println("类加载顺序测试:");
new A();
}
private int calculate() {
int a = 5;
int b = 6;
return (a + b) * INIT_DATA;
}
static class A {
static {
System.out.println("load static class A");
}
public A() {
System.out.println("load noArgsConstructor A");
new B();
}
}
static class B {
static {
System.out.println("load static class B");
}
public B() {
System.out.println("load noArgsConstructor B");
C c = null;
}
}
static class C {
static {
System.out.println("load static class C");
}
public C() {
System.out.println("load noArgsConstructor C");
}
}
}
其中loadClass的类加载过程分为:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
加载: 1.通过类型的完全限定名,产生一个代表该类型的二进制数据流(没有指明从哪里获取、怎么获取,是一个非常开放的平台),加载源包括:文件(Class文件,jar文件)、网络、计算生成(代理$Proxy)、由其它文件生成(jsp)、数据库中;2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构;3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证: 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成4个阶段的校验工作:文件格式验证、元数据验证、字节码验证、符号引用验证。若果运行的代码被反复使用和验证过,可以通过设置-Xverify:none参数关闭大部分的验证措施。
准备: 准备阶段正式为类变量分配内存并设置变量的初始值,这些变量使用的内存都应在方法区中进行分配。注:这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化是随着对象一起分配在java堆中。初始值通常是数据类型的零值;对于:public static int value=123,那么变量value在准备阶段过后的初始值为0而不是123,这时候尚未开始执行任何java方法,把value赋值为123的动作将在初始化阶段才会被执行。对于:public static final int value =123;编译时javac将会为value生成ConstantValue(常量)属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
解析: 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化: 初始化阶段是执行类构造器
类被加载到方法区中后主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用: 这个类到类加载器实例的引用 对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
tips:主类在运行过程中如果使用到其它类,会逐步加载这些类。 jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
代码中的Jvm类加载过程使用到了类加载器,分为:
类加载器示例:
package com.sonny.classexercise.jvm;
import com.sun.crypto.provider.DESKeyFactory;
/**
* 查看JDK中类加载器
*
* @author Xionghaijun
* @date 2022/9/25 20:46
*/
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
System.out.println("查看系统类加载器");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("classLoader is :" + appClassLoader);
System.out.println("classLoader is :" + extClassLoader);
System.out.println("classLoader is :" + bootstrapClassLoader);
}
}
类加载器初始化过程: 参见类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。 在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应 用类加载器)。 JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们 的应用程序。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试这个加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制的好处:
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。
package com.sonny.classexercise.jvm;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* 自定义类加载器
*
* @author Xionghaijun
* @date 2022/9/25 21:32
*/
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
public byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", File.separator);
FileInputStream fis = new FileInputStream(classPath + File.separator + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/Users/xionghaijun/TulingStudy");
Class<?> clazz = myClassLoader.loadClass("com.jvm.User1");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(o, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
//运行结果
打印方法
com.sonny.classexercise.jvm.MyClassLoaderTest$MyClassLoader
package com.sonny.classexercise.jvm;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* 自定义类加载器
*
* @author Xionghaijun
* @date 2022/9/25 21:32
*/
public class MyClassLoadeBreakrTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
public byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", File.separator);
FileInputStream fis = new FileInputStream(classPath + File.separator + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
*
* @param name
* The binary name of the class
*
* @param resolve
* If {@code true} then resolve the class
*
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// If still not found, then invoke findClass in order to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/Users/xionghaijun/TulingStudy");
Class<?> clazz = myClassLoader.loadClass("com.jvm.User1");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(o, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
//运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
Tomcat是个web容器, 那么它要解决什么问题:
Tomcat 如果使用默认的双亲委派类加载机制行不行?
不行。
一个Web应用可能需要部署过个应用程序,不同程序可能依赖同一个第三方类库的不同版本,不能要求同一个类库同个服务器中只有一份类库,要保证每个应用程序类库都是独立、隔离的;
部署在同个服务器中相同类库相同版本可共享;Web容器有自己依赖的类库,容器类库要与程序类库隔离;Web容易需支持Jsp修改,修改后不用重启服务。
Springboot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件):
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
-XX:MaxMetaspaceSize: 设置元空间最大值,默认是-1,即不限制,或只受限于本地内存大小。
-XX:MetaspaceSize: 指定触发Fullgc的初始阈值(元空间无固定初始大小),以字节为单位,默认21M,达到该值就会触发Fullgc,同时收集器会对该值进行调整:如果释放了大量空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(已设置情况下),适当提高该值。
调整元空间的大小需要FullGC,这是非常昂贵费时的操作,如果启动时发生大量FullGC,通常是由永久代或元空间发生了大小调整,基于此情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置的比初始值大,对于8G物理内存来说,一直设置成256M。
使用“ -Xss1M”设置栈空间大小默认值1M,值设置越小,一个线程里分配的栈帧就越少,但对JVM整体来说能开启的线程数会更多。
先看一个百万级订单交易系统图:
JVM参数设置:java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar microservice-eureka-server.jar
请对此场景进行调优,让其几乎不发生Full GC
参数设置:java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
结论: JVM优化尽可能让对象都在新生代里分配和回收,尽量别让太多的对象进入老年代,避免频繁对老年代进行垃圾回收,同时给系统足够的内存大小,避免新生代频繁进行垃圾回收。
1.类加载检查: 虚拟机遇到一条new指令时,先去检查这个指令参数是否在常量池中定位到一个的类的符号引用,并且检查符号引用代表的类是否已被加载、解析、初始化。如果没有,则执行类加载过程。
2.分配内存: 虚拟机在Java堆中为新生对象分配内存。
划分内存方法:
通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,使用标量替换将对象分解成若干成员变量,在栈或寄存器上分配空间,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。(难以解决对象相互循环引用)
可达性分析法: 将GC Roots对象作为起点,根据引用关系向下搜索,所走过程称为引用链,一个对象到根节点无引用关系,则表示不可达。
1.强引用:类似 Object obj = new Object();只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。
2.软引用:使用SoftReference实现,在系统将要发生内存溢出异常钱,会把这些对象列进回收范围中进行二次回收,若回收后还没足够内存,则抛出OOM。
3.弱引用:使用WeakReference实现,生存在下一次垃圾收集发生为止。
4.虚引用:使用PhantomReference实现,为了这个对象在被收集器回收时收到一个系统通知。
finalize()方法最终判断对象是否存活,即使当前对象不可达,回收该对象需要再次标记。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1). 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。
2). 第二次标记 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救 自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。 注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
需满足三个条件: