内存结构
1.程序计数器
作用:记住下一条jvm指令的执行地址
特点:
1.是线程私有的
2.是不会内存溢出的
2.虚拟机栈
2.1定义
- 栈:线程运行需要的内存空间
- 栈帧:每个方法运行时需要的内存(参数,局部变量,返回地址)
- 每个线程只能有一个活动栈帧,对应着当时正在执行的那个方法
问题:
- 垃圾分类不会涉及栈内存
- 栈内存分配并不是越大越好,因为栈内存越大,线程数目就越小
- 方法内的局部变量不一定是线程安全,如果方法内局部变量没有逃离方法的作用的访问,他是线程安全的;如果局部变量引用了对象,并逃离了方法,需要考虑线程安全
2.2栈内存溢出
java.lang.StackOverflowError
3.本地方法栈
java虚拟机调用本地的方法时提供的内存空间
4.堆
- 通过new 关键字创建的对象都会使用堆内存
- 他是线程共享的,堆中的对象都需要考虑线程安全的问题
- 有垃圾回收的机制
5.方法区
1.定义
- 所有虚拟机线程共享的区域
- 存储了跟类结构相关的信息(成员变量,方法数据,成员方法和构造器方法的代码)
- 1.8之前存在永久代
- 1.8之后存在元空间(本地内存)
2.运行时常量池
- 源代码要先编译成二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
- 运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变成真实地址
3.StringTable串池
- 底层由hashtable实现
- 常量池中的字符串仅是符号,第一次用到的时候才变为对象放到串池里面
- 利用串池的机制,来避免创建字符串对象
- 字符串变量的拼接原理是StringBuilder new String()
String s4= s1+ s2;//new StringBuilder()).append(s1).append(s2).toString() new String("ab") 会生成一个新的字符串在堆里面
- 字符串常量拼接的原理是编译期优化
/在常量池中找一个ab对象,找到了,所以不会新建对象 javac在编译期间的优化,结果已经在编译期间确定为ab
- 可以使用intern方法,主动将串池中还没有的字符串放入串池
- 1.8调用intern方法时,如果池中已经包含了一个与这个字符串相等的字符串,则返回来自这个池的字符串,否则,将此字符串对象添加到池里,并返回对此字符串对象的引用
- 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
4.StringTable位置
1.8堆空间
1.6永久代
5.StringTable垃圾回收
只有在内存紧张时才会触发
6.StringTable性能调优
- 调整hashtable桶的个数
- 考虑将字符串对象入池
6.直接内存
- 常见于NIO操作时,用于数据缓存区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
- 使用Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放内存
垃圾回收
1.如何判断对象可以回收
引用计数法
被引用+1,不被引用-1,为0时回收
弊端:循环引用
可达性分析算法
- java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
- 这些对象可以作为GC ROOT
- 启动类加载器的类,核心类,运行期间会用到的类
- 虚拟机会调用操作系统的方法,操作系统方法引用的java对象
- 活动线程中使用的对象,栈帧中使用的
- 正在加锁的对象
四种引用
- 强引用 :平时用的引用都是强引用,只有GC Root对象不通过强引用引用该对象时,该对象才会垃圾回收。
- 软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍然不足时会再次发生垃圾回收,回收软引用,可以配合引用队列来释放软引用自身
- 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用,可以配合引用队列来释放弱用本身
- 虚引用:必须配合引用队列使用,主要配合ByteBuffer使用,ByteBuffer被回收时,让虚引用对象cleaner进入引用队列,由ReferenceHandler线程定时在引用队列里查找是否有新入队的cleaner,如果有的话就调用cleaner里的clean方法,调用unsafe.freeMemory
2.垃圾回收算法
- 标记清除:速度快,但是空间不连续,会造成内存碎片
- 标记整理:没有内存碎片,效率低下
- 复制:不会产生内存碎片,占用双倍内存
3.分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加一并且交换from to
- minor gc会引发stw,暂停其他用户的线程,等待垃圾回收结束,用户线程才恢复运行
- 当对象的寿命超过阈值时,会晋升至老年代,最大寿命是15
- 如果老年代空间不足,会先尝试触发minor gc ,如果之后空间仍不足,那么触发full gc,stw的时间更长
- 大对象在老年代空间足够,但新生代空间肯定不足的时候,会直接晋升
4.垃圾回收器
1.串行
- 单线程
- 堆内存较少,适合个人电脑
- 新生代使用复制算法,老年代使用标记+整理算法
2.吞吐量优先(一定时间内垃圾回收的多少)
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,stw的时间最短
- 新生代使用复制算法,老年代使用标记+整理算法
3.响应时间优先(CMS)
- 多线程
- 堆内存较大,多核CPU
- 尽可能让单次stw的时间最短
- 初始标记(标记根对象)----->并发标记(找出剩余的垃圾stw)------>重新标记(因为在并发标记时,用户线程也在工作,所以有可能产生一些新的对象,改变对象的引用)
- 基于标记+清除算法,有可能造成比较多的内存碎片,导致内存不足,并发失败,cms就会退化为SerialOld,做一次单线程的串行回收(整理碎片)
类加载和字节码技术
1.执行流程
- 源代码
- 编译成字节码文件
- 常量池载入运行时常量池(short类型存在字节码,超过short范围的存在常量池)
- 方法字节码载入方法区
- main线程开始运行,分配栈帧内存(栈帧里有操作数栈和局部变量表)
- 条件和循环:字节码中使用goto
- 异常处理:采用Exception table 有from to table type,一旦在[from,to)范围内出现异常,则通过type匹配异常类型,如果一致,进入target所指示行数
- finally:所有的异常处理之后或者正常运行都会goto finally那一行,并且会自行catch finally中的异常并throw(但如果finally中出现了return,会吞掉异常)
2.类加载阶段
1.加载
- 将类的字节码载入方法区中,内部采用C++的instanceKlass描述Java类,他的重要field:
- _java_mirror即java的类镜像,例如对String来说,就是String.class,通过java代码进行访问的时候,不能直接访问到instanceKlass,只能先找到String.class才能进一步访问到instanceKlass,起到桥梁的作用(instanceKlass存储在方法区,java_mirror存储在堆中)
- _super父类
- _fiels成员变量
- _methods方法
- _constants常量池
- _class_locader类加载器
- _vtable虚方法表
- _itable接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替进行的
2.链接
- 验证
- 验证类是否符合jvm规范,安全性检查
- 准备
- 为static变量分配空间,设置默认值
- static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型和字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果static变量是final的,但属于引用类型,那么赋值会在初始化阶段完成
- 解析
- 将常量池中的符号引用解析为直接引用
3.初始化
- 初始化即调用
<cinit>()V
,虚拟机会保证这个类的构造方法线程安全(编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方 法<cinit>()V
) - 发生的时机(类初始化是懒惰的)
- 会导致类初始化的情况
- main方法所在类,首先会被初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量时,只会触发父类的初始化
- Class.forName()
- new会导致初始化
- 不会导致初始化的情况
- 访问类的static final静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
- 会导致类初始化的情况
3.类加载器
1.分类
括号里为加载的类的地址
- Bootstrap ClassLoader(JAVA_HOME/jre/lib )
- Extension ClassLoader(JAVA_HOME/jre/lib/ext )
- Application ClassLoader(classpath)
- 自定义类加载器(自定义)
会先询问上级是否已经加载了这个类,如果上级都没有加载,则自己加载
2.双亲委派模式
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类(递归),因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(他的搜索范围中没有找到所需要的的类),子加载器才会尝试去自己加载
3.自定义类加载器
- 什么时候需要自定义类加载器?
- 想加载非classpath,随意路径中的类文件
- 都是通过接口来使用不同的实现不同的实现,实现软件的解耦时,常用在框架设计
- 一个类有不同的版本,希望新旧的版本同时工作。这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常用于tomcat容器
- 操作
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写findClass方法(不是loadClass方法)
- 读取类文件的字节码
- 调用父类的defifineClass 方法来加载类
- 使用者调用该类加载器的loadClass方法
4.运行时优化(即时编译JIT)
1.分层编译
- JVM将执行状态分成了5个层次
- 0层,解释执行(Interpreter )将字节码解释成机器码
- 1层,使用C1即时编译器编译执行(不带profiling)
- 2层,使用C1即时编译器编译执行(带基本的profiling)
- 3层,使用C1即时编译器执行(带完全的profiling)
- 4层,使用C2即时编译器编译执行
profifiling
是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的
回边次数】等
- 即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并且存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对全平台都通用的机器码
- JIT会根据平台类型,生成平台特点的机器码
- 对于占据大部分的不常用代码,我们无需耗费时间将其编译成为机器码,而是采取解释执行的方法运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率Interpreter<C1<C2
- 即时编译器(JIT)的总的目标是发现热点代码,并加以优化
- 逃逸分析:在C2编译器里运行,分析new Object()是否逃逸,会不会在循环外被使用,发现不会。所以JIT进行逃逸分析之后,将对象创建的字节码替换掉
2.方法内联
如果发现某方法是热点方法,并且长度不长时,会进行内联,所谓内联就是把方法内代码拷贝粘贴到调用者的位置