一、类运行全过程
当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。
package com.tulingxueyuan.jvm;
public class Math {
public static int initData = 666;
public static User user = new User();
public int complate(){ // 一个方法对应一块栈帧方法区
int a = 1;
int b = 2;
int c = ( a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.complate();
}
}
图上的左边都是C语言实现的,作为java程序员需要重点关注ClassLoad类加载过程。
ClassLoad类加载过程大致分为以下几步:
1.加载:把类丢到内存里去加载;
2.验证:验证文件的格式(.class里面的格式);
3.准备:给静态变量赋默认值,分配内存空间;
4.解析:符号引用到直接引用(这个有点难理解,后面会继续补充),这里稍微提一下静态连接与动态连接。静态连接:将一些静态方法替换为所存内存的指针;动态连接:程序在运行期间将符号引用替换为直接引用。
5.初始化:对类的静态变量初始化为指定的值,执行静态代码块。
注意:主类在运行过程中如果使用到其他类,会逐步加载这些类,jar包或者war包里的类不是一次性全部加载的,是使用的时候才加载的。
二、类加载器
类加载器主要分为以下4种:
1.引导类加载器:在JRE下的lib目录下的核心类库,如rt.jar,charsets.jar等,如下图所示:
2.扩展类加载器:在JRE下的lib目录下ext扩展目录中的JAR类包,如下图所示:
3.应用程序类加载器:加载ClassPath路径下的类包,主要是你自己写的那些类;
4.自定义加载器:加载用户自定义路径下的类包;
类加载器代码演示:
package com.tulingxueyuan.jvm;
import sun.misc.Launcher;
import java.net.URL;
/**
* 类加载器
*/
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());// c++生成对象 不是java生成对象 结果为null 引导类加载器
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());// 扩展类加载器
System.out.println(TestJDKClassLoader.class.getClassLoader());// 应用程序类加载器
System.out.println();
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the boorstrapLoader:"+ bootstrapLoader);
System.out.println("the extClassloader:"+ extClassloader);
System.out.println("the appClassLoader" + appClassLoader);
System.out.println();
System.out.println("bootstrapLoader加载以下文件:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i <urLs.length ; i++) {
System.out.println(urLs[i]);
}
System.out.println();
System.out.println("extClassLoader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
-----------------------------------------------------------------
结果为:
"C:\Program Files\Java\jdk1.8.0_91\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=52366:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;E:\tulingWS\JDK\out\production\JDK" com.tulingxueyuan.jvm.TestJDKClassLoader
null
sun.misc.Launcher$ExtClassLoader@4b67cf4d
sun.misc.Launcher$AppClassLoader@18b4aac2
the boorstrapLoader:null
the extClassloader:sun.misc.Launcher$ExtClassLoader@4b67cf4d
the appClassLoadersun.misc.Launcher$AppClassLoader@18b4aac2
bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/classes
extClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
appClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;E:\tulingWS\JDK\out\production\JDK;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar
Process finished with exit code 0
自定义类加载器:
如何自定义一个类加载器呢?第一步继承java.lang.ClassLoader类,第二步重写findclass()方法;
代码演示:
package com.tulingxueyuan.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + 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);
// defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("E:/test");
// E盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.tulingxueyuan.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
----------------------------------------------------------------------
结果:
*************load User**********
========自己的加载器加载类方法=======
com.tulingxueyuan.jvm.MyClassLoaderTest$MyClassLoader
没有User1.class文件时:$MyClassLoader
package com.tulingxueyuan.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + 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);
// defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("E:/test");
// E盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.tulingxueyuan.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
----------------------------------------------------------------------
*************load User**********
========自己的加载器加载类方法=======
sun.misc.Launcher$AppClassLoader
有User1.class文件时:$AppClassLoader
三、双亲委派机制
3.1 什么是双亲委派机制
加载某个类时会先委托父加载器加载,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载路径下都无法加载,则在自己的类加载路径中查找并加载。
比如写的Math类,最先会找到应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器加载,顶层引导类加载器在自己的类加载路径里找了半天没有找到Math类,则向下退回加载的Math类的加载请求,扩展类加载器收到回复就自己加载,在自己类加载了半天也没有找到Math类,又向下退回Math类的加载请求给应用程序类加载器加载,应用程序类加载器于是在自己的类加载器找到了Math类,结果就自己加载了。
双亲委派机制简单来说就是,先找到父亲加载,不行再由儿子自己加载。
3.2 为什么要设计双亲委派机制
主要有两个原因:
1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改;
2.避免类的重复加载:当父类已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性;
代码演示:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("*******************My String Class*******************");
// 不会加载成功 跟JDK自带的String一样 沙箱安全机制
}
}
-----------------------------------------------------
结果
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
3.3 打破双亲委派机制
package com.tulingxueyuan.jvm;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + 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);
// defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new 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) {
long t0 = System.nanoTime();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if(!name.startsWith("com.tulingxueyuan.jvm")) {
c = this.getParent().loadClass(name);
} else {
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;
}
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("E:/test");
// E盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
Class clazz = classLoader.loadClass("com.tulingxueyuan.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
--------------------------------------------------------
*************load User**********
========自己的加载器加载类方法=======
com.tulingxueyuan.jvm.MyClassLoaderTest$MyClassLoader
四、JVM整体结构及内存模型
堆:所有new出来的对象;
栈:局部变量 一个方法对应一个栈帧内存空间(方法结束了栈内存释放);
局部变量表:其中的数据可以理解为一个数组下标从零开始 0代表this、1代表1;
操作数据栈:程序在运行过程中需要操作的一块临时内存空间;
程序计数器:为了区分多线程并发情况下的线程号(名,可以代表那个线程);
动态链接:符号引用变成直接引用;
本地方法:有native修饰,大多都是C语言的方法;
gc root:线程栈的本地变量、静态变量、本地方法栈的变量等
对象头:主要由三部分组成,1.Mark Word标记字段(32位图);
2.Klass Pointer类型指针:堆里面的对象通过指针指向了方法区的类信息。如第二节课中jvm整体结构及内存模型中math对象指针指向了Math.class类信息;
3.数组长度(4字节 只有数组对象才有)
Minor GC:在年青代发生的gc(采用复制算法);
Full GC:在老年代发生的gc;
Eden与Survivor默认比例是:8:1:1;
方法区:常量+静态变量+类信息;
补充:
stw机制: 全称stop the world,停止用户线程;
jvm为什么要设置stw机制?
如果不设置stw机制的话,栈线程中的局部变量都被释放了,方法执行完毕,main栈帧的指针指不到堆中了。这样就会导致大量的对象没有了,重新创建太麻烦了。
五、JVM内存参数设置
-Xms:初始堆大小;
-Xmx:最大堆大小;
-Xmn:设置年轻代大小;
-Xss:设置每个线程的堆栈大小;
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发
full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超
过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样。
方法区容量自动扩容机制:
ideal中jvm参数优化:
如果是一下配置,大概2万多次
package com.tulingxueyuan.jvm;
public class StackOverflowTest {
// JVM设置
// -Xss128k -Xss默认1M
static int count = 0;
public static void redo() {
count++;
redo();
}
public static void main(String[] args) {
try {
redo();
} catch (Throwable t) {
t.printStackTrace();
System.out.println(count);
}
}
}
----------------------------------
com.tulingxueyuan.jvm.StackOverflowTest
java.lang.StackOverflowError
at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:10)
at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:10)
24699
如果是以下配置:
package com.tulingxueyuan.jvm;
public class StackOverflowTest {
// JVM设置
// -Xss128k -Xss默认1M
static int count = 0;
public static void redo() {
count++;
redo();
}
public static void main(String[] args) {
try {
redo();
} catch (Throwable t) {
t.printStackTrace();
System.out.println(count);
}
}
}
-----------------------------------------------
java.lang.StackOverflowError
at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:9)
at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:10)
1101
结论:
-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。
六、内存分配机制详解
6.1 对象的创建
6.1.1 类加载检查
6.1.2 分配内存
1.指针碰撞(Bump the Pointer):有规整的将对象移动到指针(已使用内存与空闲内存分界线)指向的空闲空间(对象大小与空闲空间一样大),默认采用指针碰撞方式分配内存;
2.空闲列表(Free List):将不规整(已使用内存与空闲内存相互交错)的内存空间当做一个列表,如果新来对象的内存小于等于空闲列表,那么空闲列表可以存放新来的对象,否则就不能存放;
解决内存分配过程中的并发问题,CAS(compare and swap CAS底层原理在并发专题)利用CAS配上失败重试的方式保证原子性的操作同时保证内存分配同步处理。(如果第一个线程抢到内存,第二个线程不能抢到跟第一个线程同样的内存,第二个线程通过CAS加失败重试的方式抢到其他的内存)
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):为每个线程分配一个专属的内存空间大小(如果放不下就走CAS)。通过XX:+/UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。
6.1.3 初始化
对成员变量赋零初值,跟静态变量初始化一样。
6.1.4 设置对象头
由对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头主要由三部分组成:
1.Mark Word标记字段(32位图)
2.Klass Pointer类型指针:堆里面的对象通过指针指向了方法区的类信息。如第二节课中jvm整体结构及内存模型中math对象指针指向了Math.class类信息;
3.数组长度(4字节 只有数组对象才有)
对象对齐是8的整数倍
6.1.5 执行方法
程序员为属性赋值(这里的赋值跟初始化赋零值不一样,这里赋实际的值)与执行构造方法。
6.2 对象大小与压缩指针
6.2.1 什么是java对象的指针压缩?
通过XX:+UseCompressedOops参数可以把对象的内存压缩成4个字节(JDK1.6以后默认开始指针压缩);
6.2.2 为什么要进行压缩?
如果不进行指针压缩一个对象的内存就变成8个字节,堆内存很快就会占满,会触发gc(减少每个对象的内存大小);如果没有指针压缩一个对象内存是8个字节,进行指针压缩以后一个对象内存是4个字节,节约内存空间。
6.3 对象内存分配
6.3.1 对象栈上分配
对象逃逸分析:在方法中定义一个对象,这个对象被外部某个方法引用(例如作为参数调用到其他方法)。
将不会逃逸分析的对象先放入栈里,让方法结束时跟随栈内存一起被回收掉。JDK7之后默认开启逃逸分析(-XX:+DoEscapeAnalysis),如果要关闭使用参数(-XX:-DoEscapeAnalysis)。
代码演示:
public User test1(){// test1方法中的user对象 会被其他方法引用 User u = test1(); user对象逃逸test1()方法
User user = new User();
user.setName("周瑜");
user.setId(1);
return user;
}
public void test2(){// test2方法中user对象只能在test2方法里使用 没有逃逸
User user = new User();
user.setName("诸葛");
user.setId(2);
}
标量替换:对于不会发生逃逸现象的对象,JVM会优先考虑将对象放到栈内存空间,JVM也不会创建(new)对象而是将对象的成员变量拆开放入到栈帧中去,也不用担心栈帧中没有连续的内存空间。
6.3.2 对象在Eden区分配
大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够内存空间进行分配时,将会出发一次Minor GC。
Minor GC:在年青代发生的gc(采用复制算法);
Full GC:在老年代发生的gc;
Eden与Survivor默认比例是:8:1:1
6.3.3 大对象直接进入老年代
大对象就是需要连续内存空间的对象,JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,这个参数只在 Serial 和ParNew两个收集器下有效(这两个参数会在垃圾收集器时详细讲),比如设置JVM参数 -XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC;
大对象为什么直接进入老年代:为年轻代节省更多的内存空间
6.3.4 长期存活的对象将进入老年代
虚拟机给每个对象一个对象年龄(Age)计数器,如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
6.3.5 对象动态年龄判断
在触发minor gc以后,如果一个对象内存大小超过Survivor区内存大小的50%,那么这个对象会被挪到老年代中去。例如Survivor区域里现在有一批对象,年龄1+年龄2+…+年龄n的多个对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代了。
6.3.6 老年代空间分配担保机制
在触发minor gc之前,如果老年代剩余可用空间小于年轻代里现有的对象(包括垃圾对象),
会看是否配置了这个参数-XX:HandlePromotionFailure”(jdk1.8默认就设置了)的参数,如果配置了参数老年代剩余可用空间小于历史每一次minor gc后进入老年代的对象的平均大小,就会触发full gc,最后再触发minor gc;如果没有配置-XX:HandlePromotionFailure这个参数,直接触发full gc,最后再触发minor gc;
在触发minor gc之前,如果老年代剩余可用空间大于等于年青代里现有的所有对象(包括垃圾对象),直接触发minor gc。
在触发minor gc之后,如果老年代剩余可用空间小于历史每一次minor gc后进入老年代的对象的平均大小,再次触发full gc(平均做一次minor gc 就会做两次full gc)
6.4 对象内存回收
6.4.1 引用计数器
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决 对象之间相互循环引用的问题。(一般不用)
6.4.2 可达性分析算法(内存泄漏)
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的 对象都是垃圾对象,在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。
6.4.3 finalize()方法最终判定对象是否存活
一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
public class OOMTest {
public static List<Object> arrayList = new ArrayList<>();
public static void main(String[] args) {
List list = new ArrayList<>();
int i = 0;
int j = 0;
while (true){
list.add(new User(i++,UUID.randomUUID().toString()));// 此对象不会被回收
new User(j--,UUID.randomUUID().toString());// 此对象会被回收 仅触发一次finalize()
}
}
}
-----------------------------------------------------------
收的对象
关闭资源,userid=-50132即将被回收的对象
关闭资源,userid=-50131即将被回收的对象
关闭资源,userid=-50130即将被回收的对象
......
6.4.5 如何判断一个类是无用的类
这里“无用的类”是指方法区里无用的类,需要满足以下3个条件:
1.该类的所有对象都会被回收,也就是java堆中不存在该类的任何实例;
2.加载该类的classLoader已经被回收(一般不容易会被回收,只有自定义的类加载器才会被回收)
3.该类的java.lang.class对象没有在任何地方被引用,无法在任何地方反射访问该类的方法。
很难同时满足以上3点,所以方法区里也回收不了什么对象。
总结
主要是对JVM运行全过程、类加器、双亲委派机制以及其原因与打破双亲委派机制,JVM内存模型以及内存模型分配详解