- 方法区和堆是多线程共享的
- 本地方法栈,程序计数寄存器,Java虚拟机栈是每个线程独有一个
- 栈的指令集架构和寄存器指令集架构:由于Java的跨平台性,它的指令都是根据栈的架构来设计的
- 栈的指令集架构的特点,指令集小,指令多,执行性能比寄存器架构的差
Java虚拟机分类
- Hotspot 主流使用,JDK中内置的JVM
- Jrockit是最快的JVM,多用于服务器端
- J9
执行引擎
- 解释器和JIT即时编译器是相互合作完成代码翻译的
- JIT主要通过计数器查询热点代码并保存到方法区中的codecache中,比如for循环中的语句
- 解释器(走路),JIT(坐公交)。同一段路程,走路可以一直走但是比较慢,坐公交要等待,但是一旦坐上就很快
类加载的过程
- 加载、链接(验证、准备、解析),初始化
https://zhuanlan.zhihu.com/p/33509426
- 初始化阶段就是执行类构造器
<clinit>
的过程,只会被执行一次。如果类中没有静态变量或者静态方法块就不会有<clinit>
,只有<init>
方法。
类加载器分类
- 引导类加载器,扩展类加载器和系统类加载器。他们之间是包含关系,非继承关系。
- 用户自定义的类,使用系统类加载器,Java核心类库使用引导类加载器
双亲委派机制
- 在文件夹下定义一个
java.lang.String
类,里面写一个静态代码块输出语句。当一个测试类创建一个String类对象的时候不会创建自定义的String类。因为类加载器会向上委托,相当于先递归向上委托到启动类加载器,然后判断是否由自己加载,如果不是就分配给扩展类加载器,如果还不是就分配给系统类加载器。 - 如果再自定义的String类中定义一个main方法,去执行的时候会报找不到main方法的错误。原因是该String类直接由启动类加载器加载。(判断依据就是类所在的包名,java.lang)
- 自定义类一般都由系统类加载器加载。
- 双亲委派机制的优势:防止java核心api代码被篡改(沙箱安全机制),防止类重复加载。
Java虚拟机运行时数据区
PC寄存器
- 为什么要有PC寄存器?
- PC寄存器为什么是线程私有的?
- 线程轮流使用CPU资源
- 问题1:使用PC寄存器可以记录当前线程执行到的位置,线程切换回来的时候直到从哪里开始。
- 问题2:如果使用公共的PC寄存器会使得程序调度混乱。
Java虚拟机栈
- 每个线程都有自己的栈,每个方法对应一个栈
- 虚拟机的大小越大,方法可以递归调用的方法越多
- 如图,如果方法2调用了方法3,方法3出现了异常但是没有处理,那么方法3就会将异常抛给方法2,让方法2处理。
栈帧
- 局部变量表:保存方法中的局部变量,局部返回值等等,局部变量只在当前的方法中有效。栈帧的 大小,最由局部变量表的大小和操作数栈的大小决定。
- 局部变量表中存放编译器可知的各种基本数据类型8种(
char byte short boolean int float double long
),引用类型(只存放一个引用,具体的值在堆中存储),returnAddress类型的变量。 - 关于slot
- 使用static修饰的方法的局部变量表中没有
this
的区域,因此不能调用this
访问类中的内部变量。但构造方法和普通实例方法中有,并且放在局部变量的首位,所以可以调用this
对象。 - 局部变量和成员变量的区别
- initialization阶段给类变量显示赋值,或静态代码块赋值
- 操作数栈
- 动态链接:每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用。
- 方法返回地址
方法的绑定
- 动态绑定和静态绑定
- 编译时能确定的就是静态绑定,不能确定的就是动态绑定
- 静态绑定比如类的构造器中super()和this(),动态绑定比如类的多态性,接口方法调用
Java虚拟机栈的五个问题
- 局部变量的作用域仅仅在方法内就是线程安全的,但如果返回了局部变量或者局部变量作为形参,那就不安全。
本地方法接口
- native method interface
- native method library
- 使用native修饰方法,调用其他语言比如c或者c++等等。
- 为了与Java外环境交互,与操作系统交互如sun’s java
堆空间
- main方法中new一个实例对象的话,在Java虚拟机栈中的局部变量表会生成一个对象引用,引用的是堆空间中创建的实例对象,而实例对象的具体方法和常量池则在方法区
- 一个方法结束之后,即当前方法的栈帧出栈,Java栈中指向堆空间实例对象的引用随之销毁。但是此时的对中的实际对象并不会销毁,而是通过后期GC判断堆空间对象上的引用个数进行垃圾回收。
- 内存空间细致分大致分为三代,JDK7及之前新生代,老年代,永久代。JDK8永久代变成了元空间
- 堆空间内存大小分配
- 堆空间实际体现的大小是伊甸园区 + 一个幸存者区 + 老年代的大小
- 堆空间OOM可以通过一直循环创建对象来实现,通过visualVM可以查看占据大的内存是什么
- 年轻代和老年代
- 新生代和老年代的参数设置
- 新生代与老年代的大小比例默认是1:2,新生代中伊甸园区和幸存者1号和幸存者2号的比例默认为8:1:1 。但实际上需要显示设置SurvivorRatio=8才可以成功。
对象分配以及垃圾回收
-
对象首先会被分配到伊甸园区中,如果伊甸园区中满了会触发Y/Minor GC,将需要保留的对象发配到幸存者0区,但幸存者区满了不会触发GC***,如果保留了对象,那么该对象的计数器就会+1,如果够了15就会放到老年代*。
-
注意:有的对象可能不到15,或者刚创建就会发配到老年代。
-
- minor GC major GC full GC
- 堆空间不分代可以吗?可以,分代是为了优化GC性能
- 内存分配策略
- TLAB(Thread Local Allocation Buffer)是JVM内存分配的首选,大小时Eden区的1%
- 堆空间一定都是共享的吗?
- TLAB是为了解决不同线程之间为对象分配内存的线程安全性问题,内存中的数据还是线程共享的。(相当于提前为每一个线程分配了一小块区域,说你就先来你这里分配对象空间,如果不够再从Eden区申请)。Eden区不同线程分配空间就需要加锁。
- 逃逸分析,如果对象不发生逃逸(方法外没有调用对象实例),那就可以在栈中创建对象而不需要在堆上创建。减少堆GC的频率,提升性能。
- 结论,能用局部变量就不定在方法外
- 结论,能用局部变量就不定在方法外
- 对象都是在堆上创建的,逃逸分析技术不成熟。默认开启逃逸分析实则使用的是***标量替换***。标量替换就是把对象肢解为基本数据对象然后再放到栈空间上。
方法区
- 方法区的基本理解,针对hotspot虚拟机对方法区的实现JDK8改为了元空间,JDK7之前实现叫永久代
- 元空间使用的是本地内存而不是Java虚拟机分匹配的内存。
- 方法区和堆是两块不同的内存空间。
- 方法区存储的信息
- 通过反编译字节码文件可以知道,通过final static 修饰的变量在编译的时候就确定了值。而一般的变量则需要经过类加载子系统中的链接阶段的准备环节赋初值,然后再初始化阶段赋值。
- 字节码文件中的常量池,在经过类加载子系统之后进去运行时数据区就变成了运行时数据区的运行时常量池。
- 方法区的演进细节
- 注意,JDK8之后字符串常量池和静态变量仍在堆空间当中。
- 为什么要将永久代替换为元空间?
- 不好确定永久代的大小,元空间直接使用本地内存
- 减少
Full GC
- 永久代调优困难
- 方法区的垃圾回收,费力不讨好
- 创建对象的方式
- 创建对象的步骤
- 对象的内存布局
- 对象访问的两种方式,句柄访问和直接指针(HotSpot采用)
- 句柄访问,优点:如果堆空间发生GC,对象位置移动,Java栈当中的reference不变。缺点:Java堆中需要单独划分句柄池。
- 直接指针,缺点对应上面的有点,优点对应上面的缺点。
执行引擎
- 在JVM中所处的位置
- 执行引擎中有JIT编译器和解释器,JIT编译器的时间较长,解释器解释执行的速度较快。上菜(JIT)之前上凉菜(解释器)
- JIT热点代码及侦测方式
- 使用JIT还是解释器的流程图
- JIT有C1和C2两种编译器,默认C2,其由C++编写
字符串常量池
- 字符串常量池中是不会存储相同内容的字符串的,可以通过
String.intern()
来查询字符串常量池中是否有
- String字符串拼接
-String拼接操作的底层原理
- 第一段代码是false的原因是,s2指向的是字符串常量池中的对象,s指向的是堆空间中的对象
- intern()的使用