本文基于周志明写的《深入理解java虚拟机》,汇聚书本知识点和常见面试题
电子版链接:https://pan.baidu.com/s/1BnVE5yeI60jbR-wxLDGgZw
虚拟机自动内存管理机制作用:我们new操作后,不需要手动释放内存,不容易出现内存泄露和内存溢出
java虚拟机在执行java程序的过程中会把管理的内存划分位若干个不同的区域:方法区;虚拟机栈;本地方法栈;堆;程序计数器
java内存模型
内存区域–程序计数器:
java虚拟机栈:
本地方法栈:
java堆:
作用:所有new出来的对象内存都分配在堆中,堆被分成年轻代(Yong Generation)和老年代(Old Generation),新生代又分成Eden和两个Survivor区。
引申概念:
java堆是java虚拟机所管理的内存中最大的一块
java堆是所有线程共享的一块内存区域,在虚拟机启动时创建
所有的对象实例以及数组都要在堆分配,但随着jit编译器的发展和逃逸分析技术成熟,栈上分配、标量替换优化技术导致对象分配在堆变得不那么“绝对”
堆的分类:堆是垃圾回收器管理主要区域,所以也称GC堆,从内存回收角度,垃圾收集器基本采用分代收集算法,所以java堆还可以细分新生代和老年代,细致一点还有Eden空间、From Survivor空间、To Survivor空间等;从内存分配角度看,线程共享的java堆可能划分出多个线程私有的分配缓存区。堆的分类与内容无关,无论那个区域储存的都是对象实例,划分只是更好回收更快回收内存
java堆可以是物理上不连续的内存空间,只要逻辑连续就可以了
-Xmx和-Xms控制虚拟机可拓展
在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例
JVM运行时堆的大小
-Xms堆的最小值
-Xmx堆空间的最大值
新生代堆空间大小调整
-XX:NewSize新生代的最小值
-XX:MaxNewSize新生代的最大值
-XX:NewRatio设置新生代与老年代在堆空间的大小
-XX:SurvivorRatio新生代中Eden所占区域的大小
永久代大小调整
-XX:MaxPermSize
其他
-XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收
方法区:
非虚拟机内存区域–直接内存:
对象引用和对象之间的访问方式有两种:使用句柄和直接指针
句柄访问方式:java堆中会划分一块内存作为句柄池,对象引用中储存的就是对象的句柄地址,而句柄中包含了对象实例数据和对象类型数据各自的具体地址信息
指针访问方式:java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,对象引用直接存储对象地址
句柄好处:对象被移动时(垃圾收集时移动对象很频繁)只改变句柄中实例数据指针,而对象引用本身不需要被修改
直接指针好处:速度快,节省了一次指针定位开销
出现OutOfMemoryError:java.lang.OutOfMemoryError: Java heap space解决方法。首先确认内存中的对象是否必要(分清楚内存泄露还是内存溢出)–使用Eclipse Memory Analyzer打开堆转存快照文件。如果是内存泄露,进一步通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们。如果不存在泄露,那就是内存对象还必须存活,那就检查下虚拟机堆参数(-Xmx与-Xms)与机器物理内存对比看是否还可以调大;从代码上看是否某些对象生命周期过长、持续状态时间过长的情况,尝试减少程序运行期内存消耗
hotspot虚拟机不区分本地方法栈和虚拟机栈,-Xoss参数(设置本地方法栈大小)虽然存在,但无效;栈容量只由-Xss参数设定
在单个线程下,无论是栈帧太大还是虚拟机容量太小,当内存无法分配时,虚拟机抛出的都是StackOverflowError异常;在多线程中,不断建立线程能造成内存溢出
在开发高并发的项目时,出现栈超出异常有错误堆栈可以阅读比较容易找问题,但如果是建立过多线程导致内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大对和减少栈容量换取更多的线程。
参数代码:
虚拟机栈、本地方法栈、程序计数器随线程而生灭;唯独java堆和方法区这部分的内存动态需要监管
如何判断一个对象是否存活:引用计数算法、根搜索算法
GC Roots对象有那些:
引用概念拓展:
根搜索算法不可到达的对象,不一定马上死去,需要经过两次标记过程。当发现不可达,会标记第一次并进行一轮筛选是否有必要执行finalize()方法;如果有必要执行,这个对象会被放到F-Queue队列排队中等待死亡(根据优先级调用finalize()方法),如果期间重新被引用,则逃脱死亡,如果没必要执行,没有说明~~
一个对象的finalize只会执行一次,也就是只能自救一次
finalize()运行代价高,不确定性大
新生代进行一次垃圾收集能回收70%-95%内存,而永久代远远低于次
永久代回收两部分内容:废弃常量和无用的类
如何判定废弃常量:一个String”abc”进入常量池,当没有任何对象引用它,就会被请出常量池
如何判断无用的类:该类所有实例已经被回收;加载该类的ClassLoader被回收;该类的java.lang.Class对象没有任何地方引用,即没有反射这个类—-满足上面的条件就可以被回收,但不是必然被回收3
垃圾收集算法:
垃圾收集器
Serial收集器:
ParNew收集器
Parallel Scavenge收集器
Serial Old收集器
Parallel Old收集器
CMS收集器
G1收集器
内存分配和回收策略
(1)对象优先分配在Eden,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
(2)大对象(比如很长的字符串、数组)直接进入老年代,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存) (提醒一句,编程时少创建“短命大对象”,大对象容易让内存还有不少的时候触发GC来安置他们)
(3)长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
(4)幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代
(5)空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
java垃圾回收
GC介绍
Minor GC
在年轻代Young space
(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC. 这个定义既清晰又无异议。 但仍有一些有趣的关于Minor GC事件的东西你需要了解:
综上所述,Minor GC
概念相当清晰 – 每次Minor GC只会清理年轻代.
Major GC 清理年老区(Tenured space).
Full GC 清理整个内存堆 – 既包括年轻代也包括年老代
创建新线程时,JVM会为这个线程创建一个栈,同时分配一个PC寄存器(指向第一行可执行的代码)。调用新方法时会在这个栈上创建新的栈帧数据结构。执行完成后方法对应的栈帧将消失,PC寄存器被销毁,局部变量区所有值被释放,被JVM回收。
(1)类加载器:JVM启动时或者类运行时将需要的class加载到JVM中。每个被装载的类的类型对应一个Class实例,唯一表示该类,存于堆中。
(2)执行引擎:负责执行JVM的字节码指令(CPU)。执行引擎是JVM的核心部分,作用是解析字节码指令,得到执行结果(实现方式:直接执行,JIT(just in time)即时编译转成本地代码执行,寄存器芯片模式执行,基于栈执行)。本质上就是一个个方法串起来的流程。每个Java线程就是一个执行引擎的实例,一个JVM实例中会有多个执行引擎在工作,有的执行用户程序,有的执行JVM内部程序(GC).
(3)内存区:模拟物理机的存储、记录和调度等功能模块,如寄存器或者PC指针记录器。存储执行引擎执行时所需要存储的数据。
(4)本地方法接口:调用操作系统本地方法返回结果
早期(编译器):
很少;编译时,为节省常量池空间,能确定的相同常量只用一个引用地址。
晚期(运行期):
方法内联:去除方法调用的成本;为其他优化建立良好基础,便于在更大范围采取连续优化的手段。
冗余访问消除:公共子表达式消除
复写传播:完全相等的变量可替代
无用代码消除:清除永远不会执行的代码
(1)公共子表达式消除(语言无关):如果公共子表达式已经计算过了,并且没有变化,那就没有必要再次计算,可用结果替换。
(2)数组边界检查消除(语言相关):限定循环变量在取值范围之间,可节省多次条件判断。
(3)方法内联(最重要):去除方法调用的成本;为其他优化建立良好基础,便于在更大范围采取连续优化的手段。
(4)逃逸分析(最前沿):分析对象的动态作用域;变量作为调用参数传递到其他方法中-方法逃逸;被外部线程访问-线程逃逸。
栈上分配-减少垃圾系统收集压力
同步消除-如果无法逃逸出线程,则可以消除同步
标量替换-将变量恢复原始类型来访问
小抄:final修饰的局部变量和参数,在常量池中没有符号引用,没有访问标识,对运行期是没有任何影响的,仅仅保证其编译期间的不变性。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
(1)Bootstrap ClassLoader(启动类加载器):完全由JVM控制,加载JVM自身工作需要的类(JAVA_HOME/lib)
(2)Extension ClassLoader(扩展类加载器):属于JVM自身一部分,不是JVM自身实现的(JAVA_HOME/lib/ext)
(3)Appclication ClassLoader(应用程序类加载器):父类是Extension ClassLoader,加载Classpath(用户类路径)上的类库
将Class文件加载到JVM中、审查每个类由谁加载(父优先的等级加载机制)、将Class字节码重新解析成JVM统一要求的对象(Class对象)格式。
.class->findclass->Liking:Class规范验证、准备、解析->类属性初始化赋值(static块的执行)->Class对象(这也就是为什么静态块只执行一次)
5.描述JVM类加载机制
ClassLoader首先不会自己尝试去加载类,而是把这个请求委托给父类加载器完成,每一个层次都是。只有当父加载器反馈无法完成请求时(在搜索范围内没有找到所需的类),子加载器才会尝试加载(等级加载机制、父优先、双亲委派)。
好处:类随着它的加载器一起具有一种带有优先级的层次关系;保证同一个类只能被一个加载器加载。
(1)隐式加载:继承或者引用的类不在内存中
(2)显式加载:代码中通过调用ClassLoader加载
(1)ClassNotFoundException:没有找到对应的字节码(.class)文件;检查classpath下有无对应文件
(2)NoClassDefFoundError:隐式加载时没有找到,ClassNotFoundException引发NoClassDefFoundError;确保每个类引用的类都在classpath下
(3)UnsatisfiedLinkError:(未满足链接错误)删除了JVM的某个lib文件或者解析native标识的方法时找不到对应的本地库文件
(4)ClassCastException:强制类型转换时出现这个错误;容器类型最好显示指明其所包含对象类型、先instanceof检查是不是目标类型,再类型转换
(5)ExceptionInitializerError:给类的静态属性赋值时
JVM中对象只有一份,不能被替换,对象的引用关系只有对象的创建者持有和使用,JVM不可干预对象的引用关系,因为JVM不知道对象是怎么被使用的,JVM不知道对象的运行时类型,只知道编译时类型。
但是可以不保存对象的状态,对象创建和使用后就被释放掉,下次修改后,对象就是新的了(JSP)。
(1)Java堆:存储Java对象
(2)线程:Java运行程序的实体
(3)类和类加载器:存储在堆中,这部分区域叫永久代(PermGen区)
(4)NIO:基于通道和缓冲区来执行I/O的新方式。
(5)JNI:本地代码可以调用Java方法,Java方法也可以调用本地代码
JVM是按照运行时数据的存储结构来划分内存结构的。
PC寄存器数据:严格来说是一个数据结构,保存当前正在执行的程序的内存地址。为了线程切换后能恢复到正确的执行位置,线程私有。不会内存溢出。
(1)Java栈:方法执行的内存模型,存储线程执行所需要的数据。线程私有。
–OutOfMemoryError:JVM扩展栈时无法申请到足够的空间。一个不断调用自身而不会终止的方法。
–StackOverflowError:请求的栈深度大于JVM所允许的栈深度。创建足够多的线程。
(2)堆:存储对象,每一个存在堆中Java对象都是这个对象的类的副本,复制包括继承自他父类的所有非静态属性。线程共享。
–OutOfMemoryError:对象数量到达堆容量限制。可通过不断向ArrayList中添加对象实现。
(3)方法区:存储类结构信息。包括常量池(编译期生产的各种字面量和符号引用)和运行时常量池。线程共享。
–OutOfMemoryError:同运行时常量池。
(4)本地方法栈:与Java栈类似,为JVM运行Native方法准备的空间。线程私有。(C栈)OutOfMemoryError和StackOverflowError同JVM栈。
(5)运行时常量池:代表运行时每个class文件中的常量表。运行期间产生的新的常量放入运行时常量池。
–OutOfMemoryError:不断向List中添加字符串,然后String.inern(),PermGen Space(运行时常量池属于方法区)。
(6)本地直接内存:即NIO。
–OutOfMemoryError:通过直接向操作系统申请分配内存。
(1)对象优先分配在Eden,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
(2)大对象(比如很长的字符串、数组)直接进入老年代,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存) (提醒一句,编程时少创建“短命大对象”,大对象容易让内存还有不少的时候触发GC来安置他们)
(3)长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
(4)幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代
(5)空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
通过可达性分析算法,通过一些列称为GC Roots的对象作为起始点,从这些起始点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达),则证明这个对象是不可用的。
使用可达性分析算法而不是引用计数算法。因为引用计数算法很难解决对象之间相互循环引用的问题。
(1)JVM栈(栈帧中的本地变量表)中的引用
(2)方法区中类静态属性引用
(3)方法区中常量引用
(4)本地方法栈中JNI(一般的Native方法)引用
把对象按照寿命长短来分组,分为年轻代和老年代,新创建的在老年代,经历几次回收后仍然存活的对象进入老年代,老年代的垃圾频率不像年轻代那样频繁,减少每次收集都去扫描所有对象的数量,提高垃圾回收效率。
(1)年轻代(Young区-1/4):Eden+Survior(From+To)(1/8,这个比例保证只有10%的空间被浪费,保证每次回收都只有不多于10%的对象存活)
(2)老年代(Old区 ):存放几次垃圾收集后存活的对象
(3)永久区(Perm区):存放类的Class对象
(1)标记-清除算法:首先标记处所要回收的对象,标记完成后统一清除。缺点:标记效率低,清除效率低,回收结束后会产生大量不连续的内存碎片(没有足够连续空间分配内存,提前触发另一次垃圾回收)。适用于对象存活率高的老年代。
(2)复制算法(Survivor的from和to区,from和to会互换角色):
将内存容量划分大小相等的两块,每次只使用其中一块。一块用完,就将存活的对象复制到另一块,然后把使用过的一块一次清除。不用考虑内存碎片,每次只要移动顶端指针,按顺序分配内存即可,实现简单运行高效。适用于新生代。
缺点:内存缩小为原来的一般,代价高。浪费50%的空间。
(3)标记-整理算法:
标记完成后,将存活的对象移动到一端,然后清除边界以外的内存。适用于对象存活率高的老年代。
(4)分代收集算法
根据对象存活周期不同把内存分为新生代和老年代,根据各个年代的特征采用最适当的收集算法;新生代有大批对象死去,少量存活采用复制算法;老年代存活率高,用标记整理/标记清除
CMS 收集器:Concurrent Mark Sweep 并发标记-清除。重视响应速度,适用于互联网和B/S系统的服务端上。初始标记还是需要Stop the world 但是速度很快。缺点:CPU资源敏感,无法浮动处理垃圾,会有大量空间碎片产生。
创建:
\1. 类加载检查
JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程。
\2. 对象分配内存
对象所需内存的大小在类加载完成后便完全确定(对象内存布局),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
根据Java堆中是否规整有两种内存的分配方式:(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)
指针碰撞(Bump the pointer)
Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。
空闲列表(Free List)
Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。
\3. 并发处理
对象创建在虚拟机中时非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
同步
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。
\4. 内存空间初始化
虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
注意:类的成员变量可以不显示地初始化(Java虚拟机都会先自动给它初始化为默认值)。方法中的局部变量如果只负责接收一个表达式的值,可以不初始化,但是参与运算和直接输出等其它情况的局部变量需要初始化。
\5. 对象设置
虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
\6. 执行init( )
在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。但是从Java程序的角度看,对象的创建才刚刚开始init()方法还没有执行,所有的字段都还是零。
所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。
访问定位:句柄或者直接指针。
串行垃圾回收器(Serial Garbage Collector)
并行垃圾回收器(Parallel Garbage Collector)
并发标记扫描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 GarbageCollector)
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
当标记的引用对象在tenured区域;
在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
jmap、jstack、jconsole。
加载:
在加载阶段,虚拟机主要完成三件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。
验证:
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:
1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
准备:
准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:
pirvate static int size = 12;
那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。
解析:
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化:
在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成。
什么时候 :eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等
对什么东西:从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象
做什么事情:能说出诸如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等;还能讲清楚串行、并行(整理/不整理碎片)、CMS等搜集器可作用的年代、特点、优劣势,并且能说明控制/调整收集器选择的方式
答:对象优先在新生代区中分配,若没有足够空间,Minor GC;
大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。如果对象在新生代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。
java内存模型(JMM)是线程间通信的控制机制.JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
\1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
\2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。**
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
JVM相关链接:
http://www.importnew.com/23792.html