Java执行的过程:先编译,再解释
1.什么是Java虚拟机?
Java源文件被编译成能被Java虚拟机执行的字节码文件(.class文件),Java虚拟机是一个可以执行Java字节码的虚拟机进程,它能够把Java字节码翻译成机器认识的机器码。
2.为什么Java被称为“平台无关的编程语言”?
- 不论哪种硬件,只要你装有Java虚拟机,它都认识Java字节码,它能够把Java字节码翻译成机器认识的机器码。
- 当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码不面向任何具体的平台,只面向JVM。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行。
- 相同的字节码在不同的平台上运行,这就需要JVM这个中间转换器。
Java虚拟机: 它在执行java程序的时候会把它管理的内存划分为若干的不同的内存区域。
线程独有的:程序计数器,java虚拟机栈
所有线程共享的数据区域:方法区,堆
(1)程序计数器:当前线程执行的字节码的行号指示器。线程私有,一个时刻,处理器只能执行一条线程中的指令,为了线程切换后恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各线程之间计数器互不影响。
此区域是java虚拟机中唯一一个没有规定任何OutOfMemoryError的情况。
如果当前执行的是java方法,那么程序计数器记录的是正在执行的虚拟机字节码指令的地址。
一个Native Method就是一个java调用非java代码的接口
如果当前执行的是Native方法,那么程序计数器值为空。
(2)Java虚拟机栈:线程私有,生命周期和线程相同,虚拟机描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个线帧,用于存储局部变量,操作数栈,动态链接,方法出口等**。每一个方法从调用到执行完成的过程,就对应着一个线帧从虚拟机进栈和出栈的过程**。
两种异常:当线程请求的栈的深度>虚拟机所允许的长度,就抛出StackOverflowError异常,如果虚拟机扩展时不能申请足够的空间,就抛出OutOfMemoryError异常
(3)本地方法栈。本地方法栈和Java虚拟机栈发挥的作用类似,
Java虚拟机栈为虚拟机执行Java方法服务,
本地方法栈为虚拟机使用的Native方法服务
(4)Java堆 是线程共享的一块内存区域,在虚拟机启动时创建,
- 目的:存放对象实例
- 垃圾处理器管理的主要区域,收集器采用分代收集算法,Java堆分为:新生代,老生代
- 物理上不连续的空间,逻辑上连续。
(5)**方法区:**线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量。 - 运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,
- 直接内存:直接内存不受到Java堆的大小限制
二、内存溢出
-
如果对象数量>堆的最大数量时,产生内存溢出。
先弄清楚是内存泄漏还是内存溢出:
-
如果是内存泄漏:找到泄漏的位置
-
如果是内存溢出:查看哪些对象的生命周期过长, 持有时间过长
-
虚拟机栈和本地方法栈溢出:多线程的使用
-
方法区和运行时常量溢出:方法区存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。
三、内存泄漏 垃圾收集器与内存分配策略
哪些内存需要回收?
-
引用计数算法:给每个对象添加一个引用计数器,有对象引用它,计数器加1,当引用失效时,计数器减1.计数器为0的对象就是不可能再被使用的。
-
可达性分析算法
通过一系列成为”GC Roots"对象作为起始点,从这些节点开始向下搜索,搜索所走的路径称为引用链,当一个对象到GC Roots没有路径时,证明该对象不可用。可以作为GC Roots的对象包括:- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
引用类型
强引用:Person A = new Person();只要强引用在,垃圾收集器不会回收它。
软引用:用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在发生内存溢出之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference类来实现软引用。(在内存溢出之前回收)
**弱引用:**用来描述非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会收掉只被弱引用关联的对象。通过WeakReference类来实现(垃圾收集器工作时,就回收)
虚引用:幽灵引用或者幻影引用,是最弱的一种引用关系。无法通过虚引用来取得一个对象实例,目的:这个对象被收集器回收时收到一个系统通知。
生存还是死亡? 调用finalize方法
回收方法区:永生代的垃圾收集分为:废弃常量和无用的类。
判断是废弃常量:
如果常量池中有字符串“abc”,但是系统中没有一个字符串对象指向它,需要回收它
判读是无用的类:-
该类的所有实例都被回收,Java堆中不存在该类的实例
-
加载该类的ClassLoad已被回收
-
该类对应的java.long.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集算法:
(1)标记-清理:标记出所有需要清理的对象,然后统一清理.
缺点:效率低;产生碎片问题。
(2) 复制算法: 将内存分成两部分A和B,当A用完时,将还存活的对象放在B中,然后把A全部删除。
回收新生代的方法:实际应用:将内存分成A、B1、B2,(B1 = B2)比例:8:1:1.先使用A和B1,当使用完时,将存活的对象放在B2中。因为新生代每次都有很多对象死去,只有少量存活.
设置两个Survivor区最大的好处就是解决了碎片化。
,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
(3)标记-整理方法:老生代处理方法:让所有存活的对象移向一端,然后清理掉端以外的内存。因为它存活率高。
四、垃圾收集器
(1)Serial收集器
单线程的收集器,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。对于运行在Client模式下的虚拟机是一个很好的选择。
(2)ParNew收集器
Serial收集器的多线程版本。运行在Server模式下的虚拟机中首选的新生代收集器。
(3)Parallel Scavenge收集器
新生代收集器,复制算法的收集器,并行的多线程收集器。
停顿时间短,响应速度快,高吞吐量,主要适合在后台运算而不需要太多交互的任务。
(4)Serial Old收集器
是Serial收集器的老年代版本,单线程收集器,“标记-整理”算法。
(5)Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
(6)CMS收集器
以获取最短回收停顿时间为目标的收集器,重视服务的响应速度。“标记-清除”算法。分为四步:初始标记,并发标记,重新标记,并发清除。
初始标记:标记GC Roots能直接关联到的对象,速度快。
并发标记:进行GC Roots Tracing的过程
重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
在并发标记和并发清理时让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更畅,对用户程序的影响会显得少一些。
缺点:因为它是基于“标记-清除”算法实现的,所以收集结束后会产生大量空间碎片。
(7)G1收集器:
G1收集器比CMS收集器有两个显著的改进:
(1)G1收集器是基于“标记-整理”算法
(2)它可以非常精确地控制停顿,即能让适用者明确指定在一个长度为M毫秒地时间片段内,消耗在垃圾收集上地时间不得超过N毫秒,这几乎已经是实时Java地收集器地特征了。
G1将整个Java堆划分为多个大小固定地独立区域,并且跟踪这些区域里面地垃圾堆积成都,在后台维护一个优先列表,每次根据允许地收集时间,优先回收垃圾最多地区域。
内存分配和回收策略
自动内存管理地两个问题:给对象分配内存、回收分配给对象地内存。
(1)对象优先分配在Eden
(2)大对象直接放在老年代:为了避免在Eden区以及两个Survivor区之间发生大量地内存复制
(3)长期存活地对象放在老年代,对象年龄计数器,每经过一个垃圾回收存活下来了,年龄就加1,age>=15时,进入老年代
(4)动态对象年龄判定。如果在Survivor空间中相同年龄所有对象大小地总和>Survivor空间地一半,年龄>=该年龄地对象直接进入老年代。
五、虚拟机类加载机制
Class文件:一组以8位字节位基础单位的二进制流,
常量池:存放两大类常量:字面量和符号引用。
字面量:被声明为final的常量值
类加载过程:
加载
连接:验证、准备、解析
初始化
类的加载指:将类的class文件读入内存,并为之创建一个java.lang.Class对象。也就是说,当程序中使用任何类时,系统都会为之创建一个java.lang.Class对象,作为方法区这些数据的访问入口。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,来源有:
- 从本地文件系统加载class文件
- 从jar包加载class文件,JDBC编程时用到的数据库驱动类放在Jar文件中,JVM可以从Jar文件中直接加载该class文件。
- 通过网络加载class文件
- 把一个java源文件动态编译,并执行加载。
类的连接:
类被加载时候,系统为之生成对应的Class对象,接着将会进入连接阶段:负责把类的二进制数据合并到JRE中。
类的连接分为三个阶段:
(1)验证:用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
(2)准备:负责为类变量分配内存,并设置默认初始值
(3)解析:将类的二进制数据中的符号引用替换成直接引用。
类的初始化
主要是对类变量进行初始化。对类对象初始化的方式有两种:
(1)声明类变量时指定初始值
(2)使用静态初始化块为类变量指定初始值
当程序主动使用任何一个类时,系统会保证该类以及所有的父类都会被初始化。
类初始化的时机:
当程序首次通过下面6中方式来使用某个类或接口时,系统会初始化该类和接口。
(1)创建类的实例:方式有:使用new操作符创建实例,通过反射来创建实例,通过反序列化来创建实例。
(2)调用某个类的类方法(静态方法)
(3)访问某个类或接口的类变量,
(4)使用反射方式来强制创建某个类
类加载器:
启动类加载器:Bootstrap ClassLoader,将lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。
扩展类加载器:Extension ClassLoader,负责加载lib\ext目录中的类库,开发者可以直接使用扩展类加载器
应用类加载器:Application ClassLoader。负责加载用户类路径上所指定的类库,程序中默认的类加载器。
高效并发:
缓存一致性
如何保证进程内缓存的数据一致性?
答:保障进程内缓存一致性,有三种方案。
第一种方案
可以通过单节点通知其他节点。如上图:写请求发生在server1,在修改完自己内存数据与数据库中的数据之后,可以主动通知其他server节点,也修改内存的数据。如下图:
这种方案的缺点是:同一功能的一个集群的多个节点,相互耦合在一起,特别是节点较多时,网状连接关系极其复杂。
第二种方案
可以通过MQ通知其他节点。如上图,写请求发生在server1,在修改完自己内存数据与数据库中的数据之后,给MQ发布数据变化通知,其他server节点订阅MQ消息,也修改内存数据。
这种方案虽然解除了节点之间的耦合,但引入了MQ,使得系统更加复杂。
前两种方案,节点数量越多,数据冗余份数越多,数据同时更新的原子性越难保证,一致性也就越难保证。