类加载运行过程
java命令运行某个main函数启动程序时,首先需要通过类加载器把主类加载到JVM
具体的类加载过程有以下几步:
加载→验证→准备→解析→初始化→使用→卸载
加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,java这里采用的时懒加载机制,例如:调用类main()方法时,new对象等,在加载阶段会在内存生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(主类在运行过程中如果使用到其他类,会逐步加载这些类,jar包或war包里的类不是一次性全部加载的,是使用到的时候才加载)
验证:校验字节码文件的正确性,也就是class文件的合规性,是否满足jvm的规范。
准备:给类的静态变量分配内存,并赋予默认值(比如:0,null),如果定义的时final修饰的常量,会直接赋值。
解析:将符号引用替换为直接引用,会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(变为直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的符号引用替换为直接引用。
初始化:对类的静态变量初始化为指定的值,并执行静态代码块。
运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。 类加载器的引用:这个类到类加载器实例的引用,对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
public class TestDynamicLoad { //会生成TestDynamicLoad.class文件
//静态代码块
static{ System.out.println("=========我是静态代码块=================="); }
public static void main(String[] args) {
//执行main方法 new A对象 此时加载A类,加载A的时候又会进行验证、准备、解析、初始化
new A();
System.out.println("*************load test************");
B b = null; //B不会加载,除非这里执行 new B()
}
}
class A{
static{
System.out.println("======== load A ========");
}
public A(){
System.out.println("========= initial A =============");
}
}
class B{
static{
System.out.println("======== load B ========");
}
public B(){
System.out.println("========= initial B =============");
}
}
类加载器和双亲委派机制
1.引导类加载器: 负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar
2.扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的类包
3.应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
4.自定义加载器:负责加载用户自定义路径下的类包
public class JDKClassLoader {
public static void main(String[] args) {
//获取String 这个核心类的加载器
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(JDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println("=================");
//应用程序类加载器,扩展类加载器,引导类加载器 存在 parent属性 父类加载器,但不代表继承关系
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
//引导类加载器是 C++语言实现的所以是null 其他两个都有名字
System.out.println("the bootstrapLoader :" + bootstrapLoader);
System.out.println("the extClassLoader :" + extClassloader);
System.out.println("the appClassLoader :" + appClassLoader);
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("extClassLoader加载以下文件-================");
//ext扩展类文件夹下
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("appClassLoader加载以下文件=================");
//环境变量这个路径下去找
System.out.println(System.getProperty("java.class.path"));
}
}
类加载器初始化过程:
JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造扩展类加载器,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
//Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自 己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
public ClassLoader getClassLoader() {
return this.loader;
}
public static URLClassPath getBootstrapClassPath() {
return Launcher.BootClassPathHolder.bcp;
}
***************
AppClassLoader的父类是URLClassLoader,但其父加载器(parent属性)是extClassLoader
双亲委派机制
JVM类加载器是有亲子层级结构的
自己的加载路径下都找不到目标类,则在自己类加载路径中查找并载入目标类。【向上委托先找父加载器加载,不行再向下寻找由子加载器自己加载】
父加载器 != 父类 ,app应用程序加载器和 ext扩展类加载器的 顶级父类是ClassLoader
AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法
首先,检查一下指定名称的类是否已经加载过了,如果加载过了,就不需要加载,直接返回。
然后,如果此类没有加载过,会返回null,程序继续走,会进行判断,是否有父加载器;如果有父加载器,由父加载器加载 parent.loadClass(name, false); 如果没有父加载器,调用bootstrap类加载器来加载
最后,如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载
为什么要设计双亲委派机制
1.沙箱安全机制:比如自己写的java.lang.String.class类不会被加载,这样可以防止核心API库被随意篡改
2.避免类的重复加载:当父加载器已经加载了该类时,就没有必要子加载器再加载一次,保证被加载类的唯一性。
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("**************My String Class**************");
}
}
// 运行结果: 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
// public static void main(String[] args)
//否则 JavaFX 应用程序类必须扩展javafx.application.Application
// 自定义的String类是没有加载成功的,所以无法运行,而点击运行的时候,直接去找的系统的String类
//但是系统的String类又是没有main方法的!所以报错
全盘负责委托机制
“全盘负责”是指当一个ClassLoader装载一个类时,除非显示的使用另外一个ClassLoader,该类所依赖及引用的其他类,也会依然由这个ClassLoader加载!
比如之前代码加载的类,appClassLoader加载类的时候还加载了很多依赖的其他类信息
自定义类加载器
自定义加载器需要继承java.lang.ClassLoader类,该类由两个核心方法,一个是loadClass(String,boolean),此方法实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath=classPath;
}
private byte[] loadByte(String name) throws IOException {
name=name.replaceAll("\\.","/");
FileInputStream fil = new FileInputStream(classPath + "/" + name + ".class");
int len=fil.available();
byte[] data = new byte[len];
fil.read(data);
fil.close();
return data;
}
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 myClassLoader = new MyClassLoader("F:/test");
Class aClass = myClassLoader.loadClass("study.entity.Person");
Object obj = aClass.newInstance();
Method method = aClass.getDeclaredMethod("method", null);
method.invoke(obj,null);
System.out.println(aClass.getClassLoader().getClass().getName());
}
}
将Person类的字节码文件放到了另外的文件夹里面,但是路径依然是跟 编译的时候的包路径一致才可以,例如:换了自定义路径名字
修改回来就可以了
打破双亲委派机制
用自定义类加载器加载自己实现的类,只需要重写父类的loadClass方法即可,修改一下判断逻辑,Tomcat的类加载器就打破了双亲委派类加载机制,Tomcat是个web容器。
1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
如果使用默认的类加载机制,无法加载两个相同类库的不同版本,默认的类加载器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。当修改了jsp文件后,后台线程监控文件有改动,就卸载掉这个jsp文件的类加载器,重新创建类加载器,重新加载即可。
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容 器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;
每个 webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制
注意: 同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一
样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类
加载器也是同一个才能认为他们是同一个。
jvm整体结构及内存模型
java语言的跨平台特性:不同操作系统不同平台的指令集不一样,JDK是针对不同平台操作系统有不同的版本,jre里面的jvm也是不同的。不同版本的jvm帮忙实现了跨平台,只需要一份字节码文件在各个jvm中运行即可
方法区(元空间):里面有常量池、运行常量池、类的元数据信息...;简单理解就是存放常量、静态变量、类信息的地方。
栈(线程):栈里面为一个一个的栈帧(每个方法执行就会开辟一块内存空间,用于保存局部变量等信息,与其他线程数据隔离开),同时每个线程独有一个程序计数器;栈为FILO先进后出,后执行的方法,后分配内存空间,反而先执行完,先释放掉内存空间。
本地方法栈:程序运行过程中会调用一些本地方法,通过jvm去调用C或者C++语言实现的方法来完成代码运行,此时需要开辟一块内存空间进行使用,这块空间就是本地方法栈。
每个线程都独有本地方法栈、程序计数器、栈
main方法的局部变量表跟其他的栈帧里面的局部变量表存放的有一点不一样,比如图中Math math=new math() 创建的对象在堆里面,局部变量表放的是math这个对象的位置(堆里面的地址值),而其他的栈帧里面放的是具体值。
局部变量表:第一个槽位放的this,在编译成字节码文件的时候就会把this作为第一个局部变量放入局部变量表,所以在程序中我们可以用this关键字来作为本对象进行调用!其他位置为此方法的一些局部变量
操作数栈:程序在运算过程中需要进行操作的临时存放操作数的中转空间。
动态链接:程序加载的时候只会解析那些静态方法,其他的方法,运行的时候把这些符号引用转为这个符号对应的代码所在的内存的位置,这些相应的代码的位置就放在动态连接这里面的。
方法出口:方法执行完要回到main方法的某个位置,继续往下执行。
程序计数器:每个线程独有的,字节码执行引擎每执行完一行,字节码执行引擎马上会主动修改程序计数器的值,因为多线程环境,可能执行到某行的时候,cpu的时间片被其他优先级更好的程序占用,cpu再次回来的时候不可能从头开始执行代码,所以需要个计数器告诉cpu该执行哪条指令了。
在进行GC的时候,可能会触发STW机制,会停止用户发起的线程(由用户发起的操作,比如下单之类的操作),JVM调优的目的其实就是减少full gc 或者一次full gc的时间,如果minor gc频繁,也应该减少其次数;如果方法区(元空间)满了,也会触发full gc,元空间有自动扩容机制,如果不设置元空间大小,默认触发fullgc的大小为21M
jvm内存参数设置
关于元空间的JVM参数有两个:
-XX:MaxMetaspaceSize 设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发fullgc的初始阈值(元空间无固定初始大小),以字节为单位,默认是21M,达到该值就会触发fullgc进行类型卸载,同时收集器会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
由于调整元空间大小需要full gc,这是非常昂贵的操作,如果应用在启动的时候发生大量full gc,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大!
内存溢出示例:
public class HeapTest {
byte[] a=new byte[1024*100]; //100KB
public static void main(String[] args) throws InterruptedException {
ArrayList<HeapTest> heepList = new ArrayList<>();
while(true){
heepList.add(new HeapTest());
Thread.sleep(10);
}
}
}
StackOverflowError示例:
public class StackOverFlowTest {
//-Xss默认1M 可以放19324多次调用
static int count=0;
static void redo(){
count++;
redo();
}
public static void main(String[] args) {
try {
redo();
} catch (Throwable t) {
System.out.println(count);
t.printStackTrace();
}
}
}
结论:-Xss设置越小,count值越小,说明一个线程栈里能分配的栈帧越少,但是对于jvm整体来说能开启的线程数会更多。
对象创建的主要流程
1.类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有就执行相应的类加载过程。 new关键词、对象克隆、对象序列化等。
2.分配内存:类加载检查通过后,虚拟机将为新生对象分配内存。对象所需要的内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
划分内存的方法:
a. 指针碰撞(Bump the Pointer) (默认),如果java堆中的内存是规整连续的,所有用过的内存放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离。
b.空闲列表(Free List) 如果java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单的使用指针碰撞了,虚拟机就会维护一个列表,记录尚哪些内存块是可以用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
解决并发问题的方法:
a.CAS(Compare And Swap) 虚拟机采用cas配上失败重试的方式保证更新操作的原子性,来对分配内存空间的动作进行同步处理。
b.本地线程分配缓冲(Thread Local Allocation Buffer,TLAB) 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(jvm默认开启),-XX:TLABSize指定TLAB大小。相当于给每个划线程专属的堆内存空间,如果放不下,继续走cas。
3.初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为初始值(不包括对象头),程序能访问到这些字段的数据类型对应的零值(可理解为成员变量赋值为对应类型的零值)
4.设置对象头
初始化完成后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。都存放再Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding);
对象头又包括两部分信息:
GC分代年龄(一般用4bit表示,所以年轻代之中年龄最多达到15)、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
2.Klass Pointer类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例【 如创建一个Math类,有math的类元信息(c#语言实现的)放方法区;同时为了方便java开发人员使用,堆里面还有math的mathClass对象(可以理解为供使用的镜像,一个入口,是没有存代码信息的)jvm用的头里面的类型指针(klass 类型指针指向方法区的math类元信息)】
markword 8个字节,64位。4个字节的klass 指针,整体大小是8的整数倍,不足会自行补齐,成员变量之间也有内部对齐
5.执行<init>方法
执行<init>方法,调用本地初始化方法,会调用构造方法,同时为对象属性赋上真正的值。
对象大小及指针压缩
package practice.practice.jol;
import org.openjdk.jol.info.ClassLayout;
public class Jol {
public static void main(String[] args) {
Object o = new Object();
// User o = new User();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
System.out.println("--------------------------");
}
}
都采用8的倍数补齐,在底层算法层面更加性能更加优化。
什么是java对象的指针压缩?
jdk1.6以后在64bit操作系统中,jvm支持指针压缩,jvm配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)--对象指针 ,对启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops;
启用指针压缩会一定程度减少每个对象的大小,压缩后,klass对象指针由8字节变为4字节,比如把35位表述地址,通过压缩算法,压缩为32位,35位可表述32g,内存,拿出来的时候再解压为35位,这样的话8个字节,其实可以放2个对象,为什么指针压缩?节约内存空间,压缩指针有两个,一个是压缩对象头里的指针,还有成员变量之类的指针
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm 只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内 存不要大于32G为好】
对象内存分配
对象栈上分配:【栈上分配依赖于逃逸分析和标量替换】
JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内 存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数 量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的 内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参 数传递到其他地方中。可以理解为调用某方法的时候是否有返回参数,如果没有返回参数则一般就是不可逃逸的对象。开启逃逸分析参数(-XX:+DoEscapeAnalysis)如下:
public User test(){
User user = new User();
user.setId(1);
user.setName("张三");
returen user;
}
public void test1(){
User user = new User();
user.setId(1);
user.setName("貂蝉");
//TODO 保存到数据库
}
标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该 对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认 开启。【因为对象都需要连续的内存空间,可能栈帧里面碎片比较多,此时就会有标量替换,把对象打散拆开,把成员变量这些分开存放,但给这些分开存放的成员变量一个标识,来表示都属于一个对象】
标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一 步分解的聚合量。
如果对象都放栈上,意味着几乎不会产生gc如果知道这个对象比较大,短时间内又不会被回收,不如直接放老年代,否则会在存活区来回复制,或者占用eden区内存空间,不如直接放进去可以提升性能,可自行设置分代年龄,可以节约年轻代空间
对象在Eden区分配:
大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够的空间分配时,虚拟机将发起Minor GC。
Eden与Survivor区默认8:1:1
把新生代的对象提前转移到老年代中去),下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回 收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所 以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可, JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变 化可以设置参数-XX:-UseAdaptiveSizePolicy
大对象直接进入老年代
就是需要大量连续内存空间的对象(比如:字符串、数组))。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下 有效。 比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,满足大对象条件的对象会直接进入老年代, 为了避免为大对象分配内存时的复制操作而降低效率。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代 的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
对象动态年龄判断
对象动态年 龄判断机制一般是在minor gc之后触发的
上图的情况,每14秒minor gc 放60M放到老年代(这里Survivor区域只有100M,此时动态年龄判断,都会存入老年代,每过几分钟后就会触发full gc,这些都是垃圾对象。可以做优化,可以扩大年轻代的区域)
总结:尽可能让对象都在新生代里分配和回收,尽量别 让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃 圾回收。
老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾, 如果回收完还是没有足够空间存放新的对象就会发生"OOM" 当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”。
对象内存回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)
引用计数法
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决 对象之间相互循环引用的问题。
public class ReferenceCountingGc{
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象 GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
常见的引用类型
强引用、软引用、弱引用、虚引用
强引用:普通的变量引用
public static User user = new User();
软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用及虚引用基本是JVM内部使用。
弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多, GC会直接回收掉 ,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用: 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
finalize()方法最终判定对象是否存活(只会生效一次,覆盖此方法,回收的时候可执行)
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
如何判断一个类是无用的类
要同时满足下面3个条件才能完成回收:
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2.加载该类的 ClassLoader 已经被回收。一般只有自定义的类加载器才会被回收
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。