深入理解JVM 虚拟机的学习笔记
看了两遍的JVM许多地方还是似懂非懂
结合书本一些文章重新梳理一下
深入理解jvm这本书确实不错,但感觉有他的局限性。
整理一下核心
讲的很乱,没有重点。
jdk 1.7 和1.8的区别还是非常大的。
总的预览图
1.类的加载机制
类的加载机制老是不明白,有很多面试题喜欢考类的加载
真的就5个过程
-
加载
- 加载一个类首先通过全限定名找到这个类的二进制字节流
- 把字节流的静态结构转换成方法区的运行时结构
- 在内存中生成class对象,作为方法区访问的入口,比如反射需要class对象
-
验证
就是验证合理性包括
文件格式,版本号之类的
元数据,有不有不符合java语言规范的
字节码,保证一些转换的问题,指令是正确的
-
准备
初始化静态成员变量的值为默认值
-
解析
符号引用变成直接引用 简单来说就是确定指针
-
类和接口解析
解析C类,分两种情况
1.C 不是数组类型 可能需要加载父类和接口
2.C 是数组类型,且元素是对象 就先加载这对象如Integer
-
类方法解析
1.直接查名字,有就返回引用
2.没有 在父类中查
3.在接口和父类接口中查,有的话说明是抽象类
-
-
初始化
初始化成员变量和静态代码块
结合一下JAVA编程思想中的知识
执行顺序是
父类初始化> 子类初始化
静态变量静态代码块(同级的看书写顺序)初始化 > 普通成员变量(代码块) > 构造器
看看代码
package test; public class Father { //------- 这两个顺序只和书写顺序有关 private static String staticfiled = getStaticfiled(); static { System.out.println("父类->静态代码块"); } // ------- private String field =getNormalfiled(); public String getNormalfiled(){ System.out.println("父类->普通成员变量初始化"); return "hah"; } public static String getStaticfiled(){ System.out.println("父类->静态成员变量初始化"); return "haha"; } // 普通方法块 { System.out.println("父类->普通方法块"); } public Father(){ System.out.println("父类->构造函数"); } }
package test; public class Son extends Father { //------- 这两个顺序只和书写顺序有关 private static String staticfiled = getStaticfiled(); static { System.out.println("子类->静态代码块"); } // ------- private String field =getNormalfiled(); @Override public String getNormalfiled(){ System.out.println("子类->普通成员变量初始化"); return "hah"; } public static String getStaticfiled(){ System.out.println("子类->静态成员变量初始化"); return "haha"; } // 普通方法块 { System.out.println("子类->普通方法块"); } public Son(){ System.out.println("子类->构造函数"); } // 在子类中main函数 是仅此慢于静态代码块和静态变量 public static void main(String[] args) { System.out.println("在子类中的main主函数"); Son son = new Son(); } }
看下结果图:
确定顺序:
2.类加载器
区分两个类是否同类,是看类的加载器是否相同,及classloader
双亲委派模型
双亲委派模型就是这样的结构
说双亲其实又不算双亲,因为不是继承关系,而是组合关系
类加载器收到请求加载类,
首先让父类加载器去尝试加载,
父类加载器收到请求不行再交给子类加载器
这个和dns寻址相反,dns查不到ip就交给根dns服务器来查
区别就是加载器父类不行子类上
dns 地方dns服务器不行交给根dns查
这个也是object是所有类 父类的一个原因
值得注意的是,已经加载的类会缓存起来,下次就不用加载使用就行了。
3. JVM运行时内存结构
JVM运行时内存结构和JMM(JAVA内存模型)不同这点要搞清楚
JVM运行时内存结构是JVM中的,JAVA内存模型是研究多线程内存共享的关系
这里我们讨论的是JVM运行时内存结构
String 面试题 就喜欢考字符串常量池和引用的问题
经典面试题来几个
-
String str = new String(“11”) 创建了几个字符串
创建一个或者两个字符串,字符串常量池如果有这个对象就只在堆上创建一个
否则在堆上创建一个,在字符串常量池中也创建一个。
-
String 的 intern()方法 返回一个常量池中的一个字符串,如果常量池没有,则加入常量池再返回
4. 对象的内存分配和垃圾回收
垃圾回收主要是回收heap堆中对象
对象内存分配也是分配在heap堆中
回收Eden 区域中的对象 是MinorGC
回收老年代的则是FullGC
怎么样确定一个对象是死的或者说应该回收的呢
-
1.引用计数法
只要引用还在就不应该被回收
-
2.可达性算法
这个算法像是一个二叉树,从根节点GCroot 向下遍历
找到的对象则是有用的,没有找到的则是无用
GCroot对象有
栈帧中引用的对象即方法局部变量
方法区的静态对象
方法区常量引用对象
垃圾收集算法
-
标记清除
会产生内存空间碎片不好整理
-
复制(适用于朝生夕死的)
分成几块,某块用完复制到相同的一块去,然后清除
在新生代中
从Eden + from space 到 to space 就是这样一个过程
-
标记整理(适用于长存活对象)
标记存活的向一端内存移动
-
分代(一般都是这样)
每一次清理都需要可达性算法,
那这样就需要所有对象的引用,为了速度,
JVM 使用 OopMap数据结构来知道那里有对象的引用
同时,清理需要暂停所有线程,这样就会需要STW(stop the world)
暂停线程,就会造成安全影响,为了安全,JVM只在安全点stw
完整的GC流程
垃圾收集器
收集器有很多种,需要不同新生代收集器和老年代收集器的组合
-
Serial
单线程 串行回收 新生代复制 ,老年代标记压缩,会stw
-
ParNew
Serial的多线程 新生代并行 老年代串行 新生代复制,老年代标记压缩
参数:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
-
Parallel
类似Serial,强调吞吐量
-
Parallel old
老年代,使用标记整理
-
CMS(老年代) 重点 强调响应时间
4个步骤 concurrent mark sweep
初始标记(CMS initial mark) 有 STW 标记gcroots能到的对象 时间短
并发标记(CMS concurrent mark) 搜索gcroot的子节点 时间长
重新标记(CMS remark) 有STW 时间短
并发清除(CMS concurrent sweep) 时间长
-
G1 目前最强的 (新生老年都可以)
标记整理,没有内存空间碎片,新生代和老年代不一定内存上连续
划分堆为相同大小的 Region
可以预测停顿时间
清理过程和CMS差不多
5个步骤
1.标记 触发minorGC
2.Region区扫描
3.整个堆并发扫描
4.再标记 有stw
5.并发清除 有stw