java虚拟机学习笔记
一、java运行时数据区域
1、程序计数器
2、虚拟机栈:用于存储局部变量表、操作数栈、动态链接、方法出口等信息
3、本地方法栈:与虚拟机栈的区别是虚拟机栈是为虚拟机执行的java方法服务,本地方法栈是为虚拟机使用到的本地(native)方法服务
4、堆
5、元空间:用于存储被虚拟机加载的类型信息、常量、静态变量;运行时常量池属于元空间的一部分,用于存储各种字面量和符号引用
二、java对象存储布局
在hotspot虚拟机里,对象在堆内存中的存储布局可以分为三部分:对象头、实例数据(对象的实例数据就是在java代码中能看到的属性和他们的值)、对齐填充(因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能)
对象头包括两类信息,第一类是用于存储对象自身的运行时数据,比如哈希码、gc分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向时间戳等。
这部分数据称为mark word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
第二类是类型指针,即对象指向它的类型元数据的指针,jvm通过这个指针来确定该对象是哪个类的实例。
三、jvm判断对象是否存活的算法
1、引用计数法(无法解决对象之间相互循环引用的问题)
2、可达性分析法
这个算法的基本思路是通过一系列称为“GC Roots“的根对象作为起始节点集(GC Root Set),从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何路径可达,则该对象将被判定为可回收的对象。
在jvm里,可固定作为GC Roots中的对象包括以下几种:
1、在虚拟机栈中引用的对象,比如当前正在运行的方法所使用的参数、局部变量表、临时变量等
2、在方法区中类静态属性引用的对象,比如java类的引用类型静态变量
3、在方法区中常量引用的对象,比如字符串常量池里的引用
4、在本地方法栈中JNI(java Native方法)引用的对象
5、jvm内部的引用,比如基本数据类型对应的Class对象、系统类加载器
6、所有被同步锁(synchronized)持有的对象
在可达性分析算法中判定不可达的对象,也不是一定会被清理掉,在被标记为不可达后,会判断此对象是否有必要执行finalize()方法,假如该对象没有重写finalize方法或该对象的finalize方法已经被jvm调用过一次了,则该对象会被回收,否则就执行该对象的finalize方法,如果该对象在finalize方法中重新与引用链关联上,则该对象本次就不会被回收了。(下次不可达时就会被回收,因为已经执行过finalize方法了)
从根节点枚举,是必须要暂停用户线程的(STW),当然也有OopMap、安全点、安全区域、记忆集与卡表、写屏障等种种措施来降低枚举根节点时(可达性分析)的stw时长
四、垃圾回收算法
1、标记-清除算法
标记出所有需要回收的对象,再统一回收掉所有被标记的对象,标记过程就是判定对象是否属于垃圾的过程(对象不可达)
该算法会产生大量的内存碎片
2、标记-复制算法(新生代使用)
基本思路是将内存划分为大小相等的两部分,每次只使用一部分,当这一部分用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次性清理掉。
该算法没有内存碎片问题,但如果内存中多数对象是存活的,就会产生大量的内存间复制的开销,并且内存每次只有一半在使用,利用率太低。
由于新生代的对象有98%熬不过第一轮垃圾回收,因此并不需要按照1:1的比例来划分新生代内存空间。
而是划分为一块Eden和两块survivor空间,每次分配内存只使用eden和一块survivor(fromSurvivor),垃圾收集时,将eden和survivor仍存活的对象复制到另一个survivor(toSurvivor)上,然后直接清理掉eden和survivor(fromSurvivor)空间,然后fromSurvivor变成toSurvivor, toSurvivor变成fromSurvivor。在hotspot中,eden和survivor默认比例是8:1,
当toSurvivor的空间不足以容纳垃圾回收后(minor GC/Young GC)存活的对象时,就要依赖其他内存区域(大多数是老年代)进行分配担保
3、标记-整理算法(老年代使用)
先标记所有存活的对象,然后将所有存活的对象向内存空间的一端移动(移动对象期间需要stw),再清理掉边界以外的内存。
补充:垃圾回收名词解释
部分收集(Partial GC):
新生代收集(Minor GC/Young GC)
老年代收集(Major GC/Old GC)
整堆收集(Full GC)
五、垃圾回收器
新生代:
1、serial(在jdk9中,已经取消serial与cms的配合工作)
2、parNew(目前只有parNew能和cms配合工作)
3、parallel scavenge(吞吐量优先收集器)
老年代:
1、cms
2、serial old
3、parallel old
同时工作在新生代与老年代:
G1
CMS收集器(concurrent Mark Sweep),工作在老年代,使用标记-清除算法,
工作过程分为四个步骤
1、初始标记(需要stw)
2、并发标记
3、重新标记(需要stw)
4、并发清除
初始标记仅仅只是标记下gc roots能直接关联到的对象,速度很快;
并发标记阶段就是从gc roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户线程;
重新标记阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的
停顿时间会比初始标记稍长一些,但比并发标记时间短的多。
并发清除阶段清理掉被判定已死亡的对象。耗时也较长,但没有停顿用户线程。
Cms缺点:
1、并发阶段,虽然不会导致用户线程停顿,但是会因并发而占用一部分线程(或者说是cpu的计算能力)而导致应用程序变慢,降低总吞吐量。
Cms默认启动的回收线程数是(cpu核数 + 3)/4,当cpu核数是4或以上时,cms并发回收线程占用不少于25%的cpu资源,如果cpu核数小于4,这个比值
就较高了,cpu压力变大。
2、CMS无法处理浮动垃圾,有可能出现“concurrent mode failure“进而导致一次完全的stw的full GC的产生。
在cps并发标记和并发清理阶段,用户线程继续运行,自然就可能会有新的垃圾对象不断产生,但这一部分垃圾是出现在标记过程结束以后,cms无法在本次收集中
处理掉它们,只能等待下一次垃圾收集,这就是所谓的浮动垃圾。同样由于垃圾收集阶段用户线程还在持续运行,所以必须预留足够的内存空间提供给用户线程。
因此cms并不会等到老年代几乎完全满了再进行收集。在jdk6之后,cms默认是老年代内存使用比92%(-XX:CMSInitiatingOccupancyFraction)之后开始垃圾回收,如果cms预留的内存(8%)无法满足用户线程时,就会出现一次“concurrent mode failure“,这时jvm就会冻结用户线程执行,临时启用serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
所以-XX:CMSInitiatingOccupancyFraction设置得太高容易导致大量的并发失败产生,性能反而降低,设置得太低会导致cms更容易触发垃圾回收,也不好,生产环境需要根据实际情况把握
3、内存碎片问题。Cms采用标记-清除算法,会有内存碎片问题,内存碎片过多时,会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的
连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,cms提供了-XX:+UseCMS-CompactAtFullCollection的开关参数(默认是开启的,jdk9废弃),
用于在cms不得不进行full gc时开启内存碎片的合并整理过程(会将所有存活对象移动到内存一端,需要stw)
G1收集器(Garbage First收集器)
保留了新生代和老年代概念,整个堆内存划分为若干个均等region区域(相当于新生代划分为若干个region,老年代划分为若干个region),G1从整体上来看是基于
标记-整理算法,从局部看(两个region之间)是基于标记-复制算法,不管如何,都不会产生内存碎片问题,有利于程序长时间运行,在程序为大对象分配内存时不容易
出现因无法找到连续内存而提前触发full gc的情况
G1适合工作在堆内存较大的配置下(8G或以上),cms适合堆内存较小(8G以下)。
就内存占用来说,虽然G1和CMS都采用卡表来处理跨代引用问题,但G1的卡表实现更复杂,堆中的每个region都必须有一份卡表,这导致G1的记忆集可能会占用整个堆容量的20%或以上。
六、内存分配
新建的对象优先在eden区分配
当eden区没有足够的空间进行分配时,将触发一次minor GC(Young GC),
比如针对-Xms20M,-Xmx20M、-Xmn10M的配置,即java堆大小20mb,新生代10mb(eden 8mb,survivor 1mb),老年代10mb,
有三个2mb大小和一个4mb大小的对象,假设先分配了三个2mb大小的对象,当分配4mb的对象时,由于eden区已经占用了6mb,还剩2mb,不足以分配该对象,此时将会发生一次Minor GC,gc期间jvm发现已分配的三个2mb对象仍然存活,无法放入survivor空间内(1mb),只好通过分配担保机制提前转移到老年代区,等本次垃圾收集完毕后,
4mb的对象分配在eden区中,老年代占用6mb(有三个2mb的对象)。
大对象直接进入老年代
hotspot提供了-XX:PretenureSizeThreshold参数(只针对serial和ParNew这两款收集器有效,如果是Parallel Scavenge则不支持),指定大于该设置值的对象直接在老年代分配,避免大对象在eden区和survivor区来回复制。
长期存活的对象进入老年代
每个对象都有一个对象年龄的计数器,存储在对象头中,每发生一次minor GC,新生代中仍存活的对象年龄就会加1,默认达到15后(-XX:MaxTenuringThreshold),会被晋升到老年代里
动态对象年龄判定
hotspot并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold后才能晋升到老年代,如果在survivor空间中小于等于某年龄的所有对象大小总和大于survivor空间的一半,则大于等于该年龄的对象就可以直接进入老年代(比如小于等于10岁的对象总大小超过了survivor空间的一半,则10岁或以上年龄的对象就会晋升到老年代)
空间分配担保
在jdk6之前,当发生minor GC前,jvm会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果是则进行minor GC,否则,会先查看
-XX:HandlePromotionFailure的参数值是否为true,如果允许则会检查老年代最大可用的连续空间是否会大于历次晋升老年代对象的平均大小,如果大于,则尝试进行
minor GC,如果小于,或-XX:HandlePromotionFailure设置为false,则就进行full GC而不是minor GC。
在jdk6之后,只要老年代的连续空间大于新生代对象总大小或历次晋升的平均大小,就会进行Minor GC,否则进行Full GC(相当于忽略了-XX:HandlePromotionFailure,默认就是true,不可更改)
七、虚拟机性能监控、故障处理工具
1、jstat
用于监视jvm各种运行状态信息的命令行工具。可以显示本地或远程jvm进程中的类加载、内存、垃圾收集、即时编译等运行时数据
jstat [ option pid [interval[s|ms] [count]] ]
interval和count代表查询间隔和次数,例如每250毫秒查询一次进程2764的垃圾收集状况,一共查询20次,则执行:
jstat –gc 2764 250 20
2、jmap
用于生成堆转储快照(heapdump文件,hprof结尾),也可以查询java堆和方法区详细信息,如果空间使用率、当前用的是哪种收集器
3、jstack
用于生成jvm当前时刻的线程快照(threaddump),线程快照就是当前jvm内每一条线程正在执行的方法堆栈的集合,线程出现停顿时通过jstack来查看各个线程
的调用堆栈,就可以获知没有响应的线程到底在做什么事情或者等待着什么资源
八、JVM类加载机制
Jvm把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被jvm直接使用的java类型,这个过程被称为jvm的类加载机制。
类型的加载、连接和初始化都是在程序运行期间完成的。
加载-连接-初始化-使用-卸载
类的初始化时机:
1、遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则先触发初始化。
Java中能生成这四条字节码指令的场景有:
使用new关键字实例化对象时
读取或设置一个类型的静态字段时(被final修饰、已在编译期把结果放入常量池的静态字段除外)
调用一个类型的静态方法时
2、对类进行反射调用时,如果类没有初始化,则先进行初始化
3、当初始化类时,若父类还未初始化,则先初始化父类
4、当jvm启动时,用户需要指定一个主类(包含main方法的的类),jvm会先初始化这个类
类加载过程:
一、加载
加载是类加载过程中的第一个阶段,在这个阶段jvm需要完成下面三个事情:
1、通过一个类的全限定名找到定义此类的二进制字节流(可以理解为就是class文件)
2、将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构(转换class文件为运行时数据结构)
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段完成后,class文件就按照jvm所设定的格式存储在方法区之中了
二、验证
三、准备
为类中定义的静态变量分配内存并设置类变量初始值的阶段(比如int型,设置初始值为0)
四、解析
五、初始化
在前面几个阶段里,除了在加载阶段用户可以通过自定义类加载器的方式局部参与外,其余阶段都完全由jvm主导,直到初始化阶段,
jvm才真正开始执行类中编写的java程序代码,将主导权交给应用程序(用户)。
初始化阶段其实就是执行类构造器<clinit>()方法的过程,而<clinit>()方法是javac编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并生成的,
<clinit>()方法与类的构造函数(即<init>()方法)不同,它不需要显式地调用父类构造器,jvm会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。如果是接口,
则实现类初始化时不会先执行接口的<clinit>()方法。
(比如,如果父类和子类都有static语句块,并且都有写构造器,那么当new一个子类对象时,会先执行父类的static语句块,再执行子类的static语句块,再执行父类构造器,最后执行子类构造器)
类加载器
在加载阶段,实现“通过一个类的全限定名来获取描述该类的二进制字节流“这个动作即类加载器所要做的
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确定在jvm中的唯一性。
即使两个类来源同一个class文件,被同一个jvm加载,只要类加载器不同,则这两个类必定不同。
1、启动类加载器(Bootstrap ClassLoader)
2、扩展类加载器(Extension ClassLoader)
3、应用程序类加载器(Application ClassLoader)
双亲委派模型
该模型要求除了启动类加载器外,其余的类加载器都应有自己的父类加载器。如果一个类加载器收到了类加载的请求,它会将该请求委派给父类去完成,只有父类无法完成这个加载请求时(它的搜索范围中没有找到这个类),子加载器才会尝试自己去加载这个类。
运行时栈帧结构
1、局部变量表
2、操作数栈
3、动态链接
4、方法返回地址
九、java内存模型
规定所有变量都存储在主存中,每个线程还有自己的工作内存,线程的工作内存保存了被该线程使用的变量副本,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,
而不能直接读写主存中的数据,不同的线程之间也无法直接访问对方工作内存中的变量。
关于主存与工作内存之间具体是如何交互的,有如下几个原子操作:
1、lock(使用synchronized时生效):作用于主存中的变量,把一个变量标识为一条线程独占的状态
2、unlock(使用synchronized时生效):作用于主存中的变量,把一个处于锁定状态的变量释放出来,释放变量后才可以被其他线程锁定
3、read:作用于主存变量,把一个变量值从主存中传输到线程的工作内存中
4、load:作用于工作内存的变量,把read操作从主存中得到的变量值放入工作内存的变量副本中
5、use
6、assign
7、store:作用于工作内存的变量,把工作内存中一个变量的值传送到主存中
8、write:作用于主存的变量,把store操作从工作内存中得到的变量值放入主存的变量中
所谓volatile、synchronized等保证可见性、原子性、有序性本质上都是通过上述操作的配合实现的,
另外所谓的可见性、原子性、有序性都是基于多线程并发的场景下,如果是单线程,不必考虑这些。
volatile的可见性
当一个变量被定义成volatile之后,它将具备两项特性:第一是保证可见性,这里的可见性是指当一条线程修改了这个变量的值,会立即同步到主存中,
而别的线程在访问volatile变量时,必须先从主存中刷新最新的值。
但volatile不保证原子性,体现在当线程A已经从主存中read并load最新的值后,如果此时线程B更改了变量并写入主存,那线程A中的这个变量值就不是最新的了。
所以volatile变量的适用场景是运算结果并不依赖变量的当前值。如何理解这句话呢?
比如int a = 0;
如果执行a++操作,那运算结果就依赖于a的当前值,也就是说如果当前a是1,那运算结果就是2,如果当前a是5,那运算结果就是6;
而对于boolean b = true;
如果执行set b = false;操作,则运算结果就不依赖与当前b的值,不论b当前是true或false,执行set b=false;的操作后,运算结果就是false
同样对于int c = 0;
如果执行set c = 2; 则运算结果同样不依赖当前值,那么这种情况也符合volatile的场景
volatile的有序性
volatile变量会禁止指令重排序优化,也就是访问volatile变量之前的代码不会被排序到volatile变量之后执行,而volatile变量之后的代码也不会被排到volatile变量之前来执行
原理就是程序在遇到volatile变量时,会自动增加内存屏障,这样cpu在指令重排序时不能把后面的指令重排序到内存屏障之前的位置(指令重排序无法越过内存屏障)
假设T表示一个线程,V和W分别表示两个volatile变量:
则满足:
1、在工作内存中,每次使用V前都必须先从主存刷新最新的值,用于保证能看见其他线程对变量V所做的修改
2、在工作内存中,每次修改V后都必须立刻同步会主存中,用于保证其他线程可以看到自己对变量V的修改
3、volatile变量不会被指令重排序优化,从而保证代码的执行顺序和程序的顺序相同
补充:原子性、可见性与有序性
1、原子性
java内存模型提供了lock和unlock操作来保证一系列操作集的原子性,这两个操作对应的字节码指令是monitorenter和monitorexit,这两个字节码指令反映到java代码中就是同步块-------synchronized关键字
2、可见性
volatile、synchronized、final可以保证可见性
3、有序性
volatile、synchronized保证线程之间操作的有序性
happens-before原则
时间上的先后顺序对线程来说感知上并不一定是有序的。而通过happens-before原则,可以让线程在感知上也是有序的。
比如针对int value = 0;
public void setValue(int value) {this.value = value;}
public int getValue() {return value;}
假设线程A先(时间上的先后)调用了setValue(1),然后线程B调用了同一个对象的getValue(),那么线程B的返回值是什么?
答案是不确定,尽管线程A在操作时间上先于线程B,但是无法确定线程B中getValue()方法的返回结果,换句话说,这里面的操作不是线程安全的。
解决方式是加锁或volatile修饰value
综上所述,那么什么是happens-before原则呢?
先行发生是java内存模型中定义的两项操作之间的偏序关系,比如操作A先于操作B发生,那么在发生操作B之前,操作A产生的影响能被操作B感知到。
这就是所谓的happens-before原则
java虚拟机学习笔记相关推荐
- Java 虚拟机学习笔记 | 类加载过程和对象的创建流程
前言 创建对象是 Java 语言绕不开的话题,那么对象是如何创建出来的呢?我们今天就来聊一聊.对象创建第一步就是检查类是否加载,而类的加载又牵扯到类的加载过程.如果单说对象的创建而绕开类的加载过程,感 ...
- Java虚拟机学习笔记(一)—Java虚拟机概述
一:编程语言兼容底层系统的方式大概分为两种 1.通过编译器实现兼容 例如C.C++等编程语言,既能运行与Linux系统,也能运行与Windows系统:既能运行于x86平台,也能运行于AMD平台.这种能 ...
- 深入理解JAVA虚拟机学习笔记(一)JVM内存模型
摘要: 上周末搬家后,家里的宽带一直没弄好,跟电信客服反映了N遍了终于约了个师傅明天早上来迁移宽带,可以结束一个多星期没网的痛苦日子了.这段时间也是各种忙,都一个星期没更新博客了,再不写之前那种状 ...
- Java 虚拟机学习笔记 | 运行时数据区总结
前言 要想学习好 Java,Java虚拟(JVM)的学习是绕不开的.学习 Java虚拟(JVM)首先就要先了解的就是Java虚拟(JVM)运行时数据区. 在Java语言和虚拟机规范中对运行时数据区进行 ...
- 【深入理解Java虚拟机学习笔记】第三章 垃圾收集器与内存分配策略
最近想好好复习一下java虚拟机,我想通过深读 [理解Java虚拟机 jvm 高级特性与最佳实践] (作者 周志明) 并且通过写一些博客总结来将该书读薄读透,这里文章内容仅仅是个人阅读后简短总结,加强 ...
- 【深入理解Java虚拟机学习笔记】第二章 Java 内存区域与内存溢出异常
最近想好好复习一下java虚拟机,我想通过深读 [理解Java虚拟机 jvm 高级特性与最佳实践] (作者 周志明) 并且通过写一些博客总结来将该书读薄读透,这里文章内容仅仅是个人阅读后简短总结,加强 ...
- java 准备 解析_深入理解JAVA虚拟机学习笔记24——类加载的准备和解析
每天进步一点点! 今天我们一起看一下类加载的准备阶段和解析阶段. 先看一下准备阶段:主要任务是在方法区中为类变量(仅static修饰变量,不包含实例变量)分配内存并设置类变量初始化的阶段. 这里面的区 ...
- java outofmemory_深入理解JAVA虚拟机学习笔记3——OutOfMemoryError异常
开门见山. 为了方便制造溢出,将JAVA堆的大小调整为10M. 本机用的是IntelliJ IDEA作为开发工具,进入到IDEA的安装目录,如D:\tools\IntelliJ IDEA 2017.1 ...
- java虚拟机学习笔记 【3】
为什么80%的码农都做不了架构师?>>> 认识Java虚拟机的内部体系结构 Java虚拟机的内部体系结构也许很少有人去关心,因为对于Java程序员来说,一般只需要跟API打交道 ...
- Java虚拟机学习笔记(一)--运行时数据区域
强烈推荐一个大神的人工智能的教程:http://www.captainbed.net/zhanghan 前言 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分成若干个不同的数据区域. 程 ...
最新文章
- DotNetSpeech.dll的使用
- mysql 事务值被改变_面试被问MySQL 事务的实现原理,怎么破?
- C++ Primer 5th笔记(chap 14 重载运算和类型转换)重载运算概述
- 我学到的5件事,指导2,500名有抱负的开发人员
- python opencv人脸解锁_教你使用python+Opencv完成人脸解锁
- 【刷题】BZOJ 1003 [ZJOI2006]物流运输
- Java实现PDF生成(Word文档转Pdf)
- 图结构 计算机视觉,探索图结构数据上的数据增强
- access 分组序号,使用Access SQL进行分组排名
- 这是一个没有标题的故事
- java程序员要学什么?
- 『团队协作的五大障碍』读书所得
- 一步步解密微商城系统开发流程
- python中的header_python中header是什么意思
- 扣丁软件测试基础知识,苹果无线充电线圈揭秘,iphone8无线充电线圈介绍
- tomcat和HTTP(r equest response )
- dev c 扫雷程序代码c语言,C语言 扫雷程序的实现
- Java接口的定义与实现
- window.print 添加页眉页脚
- 生活有哪些残忍的真相?
热门文章
- 免费Linux CAD应用软件
- 车牌号校验规则,包括新能源车
- 林纳斯·托瓦兹(Linus Torvalds)为什么被称作大神?
- HDU-5172-GTY's gay friends-线段树单点更新
- 源码深度解析系列之 Spring IOC
- docker安装nextcloud+onlyoffice+https
- Linux Centos7 Apache 访问 You don't have permission to access / on this server.
- UIWebView的用法
- 天线工程手册_“大神”给工控工程师快速成长的6点建议,看完你会少走弯路...
- Windows10 通过隧道进行远程桌面连接