学习JVM
JVM
JVM运行机制
内存结构
堆 | 方法区 | 栈 | 本地方法栈 | PCR | |
---|---|---|---|---|---|
存放对象 | 实例化对象,分为年轻代、老年代、永久代 | 常量、静态变量、类信息、JIT后代码 | 栈帧(局部变量表、操作数栈、动态链接、方法出口) | 本地Native方法 | 存放当前线程执行的字节码的位置指示器 |
私有/共享 | 共享 | 共享 | 私有 | 私有 | 私有 |
异常 | OutOfMemoryError | OutOfMemoryError | StackOverflowError | StackOverflowError | 不会抛出异常 |
调参 | -Xms、-Xmx、-Xmn | -XX:MetaspaceSize | -Xss |
堆
年轻代:占堆的1/3。分类如下
堆分类 | 解释 | 补充 |
---|---|---|
Eden | Java新创建的对象,如果新对象属于大对象,直接放到老年代 | 调整老年代对象大小:XX:PretenureSizeThreshold |
SurvivorFrom | 将上一次MinorGC的幸存者作为这一次MinorGC的扫描对象 | |
SurvivorTo | 上一次MinorGC的幸存者,对象晋升为老年代次数默认15次 | 调整晋升为老年代次数:XX:MaxTenuringThreshold |
老年代:占堆的2/3
永久代:存放永久的Class和Meta数据,不会发生垃圾回收
如何确定垃圾
引用计数法 | GC Roots | |
---|---|---|
优点 | 简单 | 不会产生循环依赖问题 |
缺点 | 无法解决对象之间的循环依赖问题 | 比引用计数法复杂 |
对象 | 堆中存在的对象 | 栈引用的对象、方法区中的静态引用、JNI中的引用 |
垃圾回收算法
名称 | 优点 | 缺点 |
---|---|---|
复制算法 | 最简单高效,不会产生碎片。年轻代默认算法 | 利用率低下,只有一半 |
标记清除算法 | 利用率较高 | 效率低+空间碎片问题 |
标记整理算法 | 解决了空间碎片问题 | 效率还是不高 |
分代收集算法 | 新生代用复制算法;老年代用后两者结合算法 | 并不是一种算法,而是一种思想 |
标记清除算法
复制算法
标记整理算法
分代收集算法
四种引用状态
强引用:普通存在, P p = new P()
,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。是造成内存泄露的主要原因。
软引用:通过SoftReference
类来实现软引用,在内存不足的时候会将这些软引用回收掉。
弱引用:通过WeakReference
类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference
类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。
垃圾收集器
生产环境中常用的GC收集器:新生代ParNewGC,老年代CMSGC
名称 | 周期 | 算法 | 特点 |
---|---|---|---|
Serial收集器 | 新生代 | 单线程复制算法 | 1.单线程收集器;2.缺点是STW |
ParNew收集器 | 新生代 | 多线程复制算法 | Serial多线程版本 |
Parallel Scavenge收集器 | 新生代 | 多线程复制算法 | 1.吞吐量优先;2.自适应调节吞吐量策略;3.多线程并行收集 |
Serial Old收集器 | 老年代 | 单线程标记整理算法 | Serial老年代版本 |
Parallel Old收集器 | 老年代 | 多线程标记整理算法 | Parallel Scavenge老年代版本 |
CMS收集器 | 老年代 | 多线程标记清除算法 | 并发收集、低停顿、缺点还是会产生空间碎片 |
G1收集器 | 老年代 | 标记整理+清除算法 | 并行与并发、分代收集、空间整理、可预测停顿 |
Serial收集器
ParNew收集器
Parallel Scavenge收集器
Serial Old收集器
ParallelOld收集器
CMS收集器
概念:一种以获取最短回收停顿时间为目标的GC。CMS是基于标记-清除算法实现的,是一种老年代收集器,通常与ParNew一起使用。
CMS的垃圾收集过程分为5步:有4步的说法,5步的说法,7步的说法,这里按照5步的说法
初始标记:需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
并发标记:是主要标记过程,这个标记过程是和用户线程并发执行的。
如果在重新标记之前刚好发生了一次MinorGC,会不会导致重新标记阶段Stop the World时间太长?
答:不会的,在并发标记阶段其实还包括了一次并发的预清理阶段,虚拟机会主动等待年轻代发生垃圾回收,这样可以将重新标记对象引用关系的步骤放在并发标记阶段,有效降低重新标记阶段Stop The World的时间。
重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。
并发清除:和用户线程并发执行的,基于标记结果来清理对象。
并发重置:重置CMS的数据结构,等待下一次垃圾回收,与用户线程同时运行
CMS优缺点:
CMS优点 | CMS缺点 |
---|---|
并发收集,停顿时间低 | 对CPU资源非常敏感;收集过程中会产生浮动垃圾;标记-清除方式会产生内存碎片 |
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
CMS时特殊情况:concurrent-mode-failure
现象说明:在 CMS GC 过程中,如果在并行清理的过程中老年代的空间不足以容纳应用产生的垃圾,则会抛出 “concurrent mode failure”。
影响:老年代的 CMS GC 会转入 STW 的串行,所有应用线程被暂停,停顿时间变长。
可能的原因及解决方案:
- 老年代使用太多时才触发 CMS GC,可以调整
- XX:CMSInitiatingOccupancyFraction=N
,告诉虚拟机当 old 区域的空间上升到 N% 的时候就开启 CMS; - CMS GC 后空间碎片太多,可以加上
- XX:+UseCMSCompactAtFullCollection
和-XX:CMSFullGCsBeforeCompaction=n
参数,表示经过 n 次 CMS GC 后做一次碎片整理。 - 垃圾产生速度超过清理速度(比如说新生代晋升到老年代的阈值过小、Survivor 空间过小、存在大对象等),可以通过调整对应的参数或者关注程序代码来解决。
G1收集器
JVM常用调参
public static void main(String[] args) { String JAVA_OPTS = "-Xms4096m –Xmx4096m " + "-XX:NewRatio=2 -XX:SurvivorRatio=8 -Xloggc:/home/work/log/serviceName/gc.log -XX:+PrintGCDetails " + "-XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC -XX:+UseParNewGC" + "-XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=10 "; }
JVM 参数 | 说明 |
---|---|
Xms | 初始堆大小 |
Xmx | 最大堆大小 |
Xmn | 年轻代大小 |
Xss | 每个线程的堆栈大小 |
MetaspaceSize | 首次触发 Full GC 的阈值,该值越大触发 Metaspace GC 的时机就越晚 |
MaxMetaspaceSize | 设置 metaspace 区域的最大值 |
+UseConcMarkSweepGC | 设置老年代的垃圾回收器为 CMS |
+UseParNewGC | 设置年轻代的垃圾回收器为并行收集 |
CMSFullGCsBeforeCompaction=5 | 设置进行 5 次 full gc(CMS)后进行内存压缩。由于并发收集器不对内存空间进行压缩 / 整理,所以运行一段时间以后会产生 "碎片",使得运行效率降低。此值设置运行多少次 full gc 以后对内存空间进行压缩 / 整理 |
+UseCMSCompactAtFullCollection | 在 full gc 的时候对内存空间进行压缩,和 CMSFullGCsBeforeCompaction 配合使用 |
+DisableExplicitGC | System.gc () 调用无效 |
-verbose:gc | 显示每次 gc 事件的信息 |
+PrintGCDetails | 开启详细 gc 日志模式 |
+PrintGCTimeStamps | 将自 JVM 启动至今的时间戳添加到 gc 日志 |
-Xloggc:/home/admin/logs/gc.log | 将 gc 日导输出到指定的 /home/admin/logs/gc.log |
+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs | 当堆内存空间溢出时输出堆的内存快照到指定的 /home/admin/logs |
类加载阶段
加载:JVM读取Class文件,根据Class文件描述创建Java.lang.Class对象的过程
验证:确保Class文件是否符合虚拟机的要求
准备:为类变量分配内空间并设置变量的初始值,初始值指不同数据类型的默认值,但是final和非final不一样
public class InitFinal { // 没有加final:value1在类初始化的“准备”阶段分配为int类型的默认值0,在“初始化”阶段才分配为10 private static int value1 = 10; // final表示:value2在类初始化的“准备”阶段分配为10 private static final int value2 = 10; }
解析:将常量池中的符号引用替换为直接引用
初始化:执行类构造器的<client>方法为类进行初始化,引出了下面的类初始化顺序的面试题</client>
类初始化阶段
- 父类的静态方法、静态代码块
- 子类的静态方法、静态代码块
- 父类被重写的静态方法,父类也要先执行;父类被重写的非静态方法,父类不执行
- 子类的重写父类的非静态方法
- 父类的非静态代码块、构造器
- 子类的非静态代码块、构造器
public class Father { // 这个方法被子类重写,类初始化是父类被重写的不执行,调到执行子类重写的方法 private int i = test(); private static int j = method(); // 2 静态代码块 static{ System.out.print("(1)"); } // 7 父类构造方法 Father(){ System.out.print("(2)"); } // 6 非静态代码块 { System.out.print("(3)"); } // 这个方法被子类重写,类初始化是父类被重写的不执行 public int test(){ System.out.print("(4)"); return 1; } // 1 执行静态方法 public static int method(){ System.out.print("(5)"); return 1; } }
public class Son extends Father{ // 8 父类类初始化完成,顺序执行子类非静态方法,又输出一遍9 private int i = test(); private static int j = method(); // 4 静态代码块 static { System.out.print("(6)"); } // 10 子类构造方法 Son() { System.out.print("(7)"); } // 9 子类非静态代码块 { System.out.print("(8)"); } // 5 被重写的非静态方法test方法 public int test() { System.out.print("(9)"); return 1; } // 3 静态方法 public static int method() { System.out.print("(10)"); return 1; } public static void main(String[] args) { // 实例化初始化过程1:包含子父类静态加载 new Son(); // 实例化初始化过程2:不包含所有的静态加载 new Son(); } }
执行结果:
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7) (9)(3)(2)(9)(8)(7)
类加载器
类加载机制:JVM通过双亲委派进行类的加载,当某个类加载器在接到加载类的请求时,将加载任务依次委托给上一级加载器,如果分类能加载,就父类加载;父类不能记载,再子类往下依次判断是否能加载
- 启动类加载器(bootstrapClassLoader):负责加载支撑JVM运行的位于JRE的
lib目录
下的核心类库,比如 rt.jar、charsets.jar等。底层是用C++书写,所以JVM输出为null。 - 扩展类加载器(extClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的
ext扩展目录
中的JAR 类包 - 应用类加载器(appClassLoader):用户
classpath
下自己写的类 - 自定义加载器(重写某些方法):负责加载用户
自定义路径
下的类包
public class ClassLoaderDemo { public static void main(String[] args) { ClassLoader bootstrapCL = String.class.getClassLoader(); System.out.println("启动类加载器:" + bootstrapCL); ClassLoader extCL = DESCipher.class.getClassLoader(); System.out.println("扩展类加载器:" + extCL); ClassLoader appCL = ClassLoaderDemo.class.getClassLoader(); System.out.println("应用类加载器:" + appCL); } }
执行结果:
启动类加载器:null// 启动类加载器调用底层c++,无返回值 扩展类加载器:sun.misc.Launcher$ExtClassLoader@873330 应用类加载器:sun.misc.Launcher$AppClassLoader@b4aac2
双亲委派机制
概念:加载某个类时会先找父亲加载,层层向上,如果都不行,再逐步向下由儿子加载。
双亲委派源码: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; } }
设计双亲委派机制的好处:
- 沙箱安全机制,保证安全性:比如自己写的String类不会被加载,防止JDK核心API不会被随意篡改
- 避免类的重复加载,保证唯一性:当父类加载过该类后,子类不会再加载,保证了被加载类的唯一性
自定义类加载器和打破双亲委派机制
自定义类加载器只需要extends ClassLoader
类,该类有两个核心方法:
loadClass(String, boolean)
,实现了双亲委派机制findClass()
,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
package 基础面试.JVM; import java.io.FileInputStream; import java.lang.reflect.Method; public class MyClassLoader { static class MyStaticCL extends ClassLoader { private String classPath; public MyStaticCL(String classPath) { this.classPath = classPath.replaceAll("\\.", "/"); } 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; } // 自定义类加载器:重写findClass @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); // 转换成class对象返回 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } // 打破双亲委派:重写loadClass @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); long t1 = System.nanoTime(); // ,否则使用双亲委派 if (!name.startsWith("基础面试.JVM")) { c = this.getParent().loadClass(name); } else { c = findClass(name); } 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 { String classpath = "E:\\product\\test"; // 指定类加载器:E:\product\test\基础面试\JVM下的user.class String userClass = "基础面试.JVM.User"; MyStaticCL classLoader = new MyStaticCL(classpath); Class<?> userClass1 = classLoader.loadClass(userClass); Object object = userClass1.newInstance(); Method method = userClass1.getDeclaredMethod("print", null); method.invoke(object, null); System.out.println("自定义加载器名字:" + userClass1.getClassLoader().getClass().getName()); } }
package 基础面试.JVM; public class User { private String userName ; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public void print(){ System.out.println("MyStaticCL加载的User.print方法"); } }
执行结果:
MyStaticCL加载的User.print方法 自定义加载器名字:基础面试.JVM.MyClassLoader$MyStaticCL