了无艺撸心有所爱者,忘死.什么意思

?相信对Java编程有了一定程度了解嘚同学多多少少都已经听说过、了解过Java虚拟机。就算你还未开始学习Java编程但已经打算计划去学习那你也肯定听说过一本书《深入理解Java虛拟机 JVM高级特性与最佳实践 》。在我当时正计划踏入Java这个大家庭的时候我也提前买了一堆书籍,其中就有它直到两年后,我对自己技術的不满足并且制定了一系列计划之后我才重新翻开这本书。过去的我对JVM的了解仅仅只是冰山一角从这里开始我们一起慢慢去进入Java虚擬机真实的世界。
?相信大家学习Java那就一定对Java语言为何保持着优势并且经久不衰十分清楚其中的一个配件功不可没-JVM,这是塑造了Java代码一處编译、处处运行的根本如果对其他编程语言,例如C或者C++有了解的同学应该能够很清楚在C和C++当中对于内存的管理,开发者拥有至高无仩的权利但同时开发者也需要对其中每一对象从创建到销毁都进行维护。
?而对于Java开发者来说虚拟机的自动内存管理机制能够在大部汾情况下都帮助我们管理好我们所使用的内存,对于已经十分成熟的虚拟机技术很少会出现内存泄漏以及内存溢出的问题这对于我们Java开發者来说简直就是一个十分愉悦的事情。但与此同时这样完善的内存管理机制也是一把利刃。一旦出现内存泄漏或者溢出等问题若我們对虚拟机真正的管理机制一知半解甚至不清楚,那问题将会十分难以解决甚至是致命的
?这里先把整个JVM系列的代码和脑图分享给大家。代码地址:百度脑图:。

1.探索虚拟机的内存区域

?当我们运行一个Java程序的时候JVM会将Java程序在运行过程中所用到的内存进行管理起来,將其分成若干个不同的数据区域每个区域都有其独特定义、用途以及特性。这里我们先来看一下程序运行时数据区是如何分配的

?对於运行时数据区的分配,从JDK8开始还进行了一些改变这里所展示的运行时数据区的结构是以我们平时使用的HotSpot虚拟机为例,其他虚拟机在结構上可能存在稍微的偏差对整体理解不会造成影响。

?这里我们可以很清晰地看到JDK1.8起彻底将方法区移除并替换成了直接内存中新增加的え空间另外把运行时常量池放在了堆内分配。
?另外每个数据区分布也不同主要区别是线程共享以及线程私有。这里我们大概看一下下面会对每个数据区单独介绍。

  1. 线程隔离/私有:虚拟机栈、本地方法栈、程序计数器

1.2.1 程序计数器是什么

?程序计数器实际上是一块较尛的内存空间,我们可以把它看作是当前线程所执行的字节码的行号指示器字节码解释器运行时主要就是通过修改当前线程的程序计数器的值,来依次读取需要执行的字节码指令我们程序中的分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器来完荿的。

1.2.2 为什么使用程序计数器

?我们都知道线程是一个独立的执行单元,由CPU所控制执行的对操作系统有了解的同学应该听说过时间片,其实在我们应用程序运行过程中每个线程都被会分配一个时间段也就是CPU分配给各个程序运行的时间。当我们同时运行多个应用程序时其实本质上是每个线程不停轮流切换去使用CPU的资源,看似是多个应用程序在同时执行其实本质上若只有一个CPU(内核),则一次只能处理一個时间片中的指令
?所以为了线程来回切换时能够恢复到上一次正确的执行位置,每个线程都会自己维护一个独立的程序计数器独立存储且不受其他线程的影响,是一块线程私有的内存空间

?这里我们根据实际情况来做一个讲解。这里我们先定义一个简单的User

?我們对其进行编译之后,通过javap -l查看编译后的字节码文件
?这里可以看到每一个方法上开始都会有对应的行号,这就是我们程序计数器所需偠记录的数据
?例如我们在时间片1时,CPU将资源分配给了线程1此时线程1执行到了getAge()方法的位置,CPU将时间片分配给了线程2此时线程1的程序計数器就会记录当前getAge()所在行。并将时间片切换至线程2
?线程2在时间片2内又执行到了getName()的位置,此时线程2的程序计数器也会将getName()的位置记录下來并切换到时间片3。假设时间片分配是均匀的则时间片3时线程1恢复执行,这时就需要使用到之前线程1的程序计数器所记录的位置用以恢复到上一次线程1所执行的字节码所在行多个线程之间就是这样来回切换运行的。
?这里因为线程正在执行的是一个Java方法所以这个计數器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)
?由于程序计数器是JVM默认分配的鈈由开发者控制,它的生命周期随着线程的创建而创建随着线程的结束而死亡。所以程序计数器也是唯一一个不会出现OutOfMemoryError的内存区域

1.3.1 虚擬机栈是什么?

?Java 虚拟机栈与程序计数器一样也是线程私有的。它的生命周期也和线程生命周期相同每个线程都有各自的Java虚拟机栈,並随着线程的创建而创建随着线程的死亡而死亡。它主要负责作用于Java方法执行的一块内存区域每次方法调用都是通过栈传递的。
?Java内存在粗略上可以分为堆内存(Heap)以及栈内存(Stack)栈内存指的就是这里说的虚拟机栈。

1.3.2 为什么使用虚拟机栈

?实际上,Java 虚拟机栈是由一个个栈帧(Stack Frame)組成的每个栈帧中都包括:局部变量表、操作数栈、动态链接、方法出口等信息。每个方法在执行的同时都会创建一个栈帧方法从调鼡到执行再到完成的过程,就对应着一个栈帧在虚拟机栈中的入栈到出栈的过程
?而其中的局部变量表主要存放了编译器可知的各种数據类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

1.3.3 方法是如何调用的?

?这里我们先贴一段十分简单的代码

?一段很简单的代码,大家都知道这里会先输絀methodA再输出methodB那么对于Java虚拟机来说是如何执行的呢。
?当我们调用Main方法时会为其创造一个栈帧1压入虚拟机栈中在Main方法执行过程中调用了方法B,此时虚拟机栈又会为methodB创造一个栈帧入栈接着调用方法A同理。
?可以看出虚拟机栈和我们平时所接触的数据结构中的栈十分相似Java虚擬机栈中保存的主要内容就是栈帧,每个方法的调用都会创建一个对应的栈帧压入虚拟机栈中每个方法执行结束都会将对应的栈帧弹出。
?在Java方法中主要有两种返回方式可以使栈弹出:

  1. StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展那么当线程请求栈的深度超过当前 Java虚拟机栈嘚最大深度时就抛出StackOverFlowError异常。
  2. OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展当线程请求栈时内存完全耗尽无法动态扩容时此时抛出OutOfMemoryError异常。

?上面這段代码中方法B对自己重复地调用,就会创建N个栈帧去压入虚拟机栈中因为没有终止调用的条件,所以最终线程请求的栈深度就大于虛拟机所允许的深度就会抛出StackOverflowError异常。

1.4.1 本地方法栈是什么

?上面我们介绍了Java虚拟机栈,而本地方法栈与其作用十分相似它们之间的区別主要是:Java虚拟机栈为虚拟机执行Java方法(字节码)存在,而本地方法栈则为虚拟机执行所需的Native方法存在以我们平时使用频率比较高的String为例,佷多类中都提供了native方法这些方法很多都是通过一些其他语言实现,而本地方法栈主要就是用于本地方法执行的一块内存区域
?虚拟机規范中对于本地方法栈中方法所使用的语言、数据结构以及使用方式都没有强制性的规定,所以各种虚拟机都可以根据所需自由设计实现像我们现在一般使用到的HotSpot虚拟机,设计团队认为其二者功能大致相同就直接将虚拟机栈和本地方法栈合二为一。

1.4.2 为什么使用本地方法棧

?与Java虚拟机栈相同,本地方法栈中每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法执行完成后相应的栈帧也会出栈并释放内存空间

?Java堆是Java虚拟机所管理的内存中最大的一块区域,并且是所有线程共享的一塊内存区域此内存区域的唯一目的就是为了存放对象实例,几乎所有的对象实例以及数组都在Java堆中分配内存
?因为Java堆是虚拟机中最大嘚一块内存区域,所以也是垃圾收集器主要管理的区域因此也被称作GC堆(Garbage Collected Heap)
?了解垃圾回收机制的同学应该清楚由于现在收集器基本都采用了分代垃圾收集算法,所以Java堆还能进一步细分为新生代老年代新生代老年代再进一步划分还能氛围EdenFrom SurvivorTo Survivor等区域。每一层更细致地划分都是为了更好地去分配或者回收内存

?这里我们通过jdk中提供的jmap命令就可以看到我们应用程序的内存分配情况了。
?这里我们清楚地看到新生代中的EdenFrom SpaceTo Space各个区域的内存分配情况大多数情况下,对象都会在EdenFrom区域中分配在虚拟机进行一次新生代的垃圾回收后如若Eden区中的对象依然存活,则会将其移至To区;而From区中仍然存活的对象则会根据其年龄决定去处每次GC后仍存活的对象年龄都会增加1,当到达┅定的阈值后则会移至老年代中进行管理我们可以通过-XX:MaxTenuringThreshold指定这个阈值。
?我们通过查阅一些资料可以了解到根据中的规定,Java堆在物理仩可以是一段不连续的内存空间只要在逻辑上是连续的即可。这点和我们平时使用的磁盘空间有点类似而当下比较主流的Java虚拟机设计吔都是支持扩展的(-Xmx、-Xms等参数控制) 。若堆中不足内存去分配实例并且也无法再扩展时也是会抛出OutOfMemoryError异常的。

1.6.1 方法区是什么

?方法区也是线程共享的一块内存区域,主要用于存储已被虚拟机加载的类信息(例:类版本号、方法、接口等)、常量、静态变量、即时编译器编译后的代碼等数据虽然在中将方法区描述为堆的一个逻辑部分,但是它却有一个别名-Non-Heap(非堆)在某种意义上应该还是与Java堆区分开。

?仔细阅读过会發现在中只规定了方法区的概念和作用并没有明确规定方法区如何实现。对于我们平时在HotSpot虚拟机上编写程序的开发者来说其实更多的會将方法区称为永久代,这是因为HotSpot虚拟机开发团队在设计时为了省事儿不想专门给方法区编写内存管理的代码所以将GC分代收集延用到了方法区上。
?而对于另外的虚拟机例如JRockit、J9VM等其实并不存在永久代这个概念。我们开发时肯定都使用过日志方法区和永久代的关系就好仳slf4jlog4j,虽然我也不知道这种类比合不合适但大家理解就可以。方法区就像slf4j仅仅是制订了一套规范而永久代就是HotSpot虚拟机根据方法区所制訂规范去进行的一个具体的实现。一个是规范、标准另一个是实现。
?所以永久代这个概念仅仅是存在我们使用的HotSpot虚拟机

?我们之湔也提到了,其实HotSpot虚拟机的设计团队设计出永久代有一部分是为了省事儿是骡子是马总要牵出来溜溜才知道。在经过岁月的冲洗后发現这并不是一个好主意。这样的设计使开发者更容易遇到内存溢出的问题因为永久代受限于我们设置的一个阈值,而其他的虚拟机产品呮要没有越过进程可用的内存上限即可
?所以在后续的JDK版本中,HotSpot逐渐将永久代淡化了直到JDK1.8就完全将永久代给移除,取而代之在直接内存中新设计了一块元空间
?由于永久代受限于JVM本身设置固定的大小,无法进行调整而元空间使用的是直接内存,受本机可用内存的限淛我们可以使用-XX:MaxMetaspaceSize设置最大元空间大小,默认值为unlimited也就是说它只受系统内存的限制会根据运行时应用程序所需动态调整大小。

1.6.4 永久代囷元空间的配置

?在JDK1.8之前虽然永久代会受限于设置的大小,但是我们还是可以通过一些参数去调解的只不过对于这个大小的把控需要┿分了解虚拟机的内存分配机制并根据应用程序的实际来设置才能达到利益最大化。
?相对而言垃圾收集行为在方法区中出现频率还是仳较低的,但并非数据进入方法区后就和永久代字面一样“永久存在”了这区域的内存回收目标主要是针对常量池的回收和对类型的卸載。
?从JDK1.8起HotSpot彻底移除了永久代而使用元空间。虽然元空间使用的是直接内存若不指定最大上限则虚拟机可能会耗尽所有的系统内存。峩们也可以通过-XX:MetaspaceSize指定MetaSpace元空间的初始大小以及-XX:MaxMetaspaceSize设置MetaSpace元空间的最大大小。

1.7.1 运行时常量池是什么

?运行时常量池属于方法区的一部分。我们嘚Class文件中除了有类的版本、字段、方法、接口等描述信息外还包含用于存放编译期生成的各种字面量和符号引用等信息,这部分内容会茬类加载后在运行时常量池中存放

  1. 字面量:文本字符串、final常量、基本数据类型的值等。
  2. 符号引用:类以及结构的完全限定名、字段名称囷描述符、方法名称和描述符

1.7.2 运行时常量池其实就在我们身边

?大家有时候从一些概念上总觉得他们虚无缥缈,离我们很远其实这些東西一直就在我们身边,我们先来看看下面这段代码

?上面这段代码可能大家都见过,面试有时也会碰见但是开发时间久对基础不是佷扎实或者刚开始接触开发的同学对其中的原理就会比较模糊,这里我们借助上图去了解就会变得十分简单
?当我们声明ab变量时直接將字符串"jvm"给其赋值,此时对于JVM内存的分配其实是在方法区里的运行时常量池中维护了一个基于HashSet实现的StringTable全局字符串常量池所以变量ab其实引用同一块内存空间。
?而当我们声明变量c时采用的是new一块新的内存空间,此时会在堆中申请一块新的空间去存放"jvm"字符串内容所以此時变量c与其他两个变量引用的内存空间不相同。但是当我们使用intern()方法后进行内存地址比较又变为相同的,这是因为运行时常量池有一个偅要的特性那就是具备动态性由于Java语言没有要求常量一定只会在编译期间产生,运行期间也可将新的常量加入池中String类中的intern()方法就是如此。

1.7.3 运行时常量池是否也会发生异常

?我们都知道运行时常量池既然属于方法区的一部分,那么自然会受到方法区内存的限制当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。
?另外需要注意的是从JDK1.7开始JVM就将运行时常量池从方法区中移除出来了,并在Java堆中开辟了一块区域鼡于存放运行时常量池

?直接内存又称堆外内存,直接内存并不属于虚拟机运行时数据区的一部分所以JVM并不会对其内存空间进行分配囷管理。
的I/O方式它可以直接调用Native函数库对堆外内存进行直接分配,然后通过存储在Java堆中的DirectByteBuffer对象对这块内存的引用进行操作这样可以在某些场景中显著地提高性能,因为其避免了Java堆Native堆之间来回复制数据的过程
?直接内存的分配并不会受到Java堆的限制,但是会受到本机总內存大小以及处理器寻址空间的限制所以这部分内存在没有合理使用的情况下也会导致OutOfMemoryError异常的出现。

?ps:这里还是推荐大家有时间读一讀《深入理解Java虚拟机-JVM高级特性与最佳实践 》这本书虽然与最新的技术有一些差异,但是其中除了对JVM有详细的讲解还对Java语言以及虚拟机嘚发展历史有不错的介绍。另外有时间的同学也可以去阅读一下增进自己对Java虚拟机的理解和使用。

}
与SQL不同的是HQL是面向对象的查询,查询的是对象和对象中的属性 注意:HQL中的关键字不区分大小写但是类名和属性名区分大小写
投影,就是只是查询部分字段


1.交叉连接 等效 sql 笛卡尔积
1.思想:将HQL从java源码中,提取到配置文件中


}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信