- JAVA如何实现跨平台?
Java实现跨平台主要是通过JVM
JVM在不同的平台有不同的版本,在不同的平台要安装不同的JVM版本,我们编写JAVA源码后,经过编译会生成字节码文件.class文件。
Java虚拟机JVM将.class文件翻译成不同平台对应的机器码,从而在不同的平台下运行
注意:编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。
所以,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的再次翻译才能执行。即使你将Java程序打包成可执行文件(例如
.exe),仍然需要JVM的支持。并且值得注意的是跨平台的不是JVM,是JAVA程序,因为JVM也是需要在不同的平台下安装不同版本的JVM
- JVM组成:运行时数据区、类装载子系统和字节码执行引擎
- 下面我们通过一个程序实例来了解JVM:
栈(线程):每个线程都有自己的栈内存空间,放自己线程在运行过程中它的局部变量、操作数栈、动态链接和方法出口。
栈帧:一个方法对应一块栈帧内存区域。
-
下面我们来看一下compute()方法的字节码:
0:iconst_1->将int类型常量1压入操作数栈
1:istore_1->将int类型值存入局部变量1(也就是a)
2:iconst_2->将int类型常量2压入操作数栈
3:istore_2->将int类型值存入局部变量2(也就是b)
-
每个线程运行时都会从一大块程序计数器内存空间中分配一块线程专属的程序计数器。
程序计数器:用来存放字节码中当前程序运行代码的位置(行号)。->在上述字节码中,当程序执行行号为4的行时,另一个线程把CPU时间片抢走,当前线程被挂起,另一个线程执行完毕,CPU继续执行当前线程,下次再运行compute()方法时,从行号为4的行开始执行,程序计数器就用来标识当前程序执行到的位置。
使用字节码执行引擎来动态修改程序计数器标识的位置(行号)。
4:iload_1->从局部变量1(a)中装载int类型值
5:iload_2->从局部变量2(b)中装载int类型值
6:iadd->执行int类型的加法(把2和1从操作数栈中弹出,执行加法再压回操作数栈)
7:bipush 10->将一个8位带符号整数压入栈(把10压入操作数栈)
9:imul->执行int类型的乘法
把10和3从操作数栈中弹出,执行乘法再压回操作数栈
10:istore_3->将int类型值存入局部变量3(也就是c)
11:iload_3->从局部变量3(c)中装载int类型值
-
操作数栈:在程序运行做运算的过程中,需要暂存(数据)的一个临时内存空间,运算结束后里面是空的。
-
方法出口:调用compute()方法时,记录其在main()方法中执行到哪一行,当调用完compute()方法后还能回到main()方法的对应位置上。
这里的math,是为math分配的一块内存空间,存储的不是对象(对象存储在堆里),存放的是math对象在堆中的内存地址(字符串),内存地址就是一个指针,一个对象的引用。
堆和栈之间的关系:栈上有很多指针,指向堆的对象,是对堆的对象的引用。
-
方法区:方法区在JDK1.8之后用的是直接内存,严格意义上说,方法区是放在JAVA内存区域里的,使用的内存空间是物理内存,不是用的JAVA虚拟机内部的内存。
方法区中主要存放常量(final修饰)、静态变量和类信息(比如我们上面代码的例子:Math.class(字节码文件))。
通过Math.class(字节码文件)通过类装载子系统加载到方法区。
方法区的内存空间存放的user是user对象在堆中内存空间的入口地址。
- 本地方法: 一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。
- 本地方法栈: 如果在程序运行中调用了本地方法,就会在本地方法栈内存空间里给本地方法运行过程中分配一块自己专属的内存空间。
- 堆:
新new出来的对象是存放在Eden区。
第一次Eden区满了,再往里面放对象,会触发字节码执行引擎开启minor gc(垃圾回收)。这时会JVM会从栈和方法区内找到所有的GC Roots,从GC Roots出发找到所有它引用的对象。找到的对象都标记为非垃圾对象,将这些对象复制到Survivor区s0中;其余未标记的都是垃圾对象,直接回收。
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等。
可达性分析算法:将GC Roots对象作为起点,从这些结点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的都是垃圾对象。
第二次 Eden区满了,JVM依然会从栈和方法区内找到所有的GC Roots,从GC Roots出发找到所有它引用的对象。找到的对象都标记为非垃圾对象,将这些对象(这些对象有Eden区的和Survivor区s0的)复制到Survivor区s1中。
**gc分代年龄(**存放在对象头(MarkWord)中,锁的信息也是存放咋对象头中):经历的gc次数。
第三次 Eden区满了,JVM依然会从栈和方法区内找到所有的GC Roots,从GC Roots出发找到所有它引用的对象。找到的对象都标记为非垃圾对象,将这些对象(这些对象有Eden区的和Survivor区s1的)复制到Survivor区s0中。
对象会从Eden区到Survivor区,然后在Survivor区的s0和s1区中来回流转,当分代年龄超过15时,会被JVM放到老年代。
**哪些对象可能被放入老年代?**静态变量,Spring容器的bean(controller、service等),线程池的对象、缓存对象。
Java中什么样的对象能够进入老年代?
1.大对象:所谓的大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。
2.长期存活的对象:虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认是15岁), 就将会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
3.动态对象年龄判定:为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
当老年代放满,会触发字节码执行引擎开启full gc,full gc会对整个堆的所有内存空间做整体回收。Minor gc只会回收年轻代。
但是full gc也只能去回收没有被引用的对象,当老年代持续增多,就会发生内存溢出(OOM,Out Of Memery)。
JAVA虚拟机调优的目的: 减少full gc->减少stw时间。
stw(stop the world): 停止所有用户线程(在full gc时会产生stw,minor gc的stw时间很短,可以忽略不计)。
- 为什么full gc时要进行stw?
如果不进行stw,从GC Roots找对象,找完一条线之后,找另一个时,由于没有stw,用户线程继续执行,可能gc还没有结束,但是用户线程已经结束了,用户线程结束会释放局部变量的内存空间,那指向一开始gc收集的指向堆对象的指针就没有了,刚开始垃圾回收器收集标记为非垃圾的对象,现在又变成垃圾了,但是gc还没有结束,那gc又要检查之前的变量,过程会变得非常复杂。Stw(停止用户线程)让gc收集过的对象状态不再改变。