1.main执行过程
- 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
- JVM 找到 App 的主程序入口,执行 main 方法
- 这个 main 中的第一条语句为 Student student = new Student(“tellUrDream”) ,就是让 JVM 创建一个 Student 对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中
- 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 指向方法区中的 Student 类的类型信息 的引用
- 执行 student.sayName();时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
- 执行 sayName()
其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找
2.类加载的流程
1.类加载器加载后
经过: 加载->验证->准备->解析->初始化->使用->卸载
加载:
加载class文件到内存中
将静态数据结构转化为方法去运行时数据结构
在堆中生成一个代表这个类的class对象作为数据访问的接口
验证:验证字节码文件是否符合规范,确保字节码文件不会对虚拟机造成伤害
准备:给静态变量在方法区分配内存并给静态对象赋零值,实例变量的赋值实在实例化阶段.
解析:将符号地址转化为引用地址.
初始化:初始化阶段实际上是执行类构造器方法方法的过程,并且要保证执行前父类方法的执行完毕.这个方法是按顺序执行所有类变量的赋值,由默认初始化0变成显示的初始化值, 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。
卸载: GC 将无用对象从内存中卸载
3.类加载机制:
类加载机制的核心:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它
-
4.双亲委派
1、什么是双亲委派?
双亲委派就是当一个类需要被加载的时候,会发送一个请求给类加载器,类加载器会将这个请求上升给父级,在父级中查找是否有该类,是否加载了该类,如果没有该类,就上升给核心类库,如果核心类库也没有该类,则抛出异常,让系统类加载器自己加载。
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
2、为什么需要双亲委派,不委派有什么问题?
双亲委派机制是jvm的一种保护机制,可以避免jdk代码被破坏,也可以避免类重复加载
通过委派的方式,可以避免类的重复加载,
当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
通过双亲委派的方式,还保证了安全性。
可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
3、"父加载器"和"子加载器"之间的关系是继承的吗?
不是,是一种componient组合关系
父子类加载器之间是组合关系,子类类加载器会含有一个parentClassLoader的对象,类加载的时候通常会按照树形结构的原则来进行,也就是说,首先是从parentClassLoader中尝试进行加载,当parent无法进行加载时,再从当前的类加载器进行加载
4、双亲委派是怎么实现的?
当需要加载一个类的.class文件时,会通过系统类加载器调用父类的loadClass()方法,当extClassLoader中也没有时,会调用引导类加载器中的loadClass()方法,如果都没有,则抛出异常,让系统类加载器自己加载.
从源码来看,底层是先判断该类是否加载,如果没有加载就从父类加载,默认启动类加载器为类加载器,当启动类加载器没有时,抛出异常,再使用自己的findClass()进行加载
5、我能不能主动破坏这种双亲委派机制?怎么破坏?
6、为什么重写loadClass方法可以破坏双亲委派,这个方法和findClass()、defineClass()区别是什么?
7、说一说你知道的双亲委派被破坏的例子吧
8、为什么JNDI、JDBC等需要破坏双亲委派?
9、为什么TOMCAT要破坏双亲委派?
10、谈谈你对模块化技术的理解吧!
以上,10个问题,从头开始答,你大概可以坚持到第几题?
双亲委派是怎么实现的?
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
代码不难理解,主要就是以下几个步骤:
1、先检查类是否已经被加载过
2、若没有加载则调用父加载器的loadClass()方法进行加载
3、若父加载器为空则默认使用启动类加载器作为父加载器。
4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
什么是双亲委派机制
首先,我们知道,虚拟机在加载类的过程中需要使用类加载器进行加载,而在Java中,类加载器有很多,那么当JVM想要加载一个.class文件的时候,到底应该由哪个类加载器加载呢?
这就不得不提到"双亲委派机制"。
首先,我们需要知道的是,Java语言系统中支持以下4种类加载器:
- Bootstrap ClassLoader 启动类加载器
- Extention ClassLoader 标准扩展类加载器
- Application ClassLoader 应用类加载器
- User ClassLoader 用户自定义类加载器
这四种类加载器之间,是存在着一种层次关系的,如下
一般认为上一层加载器是下一层加载器的父加载器,那么,除了BootstrapClassLoader之外,所有的加载器都是有父加载器的。
那么,所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
那么,什么情况下父加载器会无法加载某一个类呢?
其实,Java中提供的这四种类型的加载器,是有各自的职责的:
- Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
那么也就是说,一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。
为什么需要双亲委派,不委派有什么问题??
如上面我们提到的,因为类加载器之间有严格的层次关系,那么也就使得Java类也随之具备了层次关系。
或者说这种层次关系是优先级。
比如一个定义在java.lang包下的类,因为它被存放在rt.jar之中,所以在被加载过程汇总,会被一直委托到Bootstrap ClassLoader,最终由Bootstrap ClassLoader所加载。
而一个用户自定义的com.hollis.ClassHollis类,他也会被一直委托到Bootstrap ClassLoader,但是因为Bootstrap ClassLoader不负责加载该类,那么会在由Extention ClassLoader尝试加载,而Extention ClassLoader也不负责这个类的加载,最终才会被Application ClassLoader加载。
这种机制有几个好处。
首先,通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
另外,通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。
那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
"父子加载器"之间的关系是继承吗?
很多人看到父加载器、子加载器这样的名字,就会认为Java中的类加载器之间存在着继承关系。
甚至网上很多文章也会有类似的错误观点。
这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
如下为ClassLoader中父加载器的定义:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现并不复杂。
如何主动破坏双亲委派机制?
知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。
因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
loadClass()、findClass()、defineClass()区别
ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?
- loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
- findClass() 根据名称或位置加载.class字节码
- definclass() 把字节码转化为Class
这里面需要展开讲一下loadClass和findClass,我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写loadClass方法。
那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?
这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。
/**
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这个方法只抛出了一个异常,没有默认实现。
JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。
因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载。
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。
双亲委派被破坏的例子
双亲委派机制的破坏不是什么稀奇的事情,很多框架、容器等都会破坏这种机制来实现某些功能。
第一种被破坏的情况是在双亲委派出现之前。
由于双亲委派模型是在JDK1.2之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。
第二种,是JNDI、JDBC等需要加载SPI接口实现类的情况。
第三种是为了实现热插拔热部署工具。
某个用户自定义的应用程序发生变化的时候,我们首先销毁原来的应用,然后使用一个新的ClassLoader来加载改变之后的应用。而所有其他的应用程序不会受到一点干扰。****。
实现热部署
1、定义一个用户自定义应用程序的接口,这是因为,我们需要在容器应用中去加载用户自定义的应用程序。
2、我们还需要一个配置文件,让用户去配置他们的应用程序。
3、应用启动的时候,加载所有已有的用户自定义应用程序。
4、为了支持热部署,我们需要一个监听器,来监听应用发布目录中每个文件的变动。这样,当某个应用重新部署之后,我们就可以得到通知,进而进行热部署处理。
第四种时tomcat等web容器的出现。
第五种时OSGI、Jigsaw等模块化技术的应用。
为什么JNDI,JDBC等需要破坏双亲委派?
我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类是被Bootstrap加载的。
但是,调用方式除了API之外,还有一种SPI的方式。
如典型的JDBC服务,我们通常通过以下方式创建数据库连接:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");
在以上代码执行之前,DriverManager会先被bootStrap类加载器加载,因为java.sql.DriverManager类是位于rt.jar下面的 ,所以他会被根加载器加载。
类加载时,会执行该类的静态方法。其中有一段关键的代码是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这段代码,会尝试加载classpath下面的所有实现了Driver接口的实现类。
那么,问题就来了。
DriverManager****是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有Driver的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。
那么,怎么解决这个问题呢?
于是,就在JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则。
我们深入到ServiceLoader.load方法就可以看到:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
第一行,获取当前线程的线程上下⽂类加载器 AppClassLoader,⽤于加载 classpath 中的具体实现类。
为什么Tomcat要破坏双亲委派
**容器不希望它下面的webapps之间能互相访问到,所以不能用appClassLoarder去加载。**所以tomcat新建一个sharedClassLoader(它的parent是commonClassLoader,commonClassLoader的parent是appClassLoarder,默认情况下,sharedClassLoader和commonClassLoader是同一个UrlClassLoader实例),这是catalina容器使用的ClassLoader。对于每个webapp,为其新建一个webappClassLoader,用于加载webapp下面的类,这样webapp之间就不能相互访问了。tomcat的ClassLoader 与一般类加载器的顺序是相反的(正常是先不会自己尝试加载此类,而是委托给父类的加载器去完成),首先用webappClassLoader去加载某个类,如果找不到,再交给parent。而对于java核心库,不在tomcat的ClassLoader的加载范围。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。
不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。
如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。
如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。
Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
模块化技术与类加载机制
近几年模块化技术已经很成熟了,在JDK 9中已经应用了模块化的技术。
其实早在JDK 9之前,OSGI这种框架已经是模块化的了,而OSGI之所以能够实现模块热插拔和模块内部可见性的精准控制都归结于其特殊的类加载机制,加载器之间的关系不再是双亲委派模型的树状结构,而是发展成复杂的网状结构。
热部署的原理
在JDK中,双亲委派也不是绝对的了。
在JDK9之前,JVM的基础类以前都是在rt.jar这个包里,这个包也是JRE运行的基石。
这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。
在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。
Class<?> c = findLoadedClass(cn);
if (c == null) {
// 找到当前类属于哪个模块
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
//获取当前模块的类加载器
BuiltinClassLoader loader = loadedModule.loader();
//进行类加载
c = findClassInModuleOrNull(loadedModule, cn);
} else {
// 找不到模块信息才会进行双亲委派
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
}
}
沙箱安全机制``:
Java安全模型的核心就是Java沙箱。沙箱机制就是讲Java代码限定在虚拟机JVM特定的运行范围中,并且严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
例如:命名文件时不能以java.long包命名等…
方法区运行时数据区
\1. PC寄存器,程序计数器
什么是程序计数器:
程序计数器是用于存放下一条指令所在单元的地址的地方。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为"取指令"。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
PC****寄存器,程序计数器为什么是线程私有?
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,也就是说,在同一时刻一个处理器内核只会执行一条线程,处理器切换线程时并不会记录上一个线程执行到哪个位置,所以为了线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
为什么要使用PC寄存器记录当前线程的执行地址?
1.CPU需要不停的切换各个线程,线程切换回以后,需要知道自己接着从哪里继续执行。
2.JVM的字节码解释器需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么回被设定为线程私有?
为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
为什么没有OutOfMemoryError
因为程序计数器里面存储的是字节码文件的行号,这个范围是可知的,所以可以定量分配内存,不会溢出。
为什么执行Native方法,值为空?
Native方法大多是通过C实现并未编译成需要执行的字节码指令对java方法是字节码偏移量,对于native方法是undefined(native是非java代码编写的,比如C,C++, 它们无法在java编译时生成字节码,即JVM获取不到native实现,只能通过系统指令去调用native方法)
native方法执行后,线程又如何确保下一次执行的位置?
这是因为每个Java线程都直接映射到一个OS线程上执行。所以native方法就在本地线程上执行,无需理会JVM规范中的程序计数器的概念。仔细看一下JVM规范,如果一个线程执行Native方法,程序计数器的值未定义,可不是一定为空,任何值都可以。native方法执行后会退出(栈帧pop),方法退出返回到被调用的地方继续执行程序。
5.JVM内存模型
1.类装载子系统
2.运行时数据区
1.本地方法栈
当每次方法执行需要调用本地方法时,都会从本地方法区取一块空间。
2.虚拟机栈
内部栈帧:
局部变量表:存放当前方法的局部变量,在局部变量表索引为零的地方存放的是当前对象的实例引用,可以用this来获取。
而当变量的结果为一个对象时,则会存在对空间中。
操作数栈: 代码指令进行操作的一块区域,如
动态链接:在方法执行过程中,所有的方法名,变量,等都存在方法区,而栈中只有他们的动态链接,通过动态链接可以找到他们的地址
方法出口:在进入方法之前,会记录当前方法的下一条指令,进入方法执行结束之后,会继续执行上级方法的下一条代码。
3.堆
堆区执行流程:
Minor GC 会引发 STW(Stop The World) ,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复进行。对象在被创建后进入堆空间中的Eden区,当Eden区满时,会进行YuangGC,存活的对象被放入Survivor的0区。随后每次当Eden区满时,都会进行GC,Survivor区会进行交换并给对象的次数+1,当对象的次数大于15时。则会晋升到老年代。
对象在堆的分配原则:
1.优先分配到Eden
2.大对象直接分配到老年区,避免程序中出现过多大对象
3.长期存活的对象
4.当Survivor区的相同年龄的对象大小之和大于Survivor区的一半,可以直接进入老年区。
触发MinorGC/YuangGC:
1.Eden区内存不足时,会触发YuangGC。
2.YuangGC会回收整个年轻代的内存空间,包括Survivor区。
3.Survivor区满时不会触发GC
触发FullGC的条件:
1.系统调用System.gc()
2.当Old区空间不足
3.方法区空间不足
4.MinorGc后,Survivor进入老年代的对象大小大于老年代可用内存
为什么要有STW机制
4.方法区
5.程序计数器
记录正在运行程序将要执行的下一行代码的内存地址的位置
3.字节码执行引擎
运行时数据区,类装载子系统,字节码执行引擎。
6.判断对象是否可以被回收
1.引用计数器:当对象被引用时,引用计数器值加一,当引用被释放时计数器减一。计数器为零时被回收,缺点不能解决循环引用问题。
2.可达性分析:指从GCRoot开始查找,当一个对象到GCRoot没有引用链时,则对象就可以回收。
7.垃圾回收算法
1.标记清除算法
判断完对象是否可以回收后,只对可以回收的算法进行清除,将此块空间变为可用空间。
缺点:容易产生内存碎片
2.复制算法
堆中使用的就是复制算法,从内存中分一半位置来存放存活的对象。
缺点:消耗一部分内存
3.标记整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点:效率低,执行时,会同步线程
8.垃圾收集器
1.Serial-SerialOld 串行,单线程,执行时暂停所有线程,复制算法和标记整理算法
2.parallelScavenge-parallelOld (JDK1.8)
**为什么使用:**并行,多线程,吞吐量最优,执行时暂停所有线程,复制算法和标记整理算法
parallelScavenge:
使用在新生代,复制算法
parallelOld
使用在永久代,标记整理
3.ParNew-CMS
ParNew:
使用在新生代,使用复制算法,是Serial的多线程版本
CMS:
使用在老年代,使用标记清除算法,是为获取最短STW时间,会产生内存碎片。
执行流程:
初始标记:和用户线程一起执行,从GCRoot开始对关联的所有对象进行标记
并发标记:可用户线程一起,开启并发线程进行可达性分析。
重新标记:STW并发标记过程中因为用户线程而变动的那一部分对象的标记记录。时间比并发标记短很多。
标记清除:真正的清楚阶段,也是并发的。
G1
使用在所有范围的,从JDK1.9以后设为JVM默认GC,其他版本可以通过配置JVM来设置GC -XX UserG1GC
G1可以控制STW时间,每个阶段都是STW的
9.内存泄漏溢出
内存泄漏:
是指应该回收的对象未被回收,在应该被回收的时候未被回收,
内存溢出(OOM)
是指系统中存在不能回收的文件过多,最终程序运行需要的内存大于能提供的内存
产生的原因
1.内存泄漏: 内存泄露问题,严重后会导致内存溢出。
2.大文件图片
3.从数据库中取出大量文件:避免整表查询,对数据库的数据进行分页查询。
4.死循环
10.对象(12kb)
对象填充:
当一个对象占用的内存不足8的倍数时,用对象填充来弥补。
对象头:
对象头内部有存储对象自身运行时数据区(markword),里面有对象的六种
类型指针:标识当前对象是哪个类的实例
若对象为数组:对象长度数据
11.启动参数(堆70%-80%)
设置容量
#JVM启动参数不换行
#设置容量
#设置堆内存-Xmx -Xms
-Xmx4g -Xms4g
#设置单个线程栈大小 -Xss
-Xss1m
#指定mata(元空间)区域大小 -XX:MaxMetaspaceSize
-XX:MaxMetaspaceSize=2g
#开启设置
#指定GC算法
-XX:+UseG1GC -XX:MaxGCPauseMills=50
#指定GC线程数
--XX:ParallelGCThreads=4
#打印GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
#指定GC日志文件
-Xloggc;gc.log
#指定堆内存溢出时自动Dump
#指定Dump到指定位置
-XX:HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/
12.STW,安全点,安全区域
STW:当GC时,会暂停所有用户程序,并进行GC
安全点:在GC暂停时要确保暂停行为不会导致引用关系发生变化,所以会有一个标志,用户线程会不断轮询,当发现True时,就会主动去最近安全点挂起。
13.三色标记法
用来解决并发标记产生的两个问题:一个是错标,标记过不是垃圾的,变成了垃圾(也叫浮动垃圾);第二个是本来已经当做垃圾了,但是又有新的引用指向它。
黑色:一个对象的所有引用都被扫描到
灰色:有引用未被扫描到
白色:从未被扫描到,或被扫描到,没有引用指向。
初始时,所有对象都在 【白色集合】中;
将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。
重复步骤3,直至【灰色集合】为空时结束。
结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。
14.排查OOM
1.配置VM参数:当溢出时,打印信息
-XX:HeapDumpOnOutOfMemoryError
2.打开排查工具 VisualVm
3.将hprof文件导入查看
4.根据错误信息定位代码位置
15.排查CPU
1.top命令查看