Java内存区域
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
- 虚拟机栈 : stack frame 栈帧
- 程序计数器 : program counter
- 本地方法栈 : c ++ 等实现
- 堆 : 最大的一块共享内存区域 , new object 的实例 。通过引用(在栈上) 指向实例。 与堆密切相关的是垃圾收集器, 几乎所有的现在垃圾回收器都采用了分代算法, 堆空间也针对此进行了划分 。 新生代和老年代 Eden, from survivor ,to survivor (默认按照80 , 10 ,10 的比例进行划分)
- 方法区 : 主要存储一些元信息,其中包括 class信息 等 ,有称为永久代 (jdk1.8 开始废弃,进而被元空间取代 ),很少会被垃圾回收
- 直接内存: 堆外内存,不是jvm直接管理 ,操作系统管理 . nio直接相关,DirectByteBuffer 是堆外内存
虚拟机栈,程序计数器 和 本地方法栈 都是内存私有的 。
堆是虚拟机中最大的一块内存区域,我们通过new关键字创建的对象,绝大多数都会在堆上面分配内存 ,然后会在栈上通过引用去指向它。
堆上的对象由 数据区和元数据(比如class对象唯一)2 部分组成 , 并且在不同的虚拟机上面会有不同的实现
二. new关键字创建对象的流程
- 在堆内存创建对象实例
- 为对象实例成员变量赋初值
- 返回对象引用
对象在内存中的布局:
- 对象头
- 实例数据
- 对齐填充(可选)
HotSpot虚拟机
- 对象的创建及内存分配
- 对象的内存布局
- 对象的访问定位
markword 等
Java内存模型
- 主内存、工作内存
- 内存间的交互操作
- 对volatile变量的特殊规则
- 原子性、可见性、有序性
- Happpen-Before原则
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
内存分配策略
http://wanke.fun/Jvm/jvm-gc/
新生代、老年代
- 对象优先在 Eden 分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果是, Minor GC 可以确认是安全的。
如果不是,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
垃圾收集
http://wanke.fun/Jvm/jvm-gc/
垃圾搜集算法
- 判断对象是否存活
- 引用计数器
给对象中添加一个引用计数器,当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;计数器为 0 的对象就是不可能再被使用的。这个方法实现简单、效率高,但是无法解决循环引用问题。- 可达性分析
通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
方法区中
- 类静态属性引用的对象
- 常量引用的对象
- 引用
- 强
- 弱
- 软
- 虚
- 垃圾收集算法
- 标记清除
该算法分为“标记”和“清除”阶段:先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。有两问题:
效率问题
空间问题(产生大量不连续的碎片)- 复制算法
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。- 标记整理
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。- 垃圾收集器
- 分代算法
新生代:每次收集都会有大量对象死去,可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
老年代:对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。- G1
面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队期望未来可以替换掉 CMS 收集器。G1 可以直接对新生代和老年代一起回收。- Serial
串行、单线程
类文件结构
类加载机制
JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化
加载:
加载过程主要完成三件事情:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
校验
此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。
- 文件格式验证:基于字节流验证。
- 元数据验证:基于方法区的存储结构验证。
- 字节码验证:基于方法区的存储结构验证。
- 符号引用验证:基于方法区的存储结构验证。
准备
为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间
解析
把类型中的符号引用转换为直接引用。
初始化
初始化阶段是执行类构造器
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”
- 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
- 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
- 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
- 虚拟机启动时,用户会先初始化要执行的主类(含有main)
- jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
双亲委派模型,类加载器
http://wanke.fun/Jvm/jvm-%E7%B3%BB%E5%88%97%E7%AC%94%E8%AE%B0/
- 启动类加载器 BootstrapClassLoader
- 扩展类加载器 ExtClassLoader
- 应用类加载器 AppClassLoader
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
双亲委派机制的工作流程:
- 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
- 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
- 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
总结:
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法
虚拟机
1. Hotspot
2. dalvik
3. art
从Android5.0开始废弃了Dalvik,全面推行ART。
- 在Dalvik下,应用每次运行都需要通过即时编译器(JIT)将字节码转换为机器码,即每次都要编译加运行,这虽然会使安装过程比较快,但是会拖慢应用以后每次启动的效率。而在ART 环境中,应用在第一次安装的时候,字节码就会预编译(AOT)成机器码,这样的话,虽然设备和应用的首次启动(安装慢了)会变慢,但是以后每次启动执行的时候,都可以直接运行,因此运行效率会提高。
- ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这也是著名的“空间换时间大法”。
- 预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。
- ART优点:
- 系统性能的显著提升
- 应用启动更快、运行更快、体验更流畅、触感反馈更及时
- 更长的电池续航能力
- 支持更低的硬件
- ART缺点:
- 更大的存储空间占用,可能会增加10%-20%
- 更长的应用安装时间
反射
JVM的反射机制是在Java类编译成字节码的时候,通过jdk提供的工具类的反射方式访问类字节码的字段、方法。并且可以进行一些在Java代码中不能够实现的功能。
- 经典场景:
Spring的ioc的依赖注入,以及一些IDE的代码开发联想功能;
- 缺点:
反射机制的效率是很低的
- 反射性能的开销:
反射的主要操作:Class.forName()和Class.getMethod()调用本地实现,其中,getMethod()则会遍历类中的共有方法,类中没有则遍历父类,所以是非常耗时的,尤其在一些热点代码中可以采用本地缓存的方式来缓存结果。
- 性能开销主要体现在这几个方面 :
(1)数组参数的传入
(2)自动装箱和手动装箱的区别
(3)逃逸对象的优化
(4)动态实现的内联调用
注解
JAVA 中有以下几个『元注解』:
@Target:注解的作用目标
@Retention:注解的生命周期
@Documented:注解是否应当被包含在 JavaDoc 文档中
@Inherited:是否允许子类继承该注解
- @Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。
有以下一些值:
- ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
- ElementType.FIELD:允许作用在属性字段上
- ElementType.METHOD:允许作用在方法上
- ElementType.PARAMETER:允许作用在方法参数上
- ElementType.CONSTRUCTOR:允许作用在构造器上
- ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
- ElementType.ANNOTATION_TYPE:允许作用在注解上
- ElementType.PACKAGE:允许作用在包上
- @Retention 用于指明当前注解的生命周期,它的基本定义如下:
有以下几个枚举值可取:
- RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
- RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
- RetentionPolicy.RUNTIME:永久保存,可以反射获取