赞
踩
目录
--------------------------------------------------------------------------------------------------------------------------------
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
jvm线程
如果你使用 jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用 public static void main( string[]args)的main线程以及所有这个maih线程自己创建的线程。这些主要的后台系统线程在 Hotspot J里主要是以下几个:
Java虚拟机栈是什么
每个栈帧中存储着:
生命周期
作用
说明
栈的特点(优点)
栈可能出现的异常
举例:
- public class Test1 {
- static int i=0;
- public static void main(String[] args) {
- test();
- }
- public static void test(){
- System.out.println(++i);
- test();
- }
- }
JVM参数:
-Xss512K -XX:+PrintFlagsFinal
执行了5074次
-Xss修改为256K 执行了2331次
栈运行原理
补充说明:
slot变量槽
局部变量表,最基本的存储单元是slot
请看下面的示例使用jclasslib插件查看,序号0为this,序号1 double占两个 3开始是long
- public class Test3 {
- public void localvarl() {
- double c=0;
- long f=0;
- byte a=0;
- int b=0;
- boolean d=false;
- float e=0f;
- char g=0;
- }
- }
slot重复利用
- public class Test3 {
- public void localvarl() {
- int a = 0;
- System.out.println(a);
- int b = 0;
- }
- public void localvarl1() {
- {
- int a = 0;
- System.out.println(a);
- }
- int b = 0;
- }
- }
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In- First-0ut)的操作数栈,也可以称之为表达式栈( Expression Stack)。
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
举例:
- public class Test4 {
- public void test4(){
- int i=1;
- int j=2;
- int c=i+j;
- }
- }
栈顶缓存技术
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题, Hotspot JVM的设计者们提出了栈顶缓存(ToS,Top-of- Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( Symbolic Reference)保存在 class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法退出后都返回到该方法被调用的位置,存放调用该方法的pc寄存器的值。
一个方法的结束,有两种方式:
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
作用:PC寄存器用来存储指向下一条指令的地址也即将要执行的指令代码。由执行引擎读取下一条指令
CPU时间片
CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片
在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
在微观上:由于只有一个CPU,一次只能处理程序要求的一部分如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
常见问题:
PC寄存器为什么会被设定为线程私有?
举例:
本地方法接口
简单地讲,一个 Native Method就是一个Java调用非Java代码的接口。一个Native method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。
本地方法栈
本地方法栈用于管理本地方法的调用。是线程私有的
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 StackOverflowError异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个 outofmemoryerror异常。
针对不同年龄段的对象分配原则如下所示:
示例:
借助jvisualvm 的visual gc可以看到 eden、s0、s1、old区的分配情况,通过修改byte[]数组大小可以让其分配不同方式,可能进入eden区,可能直接进入old区
- public class Test6 {
- byte[] buffer = new byte[1024 * 1024*1];
-
- public static void main(String[] args) {
- ArrayList<Test6> list = new ArrayList<>();
- while (true){
- list.add(new Test6());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- }
- }
- }
官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
空间担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的,如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
为什么有TLAB
定义
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 Hotspot VN的实现,GC按照回收区域又分为两大种类型:一种是部分收集( PartialGC),一种是整堆收集(Full GC)
部分收集:不是完整收集整个]ava堆的垃圾收集。其中又分为:
年轻代GC( Minor GC)触发机制:
老年代GC( Major GC/FullGC)触发机制:
触发FullGc执行的情况有如下五种:
说明:full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。
为什么需要把Java堆分代?
不分代就不能正常工作了吗?其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
逃逸分析的基本行为就是分析对象动态作用域:
优化
逃逸分析一定快吗?
无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
存储已被虚拟机加载的类型信息、运行时常量池、静态变量、JIT代码缓存、域(field)信息、方法信息等
元空间和永久代最大区别:元空间不在虚拟机设置的内存中,而是使用本地内存
字符串常量池中是不会存储相同容的字符串的。常量池是一个固定大小的 Hashtable,默认值大小长度是1009。如果放进 string非常多,就会造成Hash冲突严重,性能下降,jdk8常量池在堆中
string类型的常量池比较特殊。它的主要使用方法有两种。
字符串拼接
- public class Test7 {
- public static void main(String[] args) {
- test7();
- }
- public static void test7() {
- String a="a"+"b"+"c";
- String b="d";
- String c=b+"e";
-
- }
- }
- public class Test8 {
- public static void main(String[] args) {
- test8();
- // test8_1();
- }
- public static void test8() {
- String a=new String("a");
- String intern = a.intern();
- String a1="a";
- System.out.println(a==intern);//false
- System.out.println(a==a1);//false
- System.out.println(a1==intern);//true
- }
-
- public static void test8_1() {
- String a=new String("a")+new String("b");
- String intern = a.intern();
- String a1="ab";
- System.out.println(a==intern);//true
- System.out.println(a==a1);//true
- System.out.println(a1==intern);//true
- }
- }
解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言 ,在一定周期内代码被调用执行频率而定,热点代码会做jit编译进行优化
现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。