赞
踩
一个类什么时候被加载,《java虚拟机规范》中并没有强制约束,而是交给虚拟机自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类
首先去检查这个类能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个类的符号引用代表的类是否已经被加载、解析、初始化。(即判断类元信息是否存在)如果没有,那么在双亲委派模式下,使用当前了类加载器以ClassLoader+包名+类名为key查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载:
加载:
验证:
准备:
解析:
初始化:
<clinit>
类初始化方法的过程,为类型非final修饰的静态变量显式赋值,同时执行静态代码块的内容,执行顺序取决于声明顺序。当我们new一个类的对象。访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,反射API对一个类进行调用,初始化当前类其父类也会被初始化。。。。那么这些都会触发类的初始化
使用:
卸载:
1、该类的所有实例都已经被GC,也就是JVM中不存在该类的任何实例
2、加载该类的ClassLoader已经被GC
3、该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
类的初始化阶段,JVM虚拟机就开始真正执行我们编写的Java代码
进行准备阶段,变量(非final修饰的static变量)已经赋值过一次系统要求的初始零值,而在初始化阶段,才真正初始化变量和其他资源
只需要注意三点即可:
<clinit>
public class Test{
//场景1:对于非静态的变量,不管是否进行显示赋值,都不会生成<clinit>()
public int num=1;
//场景2:静态的字段,没有显示赋值,不会生成<clinit>()
public static int num1;
//场景3:典型的生成<clinit>()的场景
public static int num2 = 2;
//场景4: 对于声明为static final的基本数据类型的字段,不管是否进行了显示赋值,都不会生成<clinit>() 准备环节对num3进行显示赋值
public static final int num3 = 2;
}
<clinit>
<clinit>
<clinit>
<clinit>
<clinit>
如果不是很理解这块,可以看下面一个问题,二者结合理解
public class Test{ public static int a =1;//在初始化<clinit>() 中赋值 public static final int num INI_CONSTANT =10;//在链接环节的准备阶段赋值 public static final Integer INTEGER_CONSTANT1 =Integer.valueof(10);//在初始化<clinit>() 中赋值 public static Integer INTEGER_CONSTANT1 =Integer.valueof(10);//在初始化<clinit>() 中赋值 public static final String s0 = "hello";//在链接环节的准备阶段赋值 public static final String S1 =new String("hello");//在初始化<clinit>() 中赋值 public static final int num NUM1=new Random().(10);//在初始化<clinit>() 中赋值 }
使用static+final修饰的,且显式赋值中不涉及到方法或者构造器调用的基本数据类型或String类型的显式赋值,是在类加载的准本环节显示赋值的,除此之外都是在初始化阶段进行赋值
Class只有在必须要首次使用的时候才会被加载,Java虚拟机不会无条件地装载Class类型。java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里的时候是指”主动使用“,主动使用只有以下几种情况:(即,如果出现以下情况,则会对类进行初始化操作。而初始化擦操作之前的加载、验证、准备已经完成)
java虚拟机初始化一个类时,要求它的所有父类都已经初始化,但这条规则不适用于接口
<clinit>
线程安全性问题对于<clinit>
()方法的调用,也就是类的初始化,虚拟机会在内部确保多线程环境的安全性。
虚拟机会保证一个<clinit>
()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行<clinit>
()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>
()方法完毕。
正式因为<clinit>
()方法是线程安全的,因此如果一个类的<clinit>
()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为它们看起来并没有可用的锁信息。
如果之前的线程成功加了锁,则等待队列中的线程就没有机会再执行<clinit>
()方法,那么,当需要使用这个类时,虚拟机会直接返回给它准备好的信息。
死锁举例:请看下面伪代码
class A{ static{ Class.forName("包名.B"); } } class B{ static{ Class.forName("包名.A"); } } public Class C{ public static void main(String[] args){ new A(); new B(); } }
(以上是在类加载的时候进行初始化,先加载父类是因为双亲委派机制,父类或子类的静态·初始化的先后顺序和代码声明顺序一致)
在类的加载阶段,通过一个类的全限定名来获取描述这个类的二进制字节流的动作的代码被称为类加载器(Class Loader),这个动作是可以自定是实现的
JVM支持两种类型的类加载器,分别是引导类加载器(Bootstrap ClassLoader)和自定义加载器(User-Defined ClassLoader)。
从概念上讲,自定义类加载器一般指的是程序中有开发人员自定义的一类加载器,但是java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器。
不论加载器的类型如何划分,在程序中我们最常见的类加载器只有3种:
1、引导类加载器(Bootstrap ClassLoader)
C/C++
语言实现的,嵌套在JVM内部JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path
路径下的内容),用于提供JVM自身需要的类java、javax、sun
等开头的类2、扩展类加载器(Extension ClassLoader)
java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext
子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载3、应用程序类加载器(也称为系统类加载器,AppClassLoader)
classLoader.getSystemclassLoader()
方法可以获取到该类加载器答案:不是,而是一种包含关系。
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
获取当前类的ClassLoader:
clazz.getClassLoader();
获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
创建数组的情况稍微有些特别,因为数组本省并不是由加载器负责创建,而是JVM在运行时根据需要直接创建的,但是数组的元素依然需要依靠类加载器去创建,创建数组的过程:
1、如果数组的元素是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型
2、JVM使用指定的元素类型和数组维度创建新的数组类。
基本数据类型由虚拟机预先定义,引用类型需要进行类的加载
类的卸载是很苛刻的,必须同时满足以下三个条件:
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要到该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派机制。
工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派机制的优势
如果你定义的类的全限定名和核心类库中的类一致,那么类加载器只会加载核心类库中的,不会加载你的
可以;
想要打破这种模型,那么就自定义一个类加载器,重写其中的loadClass 方法,使其不进行双亲委派。
我们先分析下双亲委派机制到底怎么实现的?
先看下ExtClassLoader与AppClassLoader
由下图可以看出 ExtClassLoader与AppClassLoader是Launcher的内部类同时它们都继承了URLClassLoader,
双亲委派的核心源码
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 { //获取当前类的加载器(AppClassLoader)的父加载器(ExtClassLoader) if (parent != null) {//ExtClassLoader的parent是null c = parent.loadClass(name, false);//父加载器(ExtClassLoader)去迭代加载 } 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(); //由ExtClassLoader调用了findClass记载 //如果ExtClassLoader也是没加载成功的话,最终会由AppClassLoader执行findClass 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、继承自ClassLoader
2、覆盖findClass(String name)方法或者loadClass()方法 ;
如果你覆盖loadClass()方法就可以打破双亲委派机制
findClass(String name)不会打破双亲委派机制
public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { String fileName =name.substring(name.lastIndexOf(".")+1)+".class"; InputStream is = getClass().getResourceAsStream(fileName); if(is==null){ throw new ClassNotFoundException(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//使用我们自定义的加载器加载
MyClassLoader myClassLoader = new MyClassLoader();
Class clazz = myClassLoader.loadClass("com.test.User");
System.out.println(clazz.getClassLoader());
//使用系统类加载器加载
System.out.println(User.class.getClassLoader());
Class userClazz = User.class;
System.out.println(userClazz.newInstance());
}
}
loadClass():就是主要进行类加载的方法,默认的双亲委派机制就实现在这个源码中
findClass():根据名称或者位置加载载.class字节码(加载成二进制流)
defineClass():把字节码转为为java.lang.Class对象
1、当我们想要自定义一个类记载器的时候,并想破坏双亲委派机制,我们会重写loadClass()方法。
2、如果我们想定义一个类加载器,但是又不想破坏双亲委派机制,我们可以重写findClass();
findClass()是jdk1.2之后ClassLoader新添加的方法,这个方法之抛出一个异常,没有默认实现;
3、defineClass()是本地方法(native修饰的),C实现的
<clinit>
)<clinit>
)直到这个类第一次使用是才进行初始化不会;
首先类是按需加载的。参考类主动使用的
会
tomcat 5:
tomcat6及以上:lib下的类全由CommonClassLoader加载
可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和一个Web应用类加载器和一个JSP类加载器
3个基础类加载器在conf/catalina.properties中进行配置
原配置(tomcat6)如下:
common.loader ="${catalina.base}/lib","${catalina.base}}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader =
shared.loader =
Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父类加载,目的就是为了优先加载Web应用自己定义的类,我们知道ClassLoader默认的方法是以双亲委派模型加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,而WebAppClassLoader类中重写了loadClass方法;同时JaperLoader也重写了,打破了双亲委派机制
Tomcat是一个Web容器,那么一个Web应用可能需要部署多个应用程序
1、部署在同一个Tomcat上的两个Web应用所使用的Java类库要相互隔离(一个应用对应一个WebClassLoader)
2、部署在同一个Tomcat上的两个Web应用所使用的Java类库要互相共享(只要一个CommonClassLoader)
3、保证Tomcat服务自身的安全不受部署的Web应用程序影响
4、需要支持JSP页面的热部署和热加载(一个JSP对应一个Jsp的类加载器,Jsp会被翻译为java代码后再编译成class文件,有自己的类加载器加载到JVM方法区,tomcat会监听jsp的修改,生成新的JaperClassLoader加载新的class文件,进而实现热部署)
热加载:是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境
热部署:是指可以在不重启服务的情况下重新部署整个项目,比如tomcat热部署就是在程序运行时,如果我们修改了war包的内容,tomcat1就会删除之前的war包解压的文件,重新解压新的war包生成新的文件夹;
如何实现热加载呢?
在程序代码更改且重新编译后,让运行的进程可以实时获取到编译后的class文件,然后重新进行加载:
1、实现自己的类加载器;
2、从自己的类加载器中加载要热加载的类。
3、不断低轮询要热加载的类class文件是否有更新,如果有更新,重新加载。
首先自定义加载器,覆盖findClass
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 自定义一个类加载器 * * bootstrap ClassLoader jdk/jre/目录下的jar包加载 * * ext ClassLoader jdk/ext/目录下的jar包加载 * * App ClassLoader --我们应用的ClassLoader * */ public class MyClassLoader extends ClassLoader { private File classPathFile; private static Map<String, Class> clazzCache = new ConcurrentHashMap<>(); public MyClassLoader() { String classPath = MyClassLoader.class.getResource("").getPath(); this.classPathFile = new File(classPath); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { return this.findClass(name, false); } protected Class<?> findClass(String name, boolean force) throws ClassNotFoundException { //之前有没有加载过这个class Class cls = clazzCache.get(name); if (force) { cls = null; } String className = MyClassLoader.class.getPackage().getName() + "." + name; if (cls == null && classPathFile != null) { File classFile = new File(classPathFile + "\\" + name.replaceAll("\\.", "/") + ".class"); if (classFile.exists()) { FileInputStream fis = null; ByteArrayOutputStream bos = null; try { fis = new FileInputStream(classFile); byte[] bytes = new byte[4096]; bos = new ByteArrayOutputStream(); int len; while ((len = fis.read(bytes)) != -1) { bos.write(bytes, 0, len); } cls = defineClass(className, bos.toByteArray(), 0, bos.size()); clazzCache.put(name, cls); } catch (Exception e) { e.printStackTrace(); } finally { try { if(fis != null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } try { if(bos != null) { bos.close(); } } catch (IOException e) { e.printStackTrace(); } } } } return clazzCache.get(name); } }
定义要热部署的文件MyLog
public interface ILog {
public void log();
}
public class MyLog implements ILog {
@Override
public void log() {
System.out.println("log, version 1.0");
}
}
定时任务监视文件的修改
public class FileDefine {
public long lastDefine = System.currentTimeMillis();
public long getLastDefine() {
return lastDefine;
}
public void setLastDefine(long lastDefine) {
this.lastDefine = lastDefine;
}
}
package com.bjpowernode.hotreload; import java.io.File; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * 测试运行 */ public class Test { public static void main(String[] args) throws Exception { Map<String, FileDefine> fileDefineMap = new ConcurrentHashMap<>(); File file = new File(Test.class.getResource("").getPath()); File[] files = file.listFiles(); for (File watchFile : files) { FileDefine fileDefine = new FileDefine(); fileDefine.setLastDefine(watchFile.lastModified()); fileDefineMap.put(watchFile.getPath(), fileDefine); } //定时任务 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10); scheduledExecutorService.scheduleAtFixedRate(new WatchDog(fileDefineMap), 3, 3, TimeUnit.SECONDS); MyClassLoader w = new MyClassLoader(); while (true) {`在这里插入代码片` Class clazz = w.findClass("MyLog"); try { ILog myLog = (ILog) clazz.newInstance(); myLog.log(); Thread.sleep(2000); }catch (Exception e) { e.printStackTrace(); } } } }
测试类:
package com.bjpowernode.hotreload; import java.io.File; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * 测试运行 */ public class Test { public static void main(String[] args) throws Exception { Map<String, FileDefine> fileDefineMap = new ConcurrentHashMap<>(); File file = new File(Test.class.getResource("").getPath()); File[] files = file.listFiles(); for (File watchFile : files) { FileDefine fileDefine = new FileDefine(); fileDefine.setLastDefine(watchFile.lastModified()); fileDefineMap.put(watchFile.getPath(), fileDefine); } //定时任务 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10); scheduledExecutorService.scheduleAtFixedRate(new WatchDog(fileDefineMap), 3, 3, TimeUnit.SECONDS); MyClassLoader w = new MyClassLoader(); while (true) { Class clazz = w.findClass("MyLog"); try { ILog myLog = (ILog) clazz.newInstance(); myLog.log(); Thread.sleep(2000); }catch (Exception e) { e.printStackTrace(); } } } }
1、Mall.java—>javac----->Mall.class----->java Mall (会启动一个JVM进程)
2、Mall.java---->javac----->Mall.class---->Mall.jar---->java -jar Mall.jar (会启动一个JVM进程)
3、Mall.java---->javac----->Mall.class---->Mall.war---->Tomcat----->startup.sh—>org.apache.catalina.startup.Bootstrap (会启动一个JVM进程)
其实运行起来都是通过一下两个指令
public class Application { //main线程---》main的线程栈,也就是虚拟机栈 public static void main(String[] args) throws Exception{ load(); System.in.read();//程序不要退出 } public static void load(){ Config config = new Config(); config.loadData(); } } public class Config { public static Manager manager = new Manager(); private int a; public String loadData(){ return "abc"; } }
1、程序计数器是一块较小的内存空间,几乎可以忽略
2、是当前线程所执行的字节码的行号指示器,指向下一个要执行的字节码执行的行号,执行引擎就可以通过程序计数器知道自己要执行那个字节码执行了
3、线程私有
4、运行时数据区中唯一一个既不存在OOM也不存在GC的地方
5、生命周期与线程一致,随线程而生,随线程而死
1、因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
2、JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
我们都知道多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
1、线程私有
2、方法执行会创建栈帧,一个方法对应一个栈帧,存储局部变量表等信息。
3、方法的调用对应着栈帧的入栈,方法结束对应着出栈
4、如果栈的大小是固定的,如果栈的可用空间小于栈帧的所需的空间,就会StackOverflowError
5、如果栈的大小是动态扩展的,如果内存的空间不足以支撑栈所神请的内存就会报OutOfMemoryError(比较少见)(HotSpot没有,HotSpot不支持栈的动态扩展)
6、栈管运行,栈帧中的局部变量表存放方法中的局部变量,所有的局部变量表中的引用变量都是GC roots
7、栈的大小可以通过 -Xss设置,如果不设置默认是1M: -Xss1M
8、生命周期与线程一致,随线程而生,随线程而死
9、栈不会发生GC,这个也可以理解,方法运行过程中,在栈中产生的数据,随着方法的结束,通过出栈操作而释放
1、每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
2、在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
3、栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
1、JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出(后进先出)原则
2、在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
3、执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
4、如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
5、不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
6、如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
7、Java方法有两种返回函数的方式。
局部变量表(Local Variables)
操作数栈(Operand Stack)(或表达式栈)
动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
一些附加信息
1、参数值的存放总是从局部变量数组索引 0 的位置开始,到数组长度-1的索引结束。
2、局部变量表,最基本的存储单元是Slot(变量槽),局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
3、在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型占用两个slot(1ong和double)。
4、JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
5、当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
6、如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
7、如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。(this也相当于一个变量)
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
SOF(StackOverflowError),栈大小分为固定的,和动态变化。如果是固定的就可能出现StackOverflowError。如果是动态变化的,内存不足时就可能出现OOM
HotSpot栈是固定的
不能保证不溢出,只能保证SOF出现的几率小
不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个虚拟机的内存空间是有限的
具体问题具体分析:
如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
/** * 面试题: * 方法中定义的局部变量是否线程安全?具体情况具体分析 * * 何为线程安全? * 如果只有一个线程才可以操作此数据,则必是线程安全的。 * 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。 */ public class StringBuilderTest { int num = 10; //s1的声明方式是线程安全的(只在方法内部用了) public static void method1(){ //StringBuilder:线程不安全 StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); //... } //sBuilder的操作过程:是线程不安全的(作为参数传进来,可能被其它线程操作) public static void method2(StringBuilder sBuilder){ sBuilder.append("a"); sBuilder.append("b"); //... } //s1的操作:是线程不安全的(有返回值,可能被其它线程操作) public static StringBuilder method3(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1; } //s1的操作:是线程安全的(s1自己消亡了,最后返回的只是s1.toString的一个新对象) public static String method4(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1.toString(); } public static void main(String[] args) { StringBuilder s = new StringBuilder(); new Thread(() -> { s.append("a"); s.append("b"); }).start(); method2(s); } }
1、与虚拟机栈基本类似
2、去别在于本地方法栈为Native方法服务
3、HotSpot虚拟机将虚拟机栈和操作数栈合并
4、有StackOverflowError和OutOfMemoryError(较少出现)
5、生命周期与线程一致,随线程而生,随线程而死
6、没有GC
1、线程共享
2、虚拟机启动时创建
3、虚拟机所管理内存中最大的一块区域
4、存放所有的实例对象和数组
5、GC垃圾收集器的主要管理区域
6、可以分为新生代和老年代,比例默认是1:3
新生代由分为,Eden区、s0区、s1区,比例默认是8:1:1
7、可通过-Xmx、-Xms调节堆大小
8、无法在扩展,java.lang.OutOfMemoryError:Java heap space
9、如果从分配内存的角度看。所有线程共享的java堆中可以划分出多个线程私有的分配缓冲区(Thread Loacl Allocation Buffer ,TLAB),以提升对象分配时的效率
1、如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。
2、对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代
3、对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置
针对不同年龄段的对象分配原则如下所示:
优先分配到Eden:开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,但是因为老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢
大对象直接分配到老年代:尽量避免程序中出现过多的大对象
长期存活的对象分配到老年代
动态对象年龄判断
空间分配担保: -XX:HandlePromotionFailure 。
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
如果想要StringBuffer sb不发生逃逸,可以这样写
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
不是,【栈上分配】、【标量替换】、【缩消除】
栈上分配:JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了
选项“-XX:+DoEscapeAnalysis”显式开启逃逸分析
/** * 栈上分配测试 * -Xmx128m -Xms128m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails */ public class StackAllocation { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 10000000; i++) { alloc(); } // 查看执行时间 long end = System.currentTimeMillis(); System.out.println("花费的时间为: " + (end - start) + " ms"); // 为了方便查看堆内存中对象个数,线程sleep try { Thread.sleep(1000000); } catch (InterruptedException e1) { e1.printStackTrace(); } } private static void alloc() { User user = new User();//未发生逃逸 } static class User { } }
1、线程同步的代价是相当高的,同步的后果是降低并发性和性能。
2、在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
3、如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
代码中对hollis这个对象加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
public static void main(String args[]) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x" + point.x + ";point.y" + point.y);
}
class Point {
private int x;
private int y;
}
以上代码,经过标量替换后,就会变成
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x = " + x + "; point.y=" + y);
}
有以下三种:
1、指针碰撞:内存规整的情况下
2、空闲列表:内存不规整的情况下
选择哪种分配方法由Java堆是否规整决定,而Java堆是否规整又由垃圾回收器是否带有空间压缩整理(Compact)的能力决定;
因为,当使用的是Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效
而当使用CMS这种基于清除算法的收集器时,理论上只能采用较为复杂的空闲列表来分配内存;
3、本地线程分配缓冲(TLAB):对象的创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来的及修改,对象B又使用了原来的指针来分配内存的情况
那么解决方式有两种:
(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性
(2)线程隔离,把内存分配的动作按照线程划分到不同的空间之中进行,即每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要进行同步锁定,虚拟机是否需要TLAB,可以通过-XX:+/-UseTLAB
参数来设定(用空间换时间的思想,java中ThreadLocal也是空间换时间的思想)
在为每个线程分配小内存TLAB的时候,可能也存在并发问题,JVM采用了CAS+失败重试的机制解决TLAB分配时的并发问题
在HotSpot虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding);
第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个对象的实例,另外,如果是Java数组,对象头中还有一块用于记录数组长度的数据,因为普通对象可以通过Java元数据确定大小,数组不可以
Java堆中用于存储对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径来避免垃圾回收来清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制的后,就会产出内存溢出:
我们堆内存的设置大小是128M
通过JDK自带的监控工具(VisualVm)分析
最中会发生溢出
MAT工具分析xxx.hprof文件,排查溢出的原因,首先,通过以下参数,设置在堆发生溢出时,在我们指定的路径下生成heapdump.hprof文件,保存堆的快照信息
+XX:+HeapDumpOnOutOfMemoryError
+XX:HeapDumpPath=d:/dev/heapdump.hprof
在发生溢出时,自动为我们生成如下文件
打开MAT(关于MAT的使用可以看我JVM调优篇)
MAT的使用就先不多介绍了
在JVM堆里存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还存活者,哪些已经死去。
java通过可达性分析算法,来判定对象是否存活;
该算法的基本思路:通过一系列称为GC Root的根节点开始,根据引用关系向下搜索,搜索过程中所走过的路径称为”引用链“,如果某个对象到GC Root间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收
对象object 5、object 6、object 7虽然有关联,但是他们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
1、在虚拟机栈(栈帧中的局部变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量所引用的对象
2、方法区/元空间中的类的静态属性所引用的对象
3、方法区/元空间中常量引用的对象
4、在本地方法栈中(JNI)引用的对象
5、Java虚拟机内部的引用。如基本数据1类型对应的Class对象,一些常驻的异常对象(比如NullPointXecepttion、OutOfMemoryError等),还有系统类加载器
6、所有被同步锁(synchronized)持有的关键字
7、反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
8、其他可能临时性加入的对象
1、句柄访问
缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低
优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改
2、直接指针(HotSpot采用)
优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值
Java里有不同的引用类型,分别是强引用、软引用、弱引用和虚引用
public class StrongReferenceTest { public static void main(String[] args) { StringBuffer str = new StringBuffer ("Hello"); StringBuffer str1 = str; str = null; System.gc(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(str1); } }
Hello
声明方式:
Object obj = new Object();// 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用
public class SoftReferenceTest { public static class User { public User(int id, String name) { this.id = id; this.name = name; } public int id; public String name; @Override public String toString() { return "[id=" + id + ", name=" + name + "] "; } } public static void main(String[] args) { //创建对象,建立软引用 // SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk")); //上面的一行代码,等价于如下的三行代码 User u1 = new User(1,"songhk"); SoftReference<User> userSoftRef = new SoftReference<User>(u1); u1 = null;//取消强引用 //从软引用中重新获得强引用对象 System.out.println(userSoftRef.get()); System.out.println("---目前内存还不紧张---"); System.gc(); System.out.println("After GC:"); // //垃圾回收之后获得软引用中的对象 System.out.println(userSoftRef.get());//由于堆空间内存足够,所有不会回收软引用的可达对象。 System.out.println("---下面开始内存紧张了---"); try { //让系统认为内存资源紧张、不够 // byte[] b = new byte[1024 * 1024 * 7]; byte[] b = new byte[1024 * 7168 - 635 * 1024]; } catch (Throwable e) { e.printStackTrace(); } finally { //再次从软引用中获取数据 System.out.println(userSoftRef.get());//在报OOM之前,垃圾回收器会回收软引用的可达对象。 } } }
[id=1, name=songhk]
---目前内存还不紧张---
After GC:
[id=1, name=songhk]
---下面开始内存紧张了---
null
java.lang.OutOfMemoryError: Java heap space
at com.atguigu.java1.SoftReferenceTest.main(SoftReferenceTest.java:48)
Process finished with exit code 0
public class WeakReferenceTest { public static class User { public User(int id, String name) { this.id = id; this.name = name; } public int id; public String name; @Override public String toString() { return "[id=" + id + ", name=" + name + "] "; } } public static void main(String[] args) { //构造了弱引用 WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk")); //从弱引用中重新获取对象 System.out.println(userWeakRef.get()); System.gc(); // 不管当前内存空间足够与否,都会回收它的内存 System.out.println("After GC:"); //重新尝试从弱引用中获取对象 System.out.println(userWeakRef.get()); } }
[id=1, name=songhk]
After GC:
null
Process finished with exit code 0
虚引用:PhantomReference 就像没有引用一样,其作用就是在引用对象被GC回收时候触发一个一同通知,或者触发进一步的处理
// 声明强引用
Object obj = new Object();
// 声明引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 声明虚引用(还需要传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
public class PhantomReferenceTest { public static PhantomReferenceTest obj;//当前类对象的声明 static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用队列 public static class CheckRefQueue extends Thread { @Override public void run() { while (true) { if (phantomQueue != null) { PhantomReference<PhantomReferenceTest> objt = null; try { objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove(); } catch (InterruptedException e) { e.printStackTrace(); } if (objt != null) { System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了"); } } } } } @Override protected void finalize() throws Throwable { //finalize()方法只能被调用一次! super.finalize(); System.out.println("调用当前类的finalize()方法"); obj = this; } public static void main(String[] args) { Thread t = new CheckRefQueue(); t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束。 t.start(); phantomQueue = new ReferenceQueue<PhantomReferenceTest>(); obj = new PhantomReferenceTest(); //构造了 PhantomReferenceTest 对象的虚引用,并指定了引用队列 PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue); try { //不可获取虚引用中的对象 System.out.println(phantomRef.get()); System.out.println("第 1 次 gc"); //将强引用去除 obj = null; //第一次进行GC,由于对象可复活,GC无法回收该对象 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } System.out.println("第 2 次 gc"); obj = null; System.gc(); //一旦将obj对象回收,就会将此虚引用存放到引用队列中。 Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
null
第 1 次 gc
调用当前类的finalize()方法
obj 可用
第 2 次 gc
追踪垃圾回收过程:PhantomReferenceTest实例被GC了
obj 是 null
Process finished with exit code 0
JVM堆内存分代模型:年轻代、老年代
大部分对象朝生夕死,少数对象长期存活
JVM里垃圾回收针对的是 新生代、老年代、还有元空间(方法区)(永久代)
不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接从内存中清理掉了,也就是虚拟机栈不存在垃圾回收
代码里创建的对象一般就两种:
1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被回收,对象在Eden区创建,如果Eden区的空间不足时,就会触发Minor GC,对新生代进行垃圾回收,把存活的对象移动到from区(S0),对象年龄记为1,如果Eden区再次满了,会再次触发Minor GC,对Eden区和From区进行垃圾回收,把存活的对象移动到to区,from来的对象年龄记为2,Eden区中记为1。。。。。。
2、一种是长期存活的,需要一直生活在Java堆中,让程序后续不停低使用,通过新生代S0区和S1区来回被垃圾回收15次后,进入堆内存的老年代中,这里的15次也称为对象年龄
具体的过程(这是一种通用情况):
1、new的对象先放伊甸园区。此区有大小限制。
2、当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。
3、然后将伊甸园中的剩余对象移动到幸存者0区。
4、如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5、如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6、啥时候能去养老区呢?可以设置次数。默认是15次。可以设置新生区进入养老区的年龄限制,设置 JVM 参数:-XX:MaxTenuringThreshold=N 进行设置
7、在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
8、若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。
特殊情况:
1、如果来了一个新对象,先看看 Eden 是否放的下?
2、将对象放到老年区又有两种情况:
3、如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区
4、年龄15是默认,因为jdk8默认的垃圾收集器是并行收集器(Parallel GC),对于并发收集器(CMS)默认是6
5、动态年龄判断
使用这两个参数可以打印JVM默认的设置参数,第一个是简单版,第二个是详细版
-XX:+PrintFlagsFinal
-XX:+PrintCommandLineFlags
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升到老年代
结论:动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄时的总和大于50%(可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认是50),那么比X大的都会被晋升到老年代;
1、Survivor区域分布如下
1—3岁总和小于50%
2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区内存分配如下:
这时,从1—4岁,总和51%大于50%,但是此时没有大于4岁的对象,即不会发生晋升
3、又经历过一次新生代GC后,有40%的对象进入Survivor区,Survivor分布如下图:
Survivor区的对象年龄从小到大进行累加,当累加到年龄3时的总和大于50%,那么比3大的都会晋升到老年代。即4岁的20%,5岁的20%会晋升到老年代
如果创建new User()时,Eden区的空间不够(S区也放不下)了,就会进行一次新生代的垃圾回收(Minor GC),那么如果开启了空间分配担保,那么就会触发下面这个过程
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
老年代空间担保的目的就是避免频繁地发生Full GC
1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;
2、动态对象年龄判断;
3、老年代空间担保机制;
4、大对象直接进入老年代;
大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;
我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS的收集器组合;
1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;
2、元空间与Java堆类似,是线程共享的内存区域;
3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;
4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;
-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m
5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;
6、元空间内存不足时,将抛出OutOfMemoryError;
方法区,内部包含了运行时常量池
字节码文件,内部包含了常量池。
常量池中有啥?
首先明确:只有Hotspot才有永久代。BEA JRockit、IBMJ9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一
这个静态变量是指变量名,实体要具体分析,但是new 出来的都是在堆上
1、为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。
2、对永久代进行调优是很困难的。方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再用的类型,方法区的调优主要是为了降低Full GC
1、JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。
2、这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
为什么 JDK9 改变了 String 的结构
节约了一些空间
// 之前
private final char value[];
// 之后
private final byte[] value
同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改
先说结论
1、常量与常量的拼接结果在常量池,原理是编译期优化
2、常量池中不会存在相同内容的变量
3、拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
4、如果拼接的结果调用intern()方法,根据该字符串是否在常量池中存在,分为:
但是intern() 在jdk6和jdk7、8中有区别,看下面那个有点难的面试题
1、intern是一个native方法,调用的是底层C的方法
2、字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。(这是源码里的大概翻译)
3、如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会4、从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。比如:
String myInfo = new string(“I love atguigu”).intern();
5、也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
(“a”+“b”+“c”).intern()==“abc”
6、通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
new String(“ab”)会创建几个对象?
/**
* 题目:
* new String("ab")会创建几个对象?看字节码,就知道是两个。
* 一个对象是:new关键字在堆空间创建的
* 另一个对象是:字符串常量池中的对象"ab"。 字节码指令:ldc
*
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("ab");
}
}
new String(“a”) + new String(“b”) 会创建几个对象?
/** * 思考: * new String("a") + new String("b")呢? * 对象1:new StringBuilder() * 对象2: new String("a") * 对象3: 常量池中的"a" * 对象4: new String("b") * 对象5: 常量池中的"b" * * 深入剖析: StringBuilder的toString(): * 对象6 :new String("ab") * 强调一下,toString()的调用,在字符串常量池中,没有生成"ab" * */ public class StringNewTest { public static void main(String[] args) { String str = new String("a") + new String("b"); } }
** * 如何保证变量s指向的是字符串常量池中的数据呢? * 有两种方式: * 方式一: String s = "shkstart";//字面量定义的方式 * 方式二: 调用intern() * String s = new String("shkstart").intern(); * String s = new StringBuilder("shkstart").toString().intern(); * */ public class StringIntern { public static void main(String[] args) { String s = new String("1"); s.intern();//调用此方法之前,字符串常量池中已经存在了"1" String s2 = "1"; System.out.println(s == s2);//jdk6:false jdk7/8:false /* 1、s3变量记录的地址为:new String("11") 2、经过上面的分析,我们已经知道执行完pos_1的代码,在堆中有了一个new String("11") 这样的String对象。但是在字符串常量池中没有"11" 3、接着执行s3.intern(),在字符串常量池中生成"11" 3-1、在JDK6的版本中,字符串常量池还在永久代,所以直接在永久代生成"11",也就有了新的地址 3-2、而在JDK7的后续版本中,字符串常量池被移动到了堆中,此时堆里已经有new String("11")了 出于节省空间的目的,直接将堆中的那个字符串的引用地址储存在字符串常量池中。没错,字符串常量池 中存的是new String("11")在堆中的地址 4、所以在JDK7后续版本中,s3和s4指向的完全是同一个地址。 */ String s3 = new String("1") + new String("1");//pos_1 s3.intern(); String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址 System.out.println(s3 == s4);//jdk6:false jdk7/8:true } }
1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;
2、像在JDK 1.4中新加入了NIO(New Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;
3、可能导致OutOfMemoryError异常出现; netty
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I / O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;
由直接内存导致的内存溢出,无法生成Heap Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:/dev/heapdump.hprof
-Xms Java堆内存的大小;
-Xmx Java堆内存的最大大小;
-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;
-XX:MetaspaceSize 元空间大小;
-XX:MaxMetaspaceSize 元空间最大大小;
-Xss 每个线程的栈内存大小;
-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;
-XX:MaxTenuringThreshold=5 年龄阈值;
-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;
-XX:+UseG1GC 指定使用G1垃圾回收器
–查看默认的堆大小及默认的垃圾收集器
java -XX:+PrintCommandLineFlags -version
lucene给我们提供了一个工具方法可以计算;
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.0.0</version>
</dependency>
@Data public class Order { private int id; private String name; private BigDecimal money; private byte[] bytes = new byte[1024 * 1024]; //1024kb = 1m public static void main(String[] args) throws IOException { Order order = new Order(); //计算指定对象及其引用树上的所有对象的综合大小,单位字节 long size = RamUsageEstimator.sizeOf(order); //计算指定对象及其引用树上的所有对象的综合大小,返回可读的结果,如:2KB String humanSize = RamUsageEstimator.humanSizeOf(order); System.out.println(size); System.out.println(humanSize); } }
因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;
所以需要分成两个区域来放不同的对象;
1、绝大多数对象都是朝生夕灭的;
如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;
2、熬过越多次垃圾收集的对象就越难以回收;
如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;
3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域 ,同时也有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;
Minor GC/Young GC :新生代收集
Major GC/Old GC:老年代收集
Full GC:整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;
Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;
4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:
(1)复制算法
(2)标记-清除算法
(3)标记-整理算法
1、如果没有Survivor区会怎么样?
此时每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;
2、如果只有1个Survivor区会怎么样?
刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;
如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;
如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;
一个eden区 ,新生代对象出生的地方;
两个survivor区,一个用来保存上次新生代GC存活下来的对象,还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;
统计和经验表明,90%的对象朝生夕死存活时间极短,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存;
这两个算法都是标记阶段使用的算法,但是JAVA没有采用引用计数算法,主要是引用计数算法解决不了循环依赖的问题
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
finalize()只会被调用一次。
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
标记-清除算法的缺点
空闲列表
何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放(也就是覆盖原有的地址)。
关于空闲列表是在为对象分配内存的时候提过:
如果内存规整
采用指针碰撞的方式进行内存分配
如果内存不规整
虚拟机需要维护一个空闲列表
采用空闲列表分配内存
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收
优点:实现简单,效率高,解决了标记-清除算法导致的内存碎片问题;
缺点:
代价太大,将可分配内存缩小了一半,空间浪费太多了;
不适合存活对象多的场景,对象存活率较高时就要进行较多的复制操作,效率将会降低;因为复制算法常用于新生代的垃圾回收
一般虚拟机都会采用该算法来回收新生代,但是JVM对复制算法进行了改进,JVM并没有按照1:1的比例来划分新生代的内存空间,因为通过大量的统计和研究表明,90%以上的对象都是朝生夕死的,所以JVM把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有另外一个Survivor空间即10%的新生代会被“浪费”;
当然,90%的对象可被回收仅仅是大部分情况下,我们无法百分百保证每次回收都只有不多于10%的对象存活,因此JVM还有一个空间担保机制的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其它内存区域(实际上就是老年代)进行空间分配担保(Handle Promotion,也就是冒险Minor GC一下);
标记-整理算法是根据老年代的特点而产生的;
1、标记
标记过程与上面的标记-清理算法一致,也是基于可达性分析算法进行标记;
2、整理
和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存;
而标记-清除算法不移动存活对象,导致有大量不连续空间,即内存碎片,而老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的引用,这是一种比较耗时的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿我们也称为“Stop The World”即STW;
但是即便是移动存活对象是耗时的操作,但是如果不这么做,那么在充满内存碎片的空间中分配对象,又影响了对象的分配和访问的效率,所以JVM权衡两者之后,还是采用了移动存活对象的方式,也就是对内存进行了整理;
另外像cms垃圾收集器,平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间,所以像基于标记-清除算法的CMS收集器面临空间碎片过多时就会进行一次整理;
优点:
1、不会像复制算法那样划分两个区域,提高了空间利用率;
2、不会产生不连续的内存碎片;
缺点:效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;
现在一般虚拟机的垃圾收集都是采用“ 分代收集 ”算法;
根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;
新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;
上述现有的算法,在垃圾回收过程中,应用软件将处于一种Stop the World的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。
增量收集算法基本思想
1、如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
2、总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
增量收集算法的缺点
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
主要针对G1收集器来说的
1、一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
2、分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
1、在默认情况下,通过System.gc()者Runtime.getRuntime().gc() 的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
2、然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)
3、JVM实现者可以通过System.gc() 调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
System.gc();//提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc
//与Runtime.getRuntime().gc();的作用一样。
// System.runFinalization();//强制调用使用引用的对象的finalize()方法
}
//如果发生了GC,这个finalize()一定会被调用
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重写了finalize()");
}
}
输出结果不确定:有时候会调用 finalize() 方法,有时候并不会调用
SystemGCTest 重写了finalize()
或
空
1、 Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
2、可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿,为什么需要停顿所有 Java 执行线程呢
3、被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
4、STW事件和采用哪款GC无关,所有的GC都有这个事件。
5、哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
6、STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
7、开发中不要用System.gc() ,这会导致Stop-the-World的发生。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
如ParNew、Parallel Scavenge、Parallel Old
串行(Serial)
相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收(单线程)
并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
1、并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
2、典型垃圾回收器:CMS、G1
1、安全点(Safepoint)
2、如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
3、安全区域(Safe Region)
4、安全区域的执行流程
1、一般的垃圾回收算法至少会划分出两个年代,年轻代和老年代。但是单纯的分代理论在垃圾回收的时候存在一个巨大的缺陷:为了找到年轻代中的存活对象,却不得不遍历整个老年代,反过来也是一样的。
2、、如果我们从年轻代开始遍历,那么可以断定N, S, P, Q都是存活对象。但是,V却不会被认为是存活对象,其占据的内存会被回收了。这就是一个惊天的大漏洞!因为U本身是老年代对象,而且有外部引用指向它,也就是说U是存活对象,而U指向了V,也就是说V也应该是存活对象才是!而这都是因为我们只遍历年轻代对象!
3、所以,为了解决这种跨代引用的问题,最笨的办法就是遍历老年代的对象,找出这些跨代引用来。这种方案存在极大的性能浪费。因为从两个分代假说里面,其实隐含了一个推论:跨代引用是极少的。也就是为了找出那么一点点跨代引用,我们却得遍历整个老年代!从上图来说,很显然的是,我们根本不必遍历R。
4、因此,为了避免这种遍历老年代的性能开销,通常的分代垃圾回收器会引入一种称为记忆集的技术。简单来说,记忆集就是用来记录跨代引用的表。
1、为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题,因此我们有必要进一步 理清记忆集的原理和实现方式,以便在后续介绍几款最新的收集器相关知识时能更好地理解
2、记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。如果我们不考虑效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构。
比如说我们有老年代(非收集区域)和年轻代(收集区域)的对象之间有一条引用链
3、这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾 收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针 就可以了,并不需要了解这些跨代指针的全部细节。
4、其中,第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,这也是 目前最常用的一种记忆集实现形式,一些资料中甚至直接把它和记忆集混为一谈。前面定义中提到记 忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的 具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,读者不妨按照Java语言中HashMap与Map的关系来类比理解。 卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的
1、两个收集器间有连线,表明它们可以搭配使用
新生代收集器:Serial、ParNew、Parallel Scavenge [ˈpærəlel] [ˈskævɪndʒ]
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
垃圾收集器的最前沿成果:ZGC 和 Shenandoah
2、其中Serial Old作为CMS出现”Concurrent Mode Failure”失败的后备预案。
3、(红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
4、(绿色虚线)JDK14中:弃用Parallel Scavenge和Serial Old GC组合(JEP366)
5、(青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)
为什么要有很多收集器,一个不够吗?因为Java的使用场景很多,移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。
查看默认垃圾收集
-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID
JDK8默认是Parallel Scavenge/Parallel Old
JDK9默认是G1
1、Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
2、Serial收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。
3、Serial收集器采用复制算法、串行回收和”Stop-the-World”机制的方式执行内存回收。
4、除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial old收集器同样也采用了串行回收和”Stop the World”机制,只不过内存回收算法使用的是标记-压缩算法。
5、Serial Old是运行在Client模式下默认的老年代的垃圾回收器,Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用②作为老年代CMS收集器的后备垃圾收集方案
这个收集器是一个单线程的收集器,“单线程”的意义:它只会使用一个CPU(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
Serial 回收器的优势
1、优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。运行在Client模式下的虚拟机是个不错的选择。
2、在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
3、在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。
对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java Web应用程序中是不会采用串行垃圾收集器的。
它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,单CPU下,
ParNew还需要切换线程,可能还不如Serial;
Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads=2”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
1、HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和”Stop the World”机制。
2、那么Parallel收集器的出现是否多此一举?
3、高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
4、Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。
5、Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和”Stop-the-World”机制。
Parallel Scavenge 回收器参数设置
1、-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
2、-XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器。
分别适用于新生代和老年代
上面两个参数分别适用于新生代和老年代。默认jdk8是开启的。默认开启一个,另一个也会被开启。(互相激活)
3、-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
在默认情况下,当CPU数量小于8个,ParallelGCThreads的值等于CPU数量。
当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU_Count]/8]
4、-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
5、-XX:GCTimeRatio垃圾收集时间占总时间的比例,即等于 1 / (N+1) ,用于衡量吞吐量的大小。
取值范围(0, 100)。默认值99,也就是垃圾回收时间占比不超过1。
与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,STW暂停时间越长,Radio参数就容易超过设定的比例
6、-XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
1、在JDK1.5时期,Hotspot推出了一款在强交互应用中(就是和用户打交道的引用)几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
2、CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
3、CMS的垃圾收集算法采用标记-清除算法,并且也会”Stop-the-World”
4、不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作(因为实现的框架不一样,没办法兼容使用),所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个
5、在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
CMS 工作原理
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记)
1、初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
2、并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长 ,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
3、重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-the-World”的发生,但也远比并发标记阶段的时间短。
4、并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS分析
1、尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World”,只是尽可能地缩短暂停时间。
2、由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
3、另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
4、CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
为什么 CMS 不采用标记-压缩算法呢?
答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“stop the world”这种场景下使用
CMS 的优点与弊端
优点
弊端
1、会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
2、CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
3、CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
CMS 参数配置
-XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收任务。
开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+CMS(Old区)+Serial Old(Old区备选方案)的组合。
-XX:CMSInitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
CMS默认启动的线程数是 (ParallelGCThreads + 3) /
4,ParallelGCThreads是年轻代并行收集器的线程数,可以当做是 CPU
最大支持的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
JDK 后续版本中 CMS 的变化
G1全称Garbage First,G1垃圾回收器可以同时回收新生代和老年代,不需要两个垃圾回收器配合起来使用;
G1垃圾收集器是目前可用于生产环境的最前沿最先进的垃圾收集器,从JDK1.6u14开始试验,到JDK1.7u4达到成熟,直到JDK1.8u40才正式完成,开始可以使用;
JDK 9发布时,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则被声明为不推荐使用(Deprecate)的收集器,如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃,CMS后续将退出历史舞台;
G1垃圾收集器的基本原理
G1是一款可以让我们设置垃圾回收的预期停顿时间的垃圾收集器,设置参数是-XX:MaxGCPauseMillis
,默认值是200ms
;
其实我们对内存合理分配,优化jvm参数,就是为了尽可能减少新生代(Minor GC),或者是整个老年代(Major GC),或者是整个Java堆(Full GC),尽量减少GC带来的系统停顿,避免影响系统处理请求,G1可以指定垃圾回收导致的系统停顿时间不能超过多久,不管垃圾的多与少,垃圾回收的时间都不要超过我们设置的值(并不是绝对的),G1全权给你负责,保证达到这个目标,这相当于我们就可以直接控制垃圾回收对系统性能的影响了;
所以G1垃圾收集器是尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是G1垃圾收集器的核心原理;
1、这与G1垃圾收集器独特的设计有关,它最大的特点就是把Java整个堆内存拆分为多个大小相等的Region [ˈridʒən] ;区域、分区
2、 G1它会追踪每个Region的回收价值,即它会计算每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?
3、G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为垃圾回收的最小单元,即每次可以选择一部分Region进行收集,避免在整个Java堆中进行全区域的垃圾收集,让G1收集器去跟踪各个Region里面的垃圾的“回收价值”,然后根据用户设定的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),然后在后台维护一个优先级列表,优先处理回收价值大的那些Region,这也是“Garbage First”名字的由来,这种使用Region划分堆内存空间,基于回收价值的回收方式,保证了G1收集器在有限的时间内尽可能收集更多的垃圾;
比如:G1通过追踪发现,1个Region中的垃圾对象有10MB,回收它需要耗费500毫秒,另一个Region中的垃圾对象有20MB,回收它需要耗费100毫秒,那么G1垃圾收集器基于回收价值计算会选择回收20MB只需要100毫秒的Region;
G1也有新生代和老年代的概念,但只不过是逻辑上的概念,也就是说一个Region此时是属于新生代的Eden空间,过一会儿可能就属于老年代空间,也就是一个Region在运行过程中动态地扮演着新生代的Eden空间、Survivor空间,或者老年代空间,每个Region并不是固定属于某一个空间,另外新生代、老年代也不一定是连续空间,可能是分开的;
刚开始Region是空的,可能谁都不属于,然后系统创建对象就分配给了新生代,这个Region被新生代对象放满之后,后续垃圾回收了这个Region,然后下一次同一个Region可能又被分配了老年代,用来放老年代的长时间需要存活的对象,所以Region随时会属于新生代也会属于老年代;
新生代和老年代各自的内存区域在不停地变动,由G1自动控制,也就是Region动态分配给新生代或者老年代,按需分配,然后触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象;
Region中有一类特殊的Humongous [hjuːˈmʌŋgəs]区域,专门用来存储大对象;
G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB,而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待;
每个region = 1m ~32m,最多有2048个region;
G1对应的是一大堆的Region内存区域,最多可以有2048个Region,比如说堆大小是4G(4096MB),那么每个Region的大小就是2MB,Region的取值范围是1M-32M,可以通过参数“-XX:G1HeapRegionSize”
指定每个Region是多少兆;
比如说堆大小是4G(4096MB),刚开始,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent
”来设置新生代初始占比,一般默认值即可,因为在系统运行中,JVM会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent
”设置,并且一旦Region进行了垃圾回收,此时新生代的Region数量就会减少,这些都是动态的;
新生代 :老年代 = 60% :40%
G1垃圾收集器依然有新生代、老年代的概念,新生代里依然有Eden和Survivor的划分,G1是从CMS发展过来的,以后是要完全取代CMS垃圾收取器的,从jdk9开始G1已经是默认的垃圾收集器,之前的很多技术原理在G1中依然可用,我们知道新生代有一个参数“-XX:SurvivorRatio=8
”,所以G1还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor;
比如新生代刚开始初始化时有100个Region,那么可能有80个Region是Eden,10个Region分别是两个Survivor,所以G1中依然有Eden和Survivor的概念,它们会各自占据不同的Region;
只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加;
5% – 60%
G1的新生代也有Eden和Survivor,其触发垃圾回收的机制也是类似的,随着不停在新生代Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%;
假设堆 4G,最大2048个region,每个region为2M,新生代最大60%=2.4G;
一旦新生代达到了设定的占据堆内存的最大大小60%,按照上面的数据大概就是有1200个Region,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区满了,此时触发新生代的GC,G1就会依然用复制算法来进行垃圾回收**,进入一个“Stop the World”状态**,然后把Eden对应的Region中的存活对象复制到S0对应的Region中,接着回收掉Eden对应的Region中的垃圾对象;
但这个过程与之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills
”参数来设定,默认值是200ms,那么G1就会通过对每个Region追踪回收它需要多少时间,可以回收多少对象来选择回收一部分Region,保证GC停顿时间控制在指定范围内,尽可能多地回收对象;
1、初始标记,需要Stop the World,不过仅仅标记一下GC Roots直接能引用的对象,这个过程速度很快,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿;
在堆新生代进行垃圾回收的时候,顺手就做了老年代的初始标记
2、并发标记,不需要Stop the World,这个阶段会从GC Roots开始追踪所有的存活对象,初始标记阶段仅仅只是标记GC Roots直接关联的对象,而在并发标记阶段,就会进行GC Roots追踪,从这个GC Root对象直接关联的对象开始往下追踪,追踪全部的存活对象,这个阶段是很耗时的,但可以和系统程序并发运行,所以对系统程序的影响不大;
3、重新标记(最终标记),需要Stop the World,用户程序停止运行,最终标记一下有哪些存活对象,有哪些是垃圾对象;
4、筛选回收,需要Stop the World,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的;
从整体上看,G1垃圾收集像是一种标记-整理算法,它不存在内存碎片问题,实际上它是一种复制算法,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 所以它并不是纯粹地追求低延迟,而是给它设定暂停目标,使其在延迟可控的情况下获得尽可能高的吞吐量;
混合垃圾收集即mixed gc,它不是一个old gc,除了回收整个young region,还会回收一部分的old region,是回收一部分老年代,而不是全部老年代,可以选择部分old region进行收集,从而可以对垃圾回收的耗时时间进行控制;
G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,它的默认值是45%,即如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段;
比如堆内存有2048个Region,如果老年代占据了其中45%的Region,也就是接近1000个Region的时候,就会开始触发一个混合回收;
在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,把各个Region中存活的对象复制到其他空闲的Region中;
如果万一出现复制时没有空闲Region可以存放存活对象了,就会停止系统程序,然后采用单线程进行标记清除和压缩整理,空闲出来一批Region,这个过程很慢;
与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫暂停用户线程,导致Full GC而产生长时间Stop The World;
1、新生代垃圾回收
2、老年代垃圾回收
3、混合回收
4、Full GC
-XX:+UseG1GC
1、针对大内存、多处理器的机器推荐采用G1垃圾收集器,比如堆大小至少6G或以上;
2、 超过50%的堆空间都被活动数据占用;
3、在要求低延迟的场景,也就是GC导致的程序暂停时间要比较少,0.5-1秒之间;
G1整体效率上比CMS好
4、对象在堆中分配频率或者年代升级频率变化比较大,防止高并发下应用雪崩现象的场景;
-XX:+UseZGC
ZGC(Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器,它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器,在JDK 11新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms;
Shenandoah作为第一款不由Oracle(包括Sun)公司的虚拟机团队所领导开发的HotSpot垃圾收集器,Oracle官方明确拒绝在 OracleJDK12 中支持 Shenandoah 收集器,它是一款只有OpenJDK才会包含的收集器,最初由RedHat公司创建的,在2014年的时候贡献给了OpenJDK,Shenandoah收集器能实现在任何堆内存大小下都把垃圾停顿时间限制在10ms以内;
内存溢出:OutOfMemory
它是指程序在申请内存时,没有足够的内存空间供其使用,抛出OutOfMemory错误;
比如申请了一个8MB空间,但是当前内存可用空间只有5MB,那么就是内存溢出;
即:OutOfMemoryError,是指没有空闲内存,垃圾收集器回收后也不能提供更多的内存空间;
内存泄露:Memory Leak
它是指程序运行后,没有释放所占用的内存空间,一次内存泄漏可能不会有很大的影响,但长时间的内存泄漏,堆积到一定程度就会产生内存溢出;
(1)单例对象,生命周期和应用程序一样长,如果单例对象持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会产生内存泄露;
(2)一些资源未闭也会导致内存泄漏,比如数据库连接,网络连接socket和IO流的连接都必须在finally中close,否则不能被回收的;
线上:4核8G机器;
JVM:栈、堆、元空间;
1、栈 1m,xss512k,一个线程是1m,一个线上项目Tomcat可能有300个线程,300m;
2、堆:大概把机器的一半内存给堆,4G(新生代、老年代);
CMS:1/3 、2/3 G1: 6:4
3、元空间: 一般512M肯定够了;
此时JVM参数如下:
-Xms4096M -Xmx4096M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:+UseG1GC
jmap -histo pid
jmap -heap pid
jmap -dump:format=b,file=heap.hprof pid
top
top -H -p pid
printf ‘%x’ tid 8ef
jstack pid
具体情况具体分析
@RestController public class OOMController { //List List<Order> orderList = new ArrayList<Order>(); @GetMapping("/hello") public Object hello(@RequestParam(required = false) String name) { List<Order> list = new ArrayList<Order>(); for (int i=0; i<15; i++) { list.add(new Order()); } return "hello " + name; } /** * 堆溢出 * * @return */ @RequestMapping("/heap") public Object heap() { //死循环 for (;;) { //Order Order order = new Order(); order.setId(1); order.setMoney(new BigDecimal(990)); order.setName("支付订单"); //放入List orderList.add(order); System.out.println("orderList: " + orderList.size()); } } }
OOMController 在Spring容器中中是单例对象,单例对象的声明周期和进程是一致的,所以不停地往orderList 放对象,迟早是要OOM的,也就是堆内存迟早要满,如果你访问得请求只是简单地返回一个String,不涉及任何查库,或者其他接口的调用,线程是可以继续工作,但是会处理很慢,如果涉及一些查库,或者请求中需要创建对象,那线程是不可以正常工作的,会报内存溢出OOM
但是上面的代码稍作修改。。。。。
@RestController public class OOMController { @GetMapping("/hello") public Object hello(@RequestParam(required = false) String name) { List<Order> list = new ArrayList<Order>(); for (int i=0; i<15; i++) { list.add(new Order()); } return "hello " + name; } /** * 堆溢出 * * @return */ @RequestMapping("/heap") public Object heap() { List<Order> orderList = new ArrayList<Order>(); //死循环 for (;;) { //Order Order order = new Order(); order.setId(1); order.setMoney(new BigDecimal(990)); order.setName("支付订单"); //放入List orderList.add(order); System.out.println("orderList: " + orderList.size()); } } }
把orderList 放到方法内,运行后。。。
所以这种场景其他请求是不受影响的
解释如下:
第一个代码中orderList 在成员变量中,Spring创建初始化创建单例对象的时候,会随单例对象分配到堆中,因为单例对象是常驻堆内存的,你可以把orderList 理解为一个Gc Root,它通过强引用指向一块数组(ArrayList的底层是数组),在访问/heap时,不断地创建对象往数组中填,当新生代的内存放不下了,就会放到老年代,直至内存溢出,但是即使溢出了orderList 指向的那块内存始终是没释放的,这是因为orderList 这个Gc Root一直在堆中。
第二个代码,orderList 作为局部变量是分配到了虚拟机栈上的栈帧中(局部变量表),它也是作为GCRoot,因为只要方法一直执行,也是会溢出的,但是后方法报错,执行结束栈帧出栈,orderList 也随栈帧出战,那么它指向的那块数组就变成了不可达对象,此时是可以被GC回收的,所以你看两种情况下监控器的内存动态是不一样的。请求自然是可以被访问处理的。
-Xms256M
-Xmx256M
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/heap.hprof
每个用户访问20次/天,500万日活用户,流量 = 500万 * 20 = 10000万 = 1亿;
购买率15%,每人1单,每天订单量 = 500万 * 15% * 1 = 75万订单/天;
二八原则,下单集中在一天4小时内,洪峰下单量 = 75万 / 4小时 = 18.75万单/小时
= 18.75万单/60分/60秒 = 52单/秒;
52k2010=52*200=10400 = 10MB/秒,每秒52单基本上JVM没有压力;
流量洪峰场景
普通4核8G服务器,一台机器抗300-400并发下单请求比较合理;
583000 / 300 = 1943台机器
300KB * 20 * 10 = 60MB的内存开销,一秒后60MB对象就成为垃圾;
内存分配
4核8G的机器,JVM给4G,剩下几个G会留给操作系统;
堆3G(新生代1.5G,老年代1.5G)
栈1MB,JVM里大概会有300-500个线程,大概300-500MB;
元空间/永久代512MB;
“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M
-XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M”
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/heap.hprof
内存占用动态推算
每秒处理300个订单,占据新生代60MB的内存空间,新生代总共有1.5G的内存空间;
1.5G * 1024MB / 60MB = 25秒 新生代Eden占满,触发Minor GC;
一般情况下一次可以回收掉90%的新生代对象,存活对象 = 1.5G * 1024MB * 10% = 150MB;
如果“-XX:SurvivorRatio”参数默认值为8,那么:
新生代Eden=1.2GB、S0 = 150MB、S1 = 150MB
如何调优?
(1)、1次Minor GC后,可能Survivor不足或者触发动态年龄判断,对象进入老年代,明显是Survivor空间不足;
新生代调整为2G,老年代为1G,此时Eden:1.6G,每个Survivor:200MB;
解决可能的Survivor不足或者触发动态年龄判断,降低新生代对象进入老年代的概率;
此时JVM参数:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M”
(2)、一般系统里的@Service、@Controller之类的注解需要长期存活,这些对象一般也不会很多,可能几十兆,应该让它们尽快进入老年代;
此时JVM参数:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5”
(3)、 一般情况下,大对象可能需要长期存活和使用,让它直接进入老年代;(根据项目实际情况来确定)
此时JVM参数如下:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M”
(4)指定合适的垃圾回收器;
此时JVM参数 :
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC”
没有使用G1垃圾收集器;
(5) 大概每隔几分钟Minor GC之后有大概200MB左右对象进入老年代,推算可能差不多1小时后,才会有接近1GB的对象进入老年代,触发Full GC,然后高峰期一过,可能需要几个小时才会一次Full GC;
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:-UseCompressedClassPointers -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
优化思路
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。