总体结构
好处:
- 数组越界自动检查
- 一次编译 处处运行
- 多态
类加载过程
- 加载
- 验证
- 准备
- 解析
- 初始化
内存结构
五大部分
- 方法区
- 堆
- 程序计数器
- 本地方法栈
- 虚拟机栈
程序计数器
线程私有。可看作是 当前线程所执行的字节码的行号指示器,字节码解释器的工作是通过改变这个计数值来读取下一条要执行的字节码指令。
多线程是通过线程轮流切换并分配处理器执行时间来实现的,任何一个时刻,一个内核只能执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。这就是一开始说的“线程私有”。如果线程正在执行的方法是Java方法,计数器记录的是虚拟机字节码的指令地址;如果是Native方法,计数器值为空。
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器
作用:记住下一条jvm指令的执行地址
特点:
- 线程私有
- 没有内存溢出问题
虚拟机栈
线程私有,声明周期与线程相同。
存储 局部变量,操作数栈,动态链接,方法出口信息
运行时所需要的内存
不用被垃圾回收
方法内变量是否是线程安全的:看是否逃离了作用范围
方法区
存储类的数据
思考一下,为什么使用元空间替换永久代?
表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。 当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。 更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。
- 1.7之前是永久代
- 在1.8之后 换成了元空间
常量池
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期优化可以使用intermn方法,主动将串池中还没有的字符串对象放入串池
// ====这些直接放在常量池
String a='';
String b='';
// 下面直接new了新对象
String c=a+b;
// 下面编译器会自己进行优化
String d="a"+"b";
1.7中 String中intern方法会拷贝一份放进常量池
直接内存
对NIO 大文件读取
分配
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引l用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
垃圾回收
-
内存溢出
-
内存泄漏
-
引用计数法:循环引用问题
-
可达性分析
可达性分析: GC-root:
- 栈帧中的对象(就是方法正在执行的变量)
引用类型
- 强引用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(WeakReference)
- 虚引用(PhantomReference)
引用队列: 用于回收中间的引用变量 配合软引用可以回收变量(需要手动回收)
FullGC直接干掉所弱引用
垃圾回收算法
结合多种算法
回收垃圾时 所有线程都停止(时间短
标记清除
找到不用的内存 进行标记 下次需要内存时候直接分配(空间不连续)
缺点:产生内存碎片,就是大内存的对象塞不进去
标记整理
复制
CMS
cms的四个回收步骤比较好理解,主要为四个步骤:
- 初始标记:这个过程十分快速,需要 stop the world,遍历所有的对象并且标记初始的gc root
- 并发标记:这个过程可以和用户线程一起并发完成,所以对于系统进程的影响较小,主要的工作为在系统线程运行的时候通过gc root对于对象进行根节点枚举的操作,标记对象是否存活,注意这里的标记也是较为迅速和简单的,因为下一步还需要重新标记
- 重新标记:需要 stop the world,这个阶段会继续完成上一个阶段的动作,对于上一个步骤标记的对象进行二次遍历,重新标记是否存活。
- 并发清理:和用户线程一起并发,负责将没有Gc root引用的垃圾对象进行回收。
从上面的步骤描述可以看到,cms的垃圾收集器已经有了很大的进步,可以实现并发的标记和并发的整理阶段做到和用户线程并发执行(但是比较吃系统资源),不干扰用户线程的对象分配操作,但是需要注意初始标记和重新标记阶段依然需要停顿。
初始标记
初始标记阶段:需要暂停用户线程, 开启垃圾收集线程, 但是仅仅是收集当前老年代的GC ROOT对象,整个运行过程的速度非常快,用户几乎感知不到。
这里需要注意的是哪些对象会作为GC ROOT,而哪些则不会,比如实例变量不是GC ROOT的对象,同时在根节点枚举当中如果发现没有被引用也会标记为垃圾对象。
哪些节点可以作为gc root
- 局部变量本身就可以作为GC ROOT
- 静态变量可以看作是Gc Root
- Long类型index的遍历循环会作为GT ROOT
总结:当有方法局部变量引用或者类的静态变量引用,就不会被垃圾线程回收。
并发标记
并发标记阶段:可以和用户线程一起并发执行,此时系统进程会不断往虚拟机中分配对象,而垃圾收集线程则会根据gc root对于老年代中的对象进行有效性检测,将对象标记为存活对象或者垃圾对象,这个阶段是最为耗时的,但是由于是和用户线程并发执行,影响不是很大。
注意这个这个阶段并不能完成标记出需要垃圾回收的对象,因为此时可能存在存活对象变为垃圾对象,而垃圾对象也可能变为存活对象。
补充 - 并发关系和并行关系在jvm的区别:
并行:指的是多条垃圾收集线程之间的关系
并发:垃圾收集器和用户线程之间的关系
重新标记
重新标记阶段:这个阶段同样需要stop world,作用是会继续完成上一个阶段的动作,其实是对第二个阶段已经标记的对象再次进行对象是否存活的标记和判断,这个过程是十分快的,因为是对上一个步骤的扫尾工作。
并发清理
并发清理阶段:这个阶段同样是和用户线程并发执行的,此时用户线程可以继续分配对象,而垃圾回收线程则进行垃圾的回收动作,这个阶段也是比较耗时的,但是由于是并发执行所以影响不是很大。
G1