一、查看字节码
1、字节码指令执行
iconst_0 取常量池0位置的值放到操作数栈中
istore_1 把操作数栈中的值放到局部变量表1位置中
iload_1 把局部变量表1位置的值放到操作数栈中
iinc 1 by 1 局部变量表1位置的值加1
2、查看字节码文件
1、jclasslib
2、服务器查看 javap
3、运行中查看字节码 软件arthas 的dump命令
二、类的生命周期
1、加载
类加载器将类的字节码加载进方法区
中,生成一个InstanceKlass对象,用来保存类的基本信息、常量池、字段、方法、虚方法表等
同时虚拟机还会在堆区生成一个Class对象,用来保存类的成员字段、方法和静态字段(jdk1.8后)
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
2、链接
连接分为三阶段:验证、准备、解析。验证是检验魔术、版本号。准备是为静态变量分配空间并设置初始值(被final修饰且=号右边是常数的变量也在这里赋值),解析是将常量池里的符号应用替换为直接引用。
3、初始化
- 执行静态代码块(仅执行一次)、给静态变量赋值
ps: init方法由构造方法和非静态代码块组成,且非静态代码块先执行。初始化时不会执行init方法。静态变量用final修饰的会在准备阶段进行初始化。
4、什么时候触发类初始化
1、调用一个类的静态方法和静态变量。注意变量是final修饰且右边是常量是不会被触发的。
2、初始化子类前,先初始化父类
3、new一个该类的对象
4、Class.forName(xxx)
5、执行Main方法的当前类
初始化时只会执行静态变量赋值和静态代码块调用,不会执行构造方法和非静态代码块。
并且非静态代码块是在构造方法之前执行的。
三、类加载器
1、双亲委派机制有什么作用?
- 保证类加载的安全·
- 避免重复加载
2、说一下双亲委派机制?
3、如何打破双亲委派机制?
自定义类加载器并重写loadClass方法
四、运行时数据区
运行时数据区分为5部分
1、程序计数器
记录下一行字节码指令的地址。执行完当前指令后,虚拟机的执行引擎根据程序计数器执行下一行指令。
程序计数器不会发生内存溢出
2、虚拟机栈
虚拟机的栈帧包括局部变量表、操作数栈、帧数据等
- 局部变量表:运行时存放所有局部变量
- 操作数栈桢:存放临时变量的一块区域
- 帧数据:包括动态链接、方法出口、异常表的引用
2.1、局部变量表
栈帧中的局部变量表是一个数组,数组的每个位置称为slot,long和double类型占用两个slot,其他类型都只占用一个slot。
主要内容有:实例方法的this对象、方法参数、方法中定义的局部变量。顺序与定义顺序一致。
为了节约空间,局部变量是可以复用的。一旦某个局部变量失效,当前slot就可以再次被使用。
2.2、设置栈大小
语法:-Xss256k
建议设置成256k,一般情况栈深度也就到几百,不会出现栈溢出。windows:1k~1025m
3、堆
堆用来存放new出来的对象
max默认是系统的1/4,total是1/64。超过上限值会抛出outOfMemory
3.1、设置堆大小
语法:-Xmx值,-Xms值 (建议Xms和-Xmx设置成一样)
Xmx必须大于2MB,Xms必须大于1MB
3.2、静态变量的存储
静态变量存储在堆的Class对象中
4、方法区
方法区存放的是:类的元信息、运行时常量池、字符串常量池
JDK1.7及之前方法区放在在堆中的永久代
里
JDK1.8及之后方法区放在直接内存的元空间
里
JDK8可以使用-XX:MaxMetaspaceSize=值
来设置元空间大小
4.1、字符串常量池
字符串常量池存储的是代码中定义的常量字符串,比如“345”。
1.7前字符串常量池包含在运行时常量池中在永久代的方法区中。1.7及之后字符串常量池分离出来放在堆中。
4.2、intern
1.7前intern()会把第一次出现的字符串复制一份放到方法区并返回地址。1.7及之后会把字符串的引用放入字符串常量池。
五、直接内存
ByteBuffer提供了allocateDirect方法用来操作直接内存
1、设置直接内存
-XX:MaxDirectMemorySize=大小
六、自动垃圾回收
6.1、方法区回收
6.1.1、类卸载的条件
6.2、堆区回收
6.2.1、怎么判断对象该被回收
引用计数法:
对象维持一个引用计数器,被引用就加1,引用取消则减1
缺点:要维持一个计数器,消耗性能。可能会导致循环引用。
可达性分析:
如果一个对象到GC root对象是可达的,那么这个对象不可被回收。
生存或者死亡?
即使在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。
要宣告一个对象的真正死亡,至少要经历两次标记过程 : 如果对象在进行可达性分析之后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情 况都视为"没有必要执行",此时的对象才是真正"死"的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。
finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中 的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;
如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。
什么是GC root对象?
1、线程Thread对象
,它会去引用方法栈桢中的参数和局部变量
2、Class对象
,Class对象保存着类的字段、方法和静态变量
3、监视器对象
4、本地方法调用时使用的全局对象
6.2.2、软引用
6.2.3、弱引用
#### 6.2.3、虚引用和终结引用
七、垃圾回收算法
标记清除
标记清除算法,分为标记和清除两个阶段,首先根据可达性算法标记出所有存活对象,等标记完毕,回收所有没有被标记的对象。标记清除算法有两个不足之处:一个是效率比较低,二是会产生空间碎片,过多的空间碎片会导致为大对象分配空间时,找不到足够的连续空间,不得不提前发生GC。
复制算法
复制算法解决了标记清除效率低的问题,复制算法将内存空间分为两半,每次只使用一块内存,当这块内存要进行垃圾回收时,将存活对象复制到另一块内存,清空使用过的内存。这样的好处是,每次都回收整个半区,并且分配空间时不需要当心空间碎片
,只需要移动堆顶指针。操作简单吞吐量大
。当然也存在内存使用率低
的问题。
标记整理算法
老年代的对象生存率较高,复制算法需要较多的复制操作,效率低。因此老年代用标记整理算法更佳。
标记整理算法分为标记和整理两个阶段,标记所有存活对象,然后将存活对象移动到堆的一端,最后清除端边界外的空间。优点是空间利用率高、不会发生空间碎片。
分代回收算法
分代回收算法将堆内存分为年轻代和老年代,年轻代有分为eden、幸存者1区、幸存者2区。
分代回收创建出来的对象,首先会被分配到Eden区,满了会触发young gc, 把存活对象复制到to区,然后清空eden区和from区的对象,最后交换from和to的引用。如果young gc后to区内存依然不足,对象将进入老年代。年龄达到15也会进入老年代。
调整堆大小
八、垃圾回收器
垃圾收集器分为年轻代和老年代
Serial+Serial Old
ParNew+CMS
Parallel Scavenge+Parallel Old
g1
Serial垃圾回收器
ParNew回收器
ParNew是Serial的多线程版本,使用的是复制算法,一般和CMS配合使用。
CMS垃圾收集器
CMS垃圾回收器是多线程的老年代的垃圾回收器。它的目标是获取最短的回收停顿时间。它是基于标记清除算法实现的,分为四个步骤:
初始标记:只标记GC root的直接引用
并发标记:根据直接引用标记引用链上的对象
重新标记:标记并发标记过程中用户创建的存活对象
并发清除
由于并发标记和并发清除这两个阶段的时间最长,而且用户可以在这两阶段同时执行,所以可以获得较短的停顿时间。
缺点:浮动垃圾和空间碎片,空间不足会启用Serial Old来回收
Parallel Scavenge垃圾收集器
Parallel Scavenge是一个多线程的年轻代垃圾回收器,可以设置最大停顿时间和吞吐量大小。适合不用和用户交互的后台任务。parallel s和ParNew最大的区别是它有一个自适应的调整策略,可以动态的调整新生代大小、Eden和Survivor区的比例、晋升老年代的年龄等。